A reasonable configuration language rcl-lang.org
configuration-language json
at master 251 lines 9.1 kB view raw
1// RCL -- A reasonable configuration language. 2// Copyright 2025 Ruud van Asseldonk 3 4// Licensed under the Apache License, Version 2.0 (the "License"); 5// you may not use this file except in compliance with the License. 6// A copy of the License has been included in the root of the repository. 7 8//! The implementation of `rcl patch`. 9 10use crate::cst::{Expr, Seq, SeqControl, Stmt, Yield}; 11use crate::error::{IntoError, Result}; 12use crate::loader::Loader; 13use crate::pprint::{concat, Doc}; 14use crate::source::{DocId, Span}; 15use crate::string::is_identifier; 16 17pub struct Patcher<'a> { 18 path: Vec<&'a str>, 19 replacement: Expr, 20} 21 22impl<'a> Patcher<'a> { 23 /// Prepare a patcher by parsing the path and replacement. 24 pub fn new(loader: &mut Loader, path: &'a str, replacement: String) -> Result<Self> { 25 let path_id = loader.load_string("path", path.to_string()); 26 let replacement_id = loader.load_string("replacement", replacement); 27 let result = Patcher { 28 path: parse_path_expr(path, path_id)?, 29 replacement: loader.get_cst(replacement_id)?, 30 }; 31 Ok(result) 32 } 33 34 /// Apply the patch to the `source_cst`. 35 /// 36 /// Because this swaps the replacement CST into place, the patcher cannot be 37 /// reused afterwards. 38 pub fn apply(mut self, input: &str, source_span: Span, source_cst: &mut Expr) -> Result<()> { 39 patch_expr( 40 input, 41 &self.path, 42 source_cst, 43 source_span, 44 &mut self.replacement, 45 ) 46 } 47} 48 49/// Parse a document path expression. 50/// 51/// A path is a sequence of identifiers separated by dots, e.g. `foo.bar.baz`. 52/// 53/// We take a `DocId`, to be able to report a span on error, so we can highlight 54/// the exact span of the problem. 55fn parse_path_expr(path: &str, doc_id: DocId) -> Result<Vec<&str>> { 56 let mut result = Vec::new(); 57 let mut start = 0; 58 loop { 59 let (has_more, end) = match path.bytes().skip(start).position(|b| b == b'.') { 60 Some(i) => (true, start + i), 61 None => (false, path.len()), 62 }; 63 let ident = &path[start..end]; 64 65 if is_identifier(ident) { 66 result.push(ident); 67 start = end + 1; 68 } else { 69 let err_span = Span::new(doc_id, start, end); 70 return err_span 71 .error("This path segment is not a valid identifier.") 72 .with_help( 73 "A document path can only contain identifiers, \ 74 not list indexes or arbitrary keys.", 75 ) 76 .err(); 77 } 78 if !has_more { 79 return Ok(result); 80 } 81 } 82} 83 84/// Splice the replacement into the source CST at the given path. 85/// 86/// If a match was found, `replacement` will contain the replaced node. That is, 87/// we swap `replacement` with the target node. 88/// 89/// We do not attempt to fix up the span information in the source CST, which 90/// means that it's bogus after patching. We could try to do that, and we could 91/// even abstract, typecheck and evaluate the new CST afterwards, but error 92/// reporting in such a spliced CST will be tricky: although spans of individual 93/// expressions are correct, when we highlight them in an error message, it will 94/// highlight the original document, which can then be confusing. Imagine we 95/// have this document: 96/// ```rcl 97/// let x: Number = 42; 98/// ``` 99/// and we patch path `x` with new value `"foobar"`. This creates a type error, 100/// which we could report at document `cmdline`, and highlight the `"foobar"` 101/// itself. But we also point to the expected type, and then we'd print out the 102/// original 42, and it would be confusing. Better to just save the patched 103/// document first and evaluate it afterwards. 104fn patch_expr( 105 input: &str, 106 path: &[&str], 107 source: &mut Expr, 108 source_span: Span, 109 replacement: &mut Expr, 110) -> Result<()> { 111 let target = match path.first() { 112 Some(tf) => tf, 113 None => { 114 // If the path is empty, then we have arrived at the final target, 115 // and we need to replace the source node itself. 116 std::mem::swap(source, replacement); 117 return Ok(()); 118 } 119 }; 120 121 // There are multiple cases below where we want to report this error defined 122 // here, with the entire source span as the search space, but we don't want 123 // to allocate the `Error` in the success case, so we construct it lazily. 124 let make_err = || { 125 let msg = concat! { 126 "Could not find '" 127 Doc::highlight(target).into_owned() 128 "' in this expression." 129 }; 130 source_span.error(msg).err() 131 }; 132 133 match source { 134 Expr::BraceLit { elements, .. } | Expr::BracketLit { elements, .. } => { 135 for element in elements.elements.iter_mut() { 136 match patch_seq(input, path, element, replacement) { 137 Some(result) => return result, 138 None => continue, 139 } 140 } 141 make_err() 142 } 143 Expr::Statements { 144 stmts, 145 body_span, 146 body, 147 } => { 148 // First, we try to match any let bindings among the statements 149 // against the target. 150 for (_span, stmt) in stmts.iter_mut() { 151 match patch_stmt(input, path, &mut stmt.inner, replacement) { 152 Some(result) => return result, 153 None => continue, 154 } 155 } 156 // If that did not match, then we descend further into the body, and 157 // try to match there. We have a choice for error reporting here: if 158 // we fail to match, we could blame it on the entire `Statements` 159 // expression, or only on the body. The correct thing to do would 160 // be to map the error here and blame it on the entire expression. 161 // However, because we cite only the first line of the error span, 162 // that will highlight the first statement, which may not even be 163 // a let-binding. I expect that keeping the blame on the body is 164 // slightly clearer. 165 patch_expr(input, path, &mut body.inner, *body_span, replacement) 166 } 167 _ => make_err(), 168 } 169} 170 171/// Splice the replacement into the source CST at the given path, if the path matches. 172/// 173/// The path must not be empty, because seq elements themselves cannot be the 174/// target of a replacement, only the right-hand side of bindings can. 175/// 176/// Returns `Ok` if the substitution was applied, `Err` if it failed inside, and 177/// `None` if the path did not match anywhere. 178fn patch_seq( 179 input: &str, 180 path: &[&str], 181 source: &mut Seq, 182 replacement: &mut Expr, 183) -> Option<Result<()>> { 184 // First we check for any let bindings before the yield. 185 for control in source.control.iter_mut() { 186 match &mut control.inner { 187 SeqControl::Stmt { stmt } => match patch_stmt(input, path, stmt, replacement) { 188 Some(result) => return Some(result), 189 None => continue, 190 }, 191 _ => continue, 192 } 193 } 194 195 let (target, suffix) = path 196 .split_first() 197 .expect("Should not call `patch_seq` with empty path."); 198 199 // If there is no match yet, we try to match the yield itself, if it has 200 // record form (`ident = expr`). 201 match &mut source.body.inner { 202 Yield::AssocIdent { 203 field, 204 value, 205 value_span, 206 .. 207 } if field.resolve(input) == *target => { 208 // We matched one segment of the path, now it's up to the inner 209 // expression to match. If that fails and returns Err, we do *not* 210 // continue the search to see if anything else might match, we 211 // greedily follow the path by first matches only. 212 Some(patch_expr(input, suffix, value, *value_span, replacement)) 213 } 214 _ => None, 215 } 216} 217 218/// Splice the replacement into the source CST at the given path. 219/// 220/// If the path matches this statement, either the substitution succeeds, and 221/// we return `Some(Ok)`, or it fails somewhere inside, and we return `Some(Err)`. 222/// If the path does not match this statement, we return `None`. 223/// 224/// The path must not be empty, because statements themselves cannot be the 225/// target of a replacement, only the right-hand side of let bindings can. 226fn patch_stmt( 227 input: &str, 228 path: &[&str], 229 source: &mut Stmt, 230 replacement: &mut Expr, 231) -> Option<Result<()>> { 232 let (target, suffix) = path 233 .split_first() 234 .expect("Should not call `patch_stmt` with empty path."); 235 236 match source { 237 Stmt::Let { 238 ident, 239 value, 240 value_span, 241 .. 242 } if ident.resolve(input) == *target => Some(patch_expr( 243 input, 244 suffix, 245 &mut *value, 246 *value_span, 247 replacement, 248 )), 249 _ => None, 250 } 251}