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