Skip to main content

aver/
ast_rewrite.rs

1//! Ident-replacement traversal that respects match-arm pattern scoping.
2//!
3//! Several passes (verify-trace local bindings in the parser, law-auto
4//! sample expansion in the Lean codegen) need to walk an `Expr` tree and
5//! substitute bare `Ident`s. Writing one walker per site is tempting,
6//! but the match-arm scoping logic is subtle — patterns introduce new
7//! bindings that shadow outer names — and each copy picked up the same
8//! shadowing bug independently. This module owns the traversal so a
9//! single fix keeps every site correct.
10//!
11//! [`rewrite_idents_scoped`] is the generic entry point: you pass an
12//! expression and a callback that maps an `Ident` name to an optional
13//! replacement (`None` leaves the identifier alone). Pattern shadowing
14//! is handled automatically — inside a `Match { arms: [(pat, body)] }`,
15//! names bound by `pat` are ignored by the callback for the duration of
16//! `body`.
17
18use std::collections::HashSet;
19
20use crate::ast::{Expr, MatchArm, Pattern, Spanned, StrPart, TailCallData};
21
22/// Collect every name introduced by a match-arm pattern. Composite
23/// patterns (tuple, cons, constructor) recursively contribute all their
24/// sub-bindings; wildcards and literals contribute nothing.
25pub fn pattern_binding_names(pattern: &Pattern) -> Vec<String> {
26    let mut out = Vec::new();
27    collect_pattern_bindings(pattern, &mut out);
28    out
29}
30
31fn collect_pattern_bindings(pattern: &Pattern, out: &mut Vec<String>) {
32    match pattern {
33        Pattern::Ident(name) => out.push(name.clone()),
34        Pattern::Cons(head, tail) => {
35            out.push(head.clone());
36            out.push(tail.clone());
37        }
38        Pattern::Tuple(items) => {
39            for item in items {
40                collect_pattern_bindings(item, out);
41            }
42        }
43        Pattern::Constructor(_, binders) => {
44            for name in binders {
45                out.push(name.clone());
46            }
47        }
48        Pattern::Wildcard | Pattern::Literal(_) | Pattern::EmptyList => {}
49    }
50}
51
52/// Walk `expr` and replace `Ident(name)` occurrences via `rewrite`.
53///
54/// Inside a `Match`, names introduced by an arm's pattern are hidden
55/// from the callback while its body is rewritten — the inner pattern
56/// binding shadows the outer substitution, so
57/// `let x = 1; match Option.Some(2) { Some(x) -> x }` leaves the body's
58/// `x` alone (→ 2) instead of replacing it with the outer `1`.
59///
60/// `rewrite` returns `Some(expr)` to substitute, `None` to leave the
61/// identifier as-is. `Resolved { name }` nodes are also considered
62/// (same shape as `Ident` from a substitution standpoint), but only
63/// when not shadowed.
64pub fn rewrite_idents_scoped<F>(expr: &Spanned<Expr>, mut rewrite: F) -> Spanned<Expr>
65where
66    F: FnMut(&str) -> Option<Spanned<Expr>>,
67{
68    let scope = HashSet::new();
69    rewrite_inner(expr, &scope, &mut rewrite)
70}
71
72fn rewrite_inner<F>(expr: &Spanned<Expr>, scope: &HashSet<String>, rewrite: &mut F) -> Spanned<Expr>
73where
74    F: FnMut(&str) -> Option<Spanned<Expr>>,
75{
76    let line = expr.line;
77    match &expr.node {
78        Expr::Ident(name) => {
79            if scope.contains(name) {
80                Spanned::new(expr.node.clone(), line)
81            } else {
82                rewrite(name).unwrap_or_else(|| Spanned::new(expr.node.clone(), line))
83            }
84        }
85        Expr::Resolved { name, .. } => {
86            if scope.contains(name) {
87                Spanned::new(expr.node.clone(), line)
88            } else {
89                rewrite(name).unwrap_or_else(|| Spanned::new(expr.node.clone(), line))
90            }
91        }
92        Expr::Literal(_) => Spanned::new(expr.node.clone(), line),
93        Expr::Attr(inner, field) => Spanned::new(
94            Expr::Attr(
95                Box::new(rewrite_inner(inner, scope, rewrite)),
96                field.clone(),
97            ),
98            line,
99        ),
100        Expr::FnCall(callee, args) => Spanned::new(
101            Expr::FnCall(
102                Box::new(rewrite_inner(callee, scope, rewrite)),
103                args.iter()
104                    .map(|a| rewrite_inner(a, scope, rewrite))
105                    .collect(),
106            ),
107            line,
108        ),
109        Expr::BinOp(op, l, r) => Spanned::new(
110            Expr::BinOp(
111                *op,
112                Box::new(rewrite_inner(l, scope, rewrite)),
113                Box::new(rewrite_inner(r, scope, rewrite)),
114            ),
115            line,
116        ),
117        Expr::Neg(inner) => Spanned::new(
118            Expr::Neg(Box::new(rewrite_inner(inner, scope, rewrite))),
119            line,
120        ),
121        Expr::Match { subject, arms } => {
122            let new_subject = Box::new(rewrite_inner(subject, scope, rewrite));
123            let new_arms = arms
124                .iter()
125                .map(|arm| {
126                    let shadowed = pattern_binding_names(&arm.pattern);
127                    if shadowed.is_empty() {
128                        MatchArm::new(
129                            arm.pattern.clone(),
130                            rewrite_inner(&arm.body, scope, rewrite),
131                        )
132                    } else {
133                        let mut extended = scope.clone();
134                        for name in shadowed {
135                            extended.insert(name);
136                        }
137                        MatchArm::new(
138                            arm.pattern.clone(),
139                            rewrite_inner(&arm.body, &extended, rewrite),
140                        )
141                    }
142                })
143                .collect();
144            Spanned::new(
145                Expr::Match {
146                    subject: new_subject,
147                    arms: new_arms,
148                },
149                line,
150            )
151        }
152        Expr::Constructor(name, payload) => Spanned::new(
153            Expr::Constructor(
154                name.clone(),
155                payload
156                    .as_ref()
157                    .map(|inner| Box::new(rewrite_inner(inner, scope, rewrite))),
158            ),
159            line,
160        ),
161        Expr::ErrorProp(inner) => Spanned::new(
162            Expr::ErrorProp(Box::new(rewrite_inner(inner, scope, rewrite))),
163            line,
164        ),
165        Expr::InterpolatedStr(parts) => Spanned::new(
166            Expr::InterpolatedStr(
167                parts
168                    .iter()
169                    .map(|part| match part {
170                        StrPart::Literal(s) => StrPart::Literal(s.clone()),
171                        StrPart::Parsed(inner) => {
172                            StrPart::Parsed(Box::new(rewrite_inner(inner, scope, rewrite)))
173                        }
174                    })
175                    .collect(),
176            ),
177            line,
178        ),
179        Expr::List(items) => Spanned::new(
180            Expr::List(
181                items
182                    .iter()
183                    .map(|i| rewrite_inner(i, scope, rewrite))
184                    .collect(),
185            ),
186            line,
187        ),
188        Expr::Tuple(items) => Spanned::new(
189            Expr::Tuple(
190                items
191                    .iter()
192                    .map(|i| rewrite_inner(i, scope, rewrite))
193                    .collect(),
194            ),
195            line,
196        ),
197        Expr::IndependentProduct(items, flag) => Spanned::new(
198            Expr::IndependentProduct(
199                items
200                    .iter()
201                    .map(|i| rewrite_inner(i, scope, rewrite))
202                    .collect(),
203                *flag,
204            ),
205            line,
206        ),
207        Expr::MapLiteral(entries) => Spanned::new(
208            Expr::MapLiteral(
209                entries
210                    .iter()
211                    .map(|(k, v)| {
212                        (
213                            rewrite_inner(k, scope, rewrite),
214                            rewrite_inner(v, scope, rewrite),
215                        )
216                    })
217                    .collect(),
218            ),
219            line,
220        ),
221        Expr::RecordCreate { type_name, fields } => Spanned::new(
222            Expr::RecordCreate {
223                type_name: type_name.clone(),
224                fields: fields
225                    .iter()
226                    .map(|(n, v)| (n.clone(), rewrite_inner(v, scope, rewrite)))
227                    .collect(),
228            },
229            line,
230        ),
231        Expr::RecordUpdate {
232            type_name,
233            base,
234            updates,
235        } => Spanned::new(
236            Expr::RecordUpdate {
237                type_name: type_name.clone(),
238                base: Box::new(rewrite_inner(base, scope, rewrite)),
239                updates: updates
240                    .iter()
241                    .map(|(n, v)| (n.clone(), rewrite_inner(v, scope, rewrite)))
242                    .collect(),
243            },
244            line,
245        ),
246        Expr::TailCall(data) => Spanned::new(
247            Expr::TailCall(Box::new(TailCallData::new(
248                data.target.clone(),
249                data.args
250                    .iter()
251                    .map(|a| rewrite_inner(a, scope, rewrite))
252                    .collect(),
253            ))),
254            line,
255        ),
256    }
257}
258
259#[cfg(test)]
260mod tests {
261    use super::*;
262    use crate::ast::{BinOp, Literal};
263
264    fn bare(e: Expr) -> Spanned<Expr> {
265        Spanned::new(e, 1)
266    }
267
268    fn int(n: i64) -> Spanned<Expr> {
269        bare(Expr::Literal(Literal::Int(n)))
270    }
271
272    fn ident(s: &str) -> Spanned<Expr> {
273        bare(Expr::Ident(s.to_string()))
274    }
275
276    #[test]
277    fn pattern_shadowing_leaves_inner_bound_ident_alone() {
278        // match Some(2) { Some(x) -> x, None -> 0 } with outer x = 1.
279        let e = bare(Expr::Match {
280            subject: Box::new(bare(Expr::Constructor(
281                "Option.Some".to_string(),
282                Some(Box::new(int(2))),
283            ))),
284            arms: vec![
285                MatchArm::new(
286                    Pattern::Constructor("Option.Some".to_string(), vec!["x".to_string()]),
287                    ident("x"),
288                ),
289                MatchArm::new(
290                    Pattern::Constructor("Option.None".to_string(), vec![]),
291                    int(0),
292                ),
293            ],
294        });
295        let out = rewrite_idents_scoped(&e, |n| if n == "x" { Some(int(1)) } else { None });
296        let Expr::Match { arms, .. } = &out.node else {
297            panic!("expected Match");
298        };
299        assert!(
300            matches!(&arms[0].body.node, Expr::Ident(s) if s == "x"),
301            "pattern-bound x should not be substituted: {:?}",
302            arms[0].body.node
303        );
304    }
305
306    #[test]
307    fn tuple_pattern_shadowing() {
308        let e = bare(Expr::Match {
309            subject: Box::new(bare(Expr::Tuple(vec![int(1), int(2)]))),
310            arms: vec![MatchArm::new(
311                Pattern::Tuple(vec![
312                    Pattern::Ident("a".to_string()),
313                    Pattern::Ident("b".to_string()),
314                ]),
315                bare(Expr::BinOp(
316                    BinOp::Add,
317                    Box::new(ident("a")),
318                    Box::new(ident("b")),
319                )),
320            )],
321        });
322        let out = rewrite_idents_scoped(&e, |n| if n == "a" { Some(int(99)) } else { None });
323        let Expr::Match { arms, .. } = &out.node else {
324            panic!();
325        };
326        let Expr::BinOp(_, l, _) = &arms[0].body.node else {
327            panic!();
328        };
329        assert!(
330            matches!(&l.node, Expr::Ident(s) if s == "a"),
331            "tuple-pattern `a` should shadow outer substitution: {:?}",
332            l.node
333        );
334    }
335
336    #[test]
337    fn rewrites_in_non_shadowed_arm() {
338        let e = bare(Expr::Match {
339            subject: Box::new(int(42)),
340            arms: vec![MatchArm::new(Pattern::Wildcard, ident("x"))],
341        });
342        let out = rewrite_idents_scoped(&e, |n| if n == "x" { Some(int(7)) } else { None });
343        let Expr::Match { arms, .. } = &out.node else {
344            panic!();
345        };
346        assert!(matches!(&arms[0].body.node, Expr::Literal(Literal::Int(7))));
347    }
348
349    #[test]
350    fn cons_pattern_shadows_head_and_tail() {
351        let e = bare(Expr::Match {
352            subject: Box::new(bare(Expr::List(vec![int(1), int(2)]))),
353            arms: vec![MatchArm::new(
354                Pattern::Cons("h".to_string(), "t".to_string()),
355                bare(Expr::Tuple(vec![ident("h"), ident("t"), ident("z")])),
356            )],
357        });
358        let out = rewrite_idents_scoped(&e, |n| match n {
359            "h" => Some(int(100)),
360            "t" => Some(int(200)),
361            "z" => Some(int(300)),
362            _ => None,
363        });
364        let Expr::Match { arms, .. } = &out.node else {
365            panic!();
366        };
367        let Expr::Tuple(items) = &arms[0].body.node else {
368            panic!();
369        };
370        // h, t shadowed (stay as Ident); z rewritten to 300.
371        assert!(matches!(&items[0].node, Expr::Ident(s) if s == "h"));
372        assert!(matches!(&items[1].node, Expr::Ident(s) if s == "t"));
373        assert!(matches!(&items[2].node, Expr::Literal(Literal::Int(300))));
374    }
375}