1use harn_lexer::{FixEdit, Lexer, Span, StringSegment, Token, TokenKind};
13
14use crate::ast_json::structural_projection;
15use crate::interpolation;
16use crate::operators::{operand_needs_parens, OperandSide, OperatorGrammar, Precedence};
17use crate::operators::{RANGE_EXCLUSIVE, RANGE_INCLUSIVE};
18use crate::{visit, Node, Parser, SNode, REMOVED_RANGE_KEYWORD};
19
20#[derive(Debug, Clone, PartialEq, Eq)]
22pub enum OperatorMigrationError {
23 LegacyParse(String),
25 CurrentParse(String),
27 MeaningChanged { line: usize },
30}
31
32impl std::fmt::Display for OperatorMigrationError {
33 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
34 match self {
35 Self::LegacyParse(error) => {
36 write!(
37 f,
38 "does not parse under the legacy operator grammar: {error}"
39 )
40 }
41 Self::CurrentParse(error) => {
42 write!(
43 f,
44 "rewrite does not parse under the current grammar: {error}"
45 )
46 }
47 Self::MeaningChanged { line } => write!(
48 f,
49 "rewrite would change the meaning of the item starting on line {line}"
50 ),
51 }
52 }
53}
54
55pub fn migrate_legacy_operators(source: &str) -> Result<Option<String>, OperatorMigrationError> {
57 let legacy = parse_program(source, OperatorGrammar::LEGACY)
58 .map_err(OperatorMigrationError::LegacyParse)?;
59 let mut edits = Vec::new();
60 collect_program_edits(source, &legacy.tokens, &legacy.program, &mut edits);
61 if edits.is_empty() {
62 return Ok(None);
63 }
64 let migrated = FixEdit::apply_all(source, &edits);
65 let current = parse_program(&migrated, OperatorGrammar::CURRENT)
66 .map_err(OperatorMigrationError::CurrentParse)?;
67 if let Some(line) = first_divergent_item(&legacy.program, ¤t.program) {
68 return Err(OperatorMigrationError::MeaningChanged { line });
69 }
70 Ok(Some(migrated))
71}
72
73struct Parsed {
74 tokens: Vec<Token>,
75 program: Vec<SNode>,
76}
77
78fn parse_program(source: &str, grammar: OperatorGrammar) -> Result<Parsed, String> {
79 let tokens = Lexer::new(source)
80 .tokenize()
81 .map_err(|error| error.to_string())?;
82 let program = Parser::new(tokens.clone())
83 .with_operator_grammar(grammar)
84 .parse()
85 .map_err(|error| error.to_string())?;
86 Ok(Parsed { tokens, program })
87}
88
89fn collect_program_edits(
90 source: &str,
91 tokens: &[Token],
92 program: &[SNode],
93 edits: &mut Vec<FixEdit>,
94) {
95 visit::walk_program(program, &mut |node| {
96 collect_node_edits(source, tokens, node, edits);
97 if let Node::InterpolatedString(segments) = &node.node {
98 for segment in segments {
99 if let StringSegment::Expression(text, line, column) = segment {
100 collect_hole_edits(source, text, *line, *column, edits);
101 }
102 }
103 }
104 });
105}
106
107fn collect_hole_edits(
108 source: &str,
109 text: &str,
110 line: usize,
111 column: usize,
112 edits: &mut Vec<FixEdit>,
113) {
114 let Ok(tokens) = interpolation::lexer(Some(source), text, line, column).tokenize() else {
115 return;
116 };
117 let Some(expression) = interpolation::parse_expression_with_grammar(
118 OperatorGrammar::LEGACY,
119 Some(source),
120 text,
121 line,
122 column,
123 ) else {
124 return;
125 };
126 collect_program_edits(source, &tokens, std::slice::from_ref(&expression), edits);
127}
128
129fn collect_node_edits(source: &str, tokens: &[Token], node: &SNode, edits: &mut Vec<FixEdit>) {
130 match &node.node {
131 Node::BinaryOp { op, left, right } => {
132 let group = op.precedence();
133 parenthesize(source, group, left, OperandSide::Left, edits);
134 parenthesize(source, group, right, OperandSide::Right, edits);
135 }
136 Node::UnaryOp { operand, .. } => {
137 parenthesize(
138 source,
139 Precedence::Prefix,
140 operand,
141 OperandSide::Prefix,
142 edits,
143 );
144 }
145 Node::Ternary {
146 condition,
147 true_expr,
148 false_expr,
149 } => {
150 let group = Precedence::Ternary;
151 parenthesize(
152 source,
153 group,
154 condition,
155 OperandSide::TernaryCondition,
156 edits,
157 );
158 parenthesize(source, group, true_expr, OperandSide::TernaryBranch, edits);
159 parenthesize(source, group, false_expr, OperandSide::TernaryBranch, edits);
160 }
161 Node::RangeExpr {
162 start,
163 end,
164 inclusive,
165 } => {
166 parenthesize(source, Precedence::Range, start, OperandSide::Left, edits);
167 parenthesize(source, Precedence::Range, end, OperandSide::Right, edits);
168 respell_range(tokens, node.span, start.span, end.span, *inclusive, edits);
169 }
170 _ => {}
171 }
172}
173
174fn parenthesize(
177 source: &str,
178 parent: Precedence,
179 operand: &SNode,
180 side: OperandSide,
181 edits: &mut Vec<FixEdit>,
182) {
183 if !operand_needs_parens(parent, Precedence::of_node(&operand.node), side)
184 || is_parenthesized(source, operand.span)
185 {
186 return;
187 }
188 edits.push(insertion(operand.span.start, "("));
189 edits.push(insertion(operand.span.end, ")"));
190}
191
192fn respell_range(
195 tokens: &[Token],
196 range: Span,
197 start: Span,
198 end: Span,
199 inclusive: bool,
200 edits: &mut Vec<FixEdit>,
201) {
202 let Some(to_index) = tokens.iter().position(|token| {
203 token.span.start >= start.end
204 && token.span.end <= end.start
205 && matches!(&token.kind, TokenKind::Identifier(word) if word == REMOVED_RANGE_KEYWORD)
206 }) else {
207 return;
208 };
209 let before = to_index
210 .checked_sub(1)
211 .map_or(tokens[to_index].span.start, |index| tokens[index].span.end);
212 let after = tokens
213 .get(to_index + 1)
214 .map_or(tokens[to_index].span.end, |token| token.span.start);
215 let operator = if inclusive {
216 RANGE_INCLUSIVE
217 } else {
218 RANGE_EXCLUSIVE
219 };
220 edits.push(replacement(before, after, operator));
221 if inclusive {
222 return;
223 }
224 if let Some(exclusive_index) = tokens.iter().position(|token| {
225 token.span.start >= end.end
226 && token.span.end <= range.end
227 && matches!(&token.kind, TokenKind::Identifier(word) if word == "exclusive")
228 }) {
229 let removal_start = tokens[exclusive_index - 1].span.end;
230 edits.push(replacement(
231 removal_start,
232 tokens[exclusive_index].span.end,
233 "",
234 ));
235 }
236}
237
238fn is_parenthesized(source: &str, span: Span) -> bool {
240 let before = source
241 .get(..span.start)
242 .and_then(|text| text.trim_end().chars().next_back());
243 let after = source
244 .get(span.end..)
245 .and_then(|text| text.trim_start().chars().next());
246 before == Some('(') && after == Some(')')
247}
248
249fn insertion(offset: usize, text: &str) -> FixEdit {
250 replacement(offset, offset, text)
251}
252
253fn replacement(start: usize, end: usize, text: &str) -> FixEdit {
254 FixEdit {
255 span: Span::with_offsets(start, end, 0, 0),
256 replacement: text.to_string(),
257 }
258}
259
260fn first_divergent_item(legacy: &[SNode], current: &[SNode]) -> Option<usize> {
262 let items = legacy.len().max(current.len());
263 (0..items).find_map(|index| {
264 let before = legacy.get(index);
265 let after = current.get(index);
266 let same = match (before, after) {
267 (Some(before), Some(after)) => {
268 structural_projection(std::slice::from_ref(before), OperatorGrammar::LEGACY)
269 == structural_projection(std::slice::from_ref(after), OperatorGrammar::CURRENT)
270 }
271 _ => false,
272 };
273 (!same).then(|| before.or(after).map_or(0, |item| item.span.line))
274 })
275}
276
277#[cfg(test)]
278mod tests {
279 use super::*;
280
281 fn migrate(source: &str) -> String {
282 migrate_legacy_operators(source)
283 .expect("migrates")
284 .unwrap_or_else(|| source.to_string())
285 }
286
287 #[test]
288 fn respells_ranges() {
289 assert_eq!(migrate("for i in 1 to 3 { }\n"), "for i in 1..=3 { }\n");
290 assert_eq!(
291 migrate("for i in 0 to n exclusive { }\n"),
292 "for i in 0..n { }\n"
293 );
294 assert_eq!(
295 migrate("let r = 0 to len(xs) - 1\n"),
296 "let r = 0..=len(xs) - 1\n"
297 );
298 }
299
300 #[test]
301 fn keeps_the_legacy_grouping_of_nil_coalescing() {
302 assert_eq!(migrate("let x = b ?? 0 + 1\n"), "let x = (b ?? 0) + 1\n");
303 assert_eq!(migrate("let x = a + b ?? 1\n"), "let x = a + (b ?? 1)\n");
304 assert_eq!(migrate("let x = (a ?? 0) + 1\n"), "let x = (a ?? 0) + 1\n");
305 assert_eq!(migrate("let x = a ?? b == c\n"), "let x = a ?? b == c\n");
306 }
307
308 #[test]
309 fn parenthesizes_bounds_that_now_bind_looser_than_a_range() {
310 assert_eq!(migrate("let r = 0 to n ?? 5\n"), "let r = 0..=(n ?? 5)\n");
311 assert_eq!(
312 migrate("let r = c ? 1 : 2 to 5\n"),
313 "let r = (c ? 1 : 2)..=5\n"
314 );
315 }
316
317 #[test]
318 fn ignores_positions_held_outside_span_fields() {
319 assert_eq!(
321 migrate(
322 "fn f() {\n for i in 1 to 6 {\n if i == 3 {\n continue\n }\n }\n}\n"
323 ),
324 "fn f() {\n for i in 1..=6 {\n if i == 3 {\n continue\n }\n }\n}\n"
325 );
326 }
327
328 #[test]
329 fn migrates_interpolation_holes_in_place() {
330 assert_eq!(
331 migrate("let s = \"${b ?? 0 + 1} and ${1 to 2}\"\n"),
332 "let s = \"${(b ?? 0) + 1} and ${1..=2}\"\n"
333 );
334 }
335
336 #[test]
337 fn is_idempotent_on_migrated_source() {
338 let once = migrate("let x = b ?? 0 + 1\nfor i in 0 to n exclusive { }\n");
339 assert_eq!(migrate_legacy_operators(&once), Ok(None));
340 }
341}