A reasonable configuration language
rcl-lang.org
configuration-language
json
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}