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::{operand_needs_parens, OperandSide, OperatorGrammar, Precedence};
17use crate::operators::{RANGE_EXCLUSIVE, RANGE_INCLUSIVE};
18use crate::{visit, Node, Parser, SNode, REMOVED_RANGE_KEYWORD};
19
20/// Why a file could not be migrated.
21#[derive(Debug, Clone, PartialEq, Eq)]
22pub enum OperatorMigrationError {
23    /// The source does not parse under the legacy grammar.
24    LegacyParse(String),
25    /// The rewritten source does not parse under the current grammar.
26    CurrentParse(String),
27    /// The rewritten source parses to a different tree. `line` is where the
28    /// first differing top-level item starts in the original source.
29    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
55/// Migrate `source`. Returns `Ok(None)` when the file needs no change.
56pub 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, &current.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
174/// Wrap `operand` in parentheses when the current grammar would not keep it
175/// as this operand and the source does not already group it.
176fn 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
192/// Replace `to` (with the whitespace around it) by `..=` or `..`, and drop an
193/// `exclusive` suffix.
194fn 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
238/// Whether the text around `span` is a `(` ... `)` pair enclosing exactly it.
239fn 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
260/// The source line of the first top-level item whose meaning differs.
261fn 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        // `IfElse` stores `then_span`, which shifts when a range respells.
320        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}