Skip to main content

harn_parser/
operator_migration.rs

1//! Rewrite source written for the pre-0.11 operator grammar.
2//!
3//! The old grammar spelled ranges `a to b` (inclusive) and `a to b exclusive`,
4//! placed range formation just above `|>`, and bound `??` tighter than `+`.
5//! [`migrate_legacy_operators`] parses a file under
6//! [`OperatorGrammar::LEGACY`], rewrites every range to `a..=b` or `a..b`, and
7//! inserts parentheses wherever the current grammar would group an operand
8//! differently. It then parses the result under the current grammar and
9//! refuses the rewrite unless both trees match, so a migrated file always
10//! keeps its meaning. Interpolation holes are migrated in place.
11
12use 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/// Why a file could not be migrated.
23#[derive(Debug, Clone, PartialEq, Eq)]
24pub enum OperatorMigrationError {
25    /// The source does not parse under the legacy grammar.
26    LegacyParse(String),
27    /// The rewritten source does not parse under the current grammar.
28    CurrentParse(String),
29    /// The rewritten source parses to a different tree. `line` is where the
30    /// first differing top-level item starts in the original source.
31    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
57/// Migrate `source`. Returns `Ok(None)` when the file needs no change.
58pub 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, &current.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
176/// Wrap `operand` in parentheses when the current grammar would not keep it
177/// as this operand and the source does not already group it.
178fn 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
196/// Replace `to` (with the whitespace around it) by `..=` or `..`, and drop an
197/// `exclusive` suffix.
198fn 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
253/// The source line of the first top-level item whose meaning differs.
254fn 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        // `IfElse` stores `then_span`, which shifts when a range respells.
313        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}