Skip to main content

panproto_expr/
subst.rs

1//! Substitution and free-variable analysis for expressions.
2
3use std::sync::Arc;
4
5use rustc_hash::FxHashSet;
6
7use crate::{Expr, Pattern};
8
9/// Collect all free variables in an expression.
10#[must_use]
11pub fn free_vars(expr: &Expr) -> FxHashSet<Arc<str>> {
12    let mut vars = FxHashSet::default();
13    collect_free(expr, &mut FxHashSet::default(), &mut vars);
14    vars
15}
16
17/// Walk `expr`, adding every variable occurrence not covered by an
18/// enclosing binder to `free`.
19///
20/// `bound` is a scratch scope shared across the whole walk. Every binding
21/// form restores it on exit by removing exactly the names this frame
22/// inserted: a name already present came from an enclosing binder that
23/// still shadows the sibling subtrees, so removing it would report bound
24/// occurrences as free, while leaving a newly inserted name behind would
25/// hide genuinely free occurrences in the siblings that follow.
26fn collect_free(expr: &Expr, bound: &mut FxHashSet<Arc<str>>, free: &mut FxHashSet<Arc<str>>) {
27    match expr {
28        Expr::Var(name) => {
29            if !bound.contains(name) {
30                free.insert(Arc::clone(name));
31            }
32        }
33        Expr::Lam(param, body) => {
34            let newly_bound = bound.insert(Arc::clone(param));
35            collect_free(body, bound, free);
36            if newly_bound {
37                bound.remove(param);
38            }
39        }
40        Expr::App(func, arg) => {
41            collect_free(func, bound, free);
42            collect_free(arg, bound, free);
43        }
44        Expr::Lit(_) => {}
45        Expr::Record(fields) => {
46            for (_, v) in fields {
47                collect_free(v, bound, free);
48            }
49        }
50        Expr::List(items) => {
51            for item in items {
52                collect_free(item, bound, free);
53            }
54        }
55        Expr::Field(expr, _) => collect_free(expr, bound, free),
56        Expr::Index(expr, idx) => {
57            collect_free(expr, bound, free);
58            collect_free(idx, bound, free);
59        }
60        Expr::Match { scrutinee, arms } => {
61            collect_free(scrutinee, bound, free);
62            for (pat, body) in arms {
63                let pat_vars = pattern_vars(pat);
64                let mut inserted = Vec::new();
65                for v in &pat_vars {
66                    if bound.insert(Arc::clone(v)) {
67                        inserted.push(Arc::clone(v));
68                    }
69                }
70                collect_free(body, bound, free);
71                for v in &inserted {
72                    bound.remove(v);
73                }
74            }
75        }
76        Expr::Let { name, value, body } => {
77            collect_free(value, bound, free);
78            let newly_bound = bound.insert(Arc::clone(name));
79            collect_free(body, bound, free);
80            if newly_bound {
81                bound.remove(name);
82            }
83        }
84        Expr::Builtin(_, args) => {
85            for arg in args {
86                collect_free(arg, bound, free);
87            }
88        }
89    }
90}
91
92/// Collect all variable names bound by a pattern.
93#[must_use]
94pub fn pattern_vars(pat: &Pattern) -> Vec<Arc<str>> {
95    let mut vars = Vec::new();
96    collect_pattern_vars(pat, &mut vars);
97    vars
98}
99
100fn collect_pattern_vars(pat: &Pattern, vars: &mut Vec<Arc<str>>) {
101    match pat {
102        Pattern::Wildcard | Pattern::Lit(_) => {}
103        Pattern::Var(name) => vars.push(Arc::clone(name)),
104        Pattern::Record(fields) => {
105            for (_, p) in fields {
106                collect_pattern_vars(p, vars);
107            }
108        }
109        Pattern::List(items) => {
110            for p in items {
111                collect_pattern_vars(p, vars);
112            }
113        }
114        Pattern::Constructor(_, args) => {
115            for p in args {
116                collect_pattern_vars(p, vars);
117            }
118        }
119    }
120}
121
122/// Rename every occurrence of the bound variable `from` to `to` inside a
123/// pattern.
124///
125/// Only binding occurrences live in a pattern, so the rewrite is a plain
126/// structural walk: constructor names, record field labels, literals, and
127/// wildcards are untouched.
128fn rename_pattern_var(pat: &Pattern, from: &str, to: &Arc<str>) -> Pattern {
129    match pat {
130        Pattern::Wildcard | Pattern::Lit(_) => pat.clone(),
131        Pattern::Var(v) => {
132            if &**v == from {
133                Pattern::Var(Arc::clone(to))
134            } else {
135                pat.clone()
136            }
137        }
138        Pattern::Record(fields) => Pattern::Record(
139            fields
140                .iter()
141                .map(|(k, p)| (Arc::clone(k), rename_pattern_var(p, from, to)))
142                .collect(),
143        ),
144        Pattern::List(items) => Pattern::List(
145            items
146                .iter()
147                .map(|p| rename_pattern_var(p, from, to))
148                .collect(),
149        ),
150        Pattern::Constructor(ctor, args) => Pattern::Constructor(
151            Arc::clone(ctor),
152            args.iter()
153                .map(|p| rename_pattern_var(p, from, to))
154                .collect(),
155        ),
156    }
157}
158
159/// The set of names a fresh binder must avoid when a binder is
160/// alpha-renamed while substituting `replacement` into `body`.
161///
162/// A fresh binder must not capture a free variable of the replacement, must
163/// not capture a variable already free in the body, and must not collide
164/// with the substitution target itself.
165fn rename_avoid_set(body: &Expr, name: &str, replacement: &Expr) -> FxHashSet<Arc<str>> {
166    let mut avoid = free_vars(replacement);
167    avoid.extend(free_vars(body));
168    avoid.insert(Arc::from(name));
169    avoid
170}
171
172/// Apply capture-avoiding substitution: replace `name` with `replacement` in `expr`.
173///
174/// Every binding form — `Lam`, `Let`, and each `Match` arm — alpha-renames
175/// its binders before descending when a binder would capture a free
176/// variable of `replacement`. A binder that shadows `name` stops the
177/// substitution instead, since no free occurrence of `name` survives under
178/// it.
179#[must_use]
180pub fn substitute(expr: &Expr, name: &str, replacement: &Expr) -> Expr {
181    match expr {
182        Expr::Var(v) => {
183            if &**v == name {
184                replacement.clone()
185            } else {
186                expr.clone()
187            }
188        }
189        Expr::Lam(param, body) => {
190            if &**param == name {
191                // param shadows the substitution target, no change
192                expr.clone()
193            } else if free_vars(replacement).contains(param) {
194                // Would capture; alpha-rename the param first
195                let fresh = fresh_name(param, &rename_avoid_set(body, name, replacement));
196                let renamed_body = substitute(body, param, &Expr::Var(Arc::clone(&fresh)));
197                Expr::Lam(
198                    fresh,
199                    Box::new(substitute(&renamed_body, name, replacement)),
200                )
201            } else {
202                Expr::Lam(
203                    Arc::clone(param),
204                    Box::new(substitute(body, name, replacement)),
205                )
206            }
207        }
208        Expr::App(func, arg) => Expr::App(
209            Box::new(substitute(func, name, replacement)),
210            Box::new(substitute(arg, name, replacement)),
211        ),
212        Expr::Lit(_) => expr.clone(),
213        Expr::Record(fields) => Expr::Record(
214            fields
215                .iter()
216                .map(|(k, v)| (Arc::clone(k), substitute(v, name, replacement)))
217                .collect(),
218        ),
219        Expr::List(items) => Expr::List(
220            items
221                .iter()
222                .map(|i| substitute(i, name, replacement))
223                .collect(),
224        ),
225        Expr::Field(e, f) => Expr::Field(Box::new(substitute(e, name, replacement)), Arc::clone(f)),
226        Expr::Index(e, idx) => Expr::Index(
227            Box::new(substitute(e, name, replacement)),
228            Box::new(substitute(idx, name, replacement)),
229        ),
230        Expr::Match { scrutinee, arms } => Expr::Match {
231            scrutinee: Box::new(substitute(scrutinee, name, replacement)),
232            arms: arms
233                .iter()
234                .map(|(pat, body)| substitute_match_arm(pat, body, name, replacement))
235                .collect(),
236        },
237        Expr::Let {
238            name: let_name,
239            value,
240            body,
241        } => {
242            let new_value = substitute(value, name, replacement);
243            if &**let_name == name {
244                // let shadows the substitution target
245                Expr::Let {
246                    name: Arc::clone(let_name),
247                    value: Box::new(new_value),
248                    body: body.clone(),
249                }
250            } else if free_vars(replacement).contains(let_name) {
251                // Would capture; alpha-rename the bound name first
252                let fresh = fresh_name(let_name, &rename_avoid_set(body, name, replacement));
253                let renamed_body = substitute(body, let_name, &Expr::Var(Arc::clone(&fresh)));
254                Expr::Let {
255                    name: fresh,
256                    value: Box::new(new_value),
257                    body: Box::new(substitute(&renamed_body, name, replacement)),
258                }
259            } else {
260                Expr::Let {
261                    name: Arc::clone(let_name),
262                    value: Box::new(new_value),
263                    body: Box::new(substitute(body, name, replacement)),
264                }
265            }
266        }
267        Expr::Builtin(op, args) => Expr::Builtin(
268            *op,
269            args.iter()
270                .map(|a| substitute(a, name, replacement))
271                .collect(),
272        ),
273    }
274}
275
276/// Substitute into one `Match` arm, alpha-renaming the arm's binders where
277/// they would capture a free variable of `replacement`.
278///
279/// An arm whose pattern binds `name` shadows the substitution entirely and
280/// is returned unchanged.
281fn substitute_match_arm(
282    pat: &Pattern,
283    body: &Expr,
284    name: &str,
285    replacement: &Expr,
286) -> (Pattern, Expr) {
287    let pvars = pattern_vars(pat);
288    if pvars.iter().any(|v| &**v == name) {
289        return (pat.clone(), body.clone());
290    }
291    let replacement_free = free_vars(replacement);
292    let mut avoid = rename_avoid_set(body, name, replacement);
293    avoid.extend(pvars.iter().cloned());
294    let mut new_pat = pat.clone();
295    let mut new_body = body.clone();
296    for v in &pvars {
297        if !replacement_free.contains(v) {
298            continue;
299        }
300        let fresh = fresh_name(v, &avoid);
301        avoid.insert(Arc::clone(&fresh));
302        new_pat = rename_pattern_var(&new_pat, v, &fresh);
303        new_body = substitute(&new_body, v, &Expr::Var(Arc::clone(&fresh)));
304    }
305    (new_pat, substitute(&new_body, name, replacement))
306}
307
308/// Generate a fresh variable name by appending primes until it's not in `avoid`.
309fn fresh_name(base: &str, avoid: &FxHashSet<Arc<str>>) -> Arc<str> {
310    let mut candidate = format!("{base}'");
311    while avoid.contains(candidate.as_str()) {
312        candidate.push('\'');
313    }
314    Arc::from(candidate)
315}
316
317#[cfg(test)]
318mod tests {
319    use super::*;
320    use crate::eval::EvalConfig;
321    use crate::{Env, Literal};
322
323    #[test]
324    fn free_vars_simple() {
325        // λx. add(x, y): y is free, x is bound
326        let expr = Expr::lam(
327            "x",
328            Expr::builtin(crate::BuiltinOp::Add, vec![Expr::var("x"), Expr::var("y")]),
329        );
330        let fv = free_vars(&expr);
331        assert!(fv.contains("y"));
332        assert!(!fv.contains("x"));
333    }
334
335    #[test]
336    fn substitute_simple() {
337        // add(x, 1) with x → 42 becomes add(42, 1)
338        let expr = Expr::builtin(
339            crate::BuiltinOp::Add,
340            vec![Expr::var("x"), Expr::Lit(Literal::Int(1))],
341        );
342        let result = substitute(&expr, "x", &Expr::Lit(Literal::Int(42)));
343        assert_eq!(
344            result,
345            Expr::builtin(
346                crate::BuiltinOp::Add,
347                vec![Expr::Lit(Literal::Int(42)), Expr::Lit(Literal::Int(1))],
348            )
349        );
350    }
351
352    #[test]
353    fn substitute_avoids_capture() {
354        // λy. add(x, y) with x → y should alpha-rename:
355        // λy'. add(y, y')
356        let expr = Expr::lam(
357            "y",
358            Expr::builtin(crate::BuiltinOp::Add, vec![Expr::var("x"), Expr::var("y")]),
359        );
360        let result = substitute(&expr, "x", &Expr::var("y"));
361        // The lambda param should be renamed to avoid capture
362        match &result {
363            Expr::Lam(param, _) => assert_ne!(&**param, "y"),
364            _ => panic!("expected Lam"),
365        }
366    }
367
368    #[test]
369    fn substitute_shadowed_by_let() {
370        // let x = 1 in add(x, y) with x → 99
371        // The value (1) contains no free occurrence of x, so it stays 1.
372        // The body is shadowed by the let binding, so x stays as x.
373        let expr = Expr::let_in(
374            "x",
375            Expr::Lit(Literal::Int(1)),
376            Expr::builtin(crate::BuiltinOp::Add, vec![Expr::var("x"), Expr::var("y")]),
377        );
378        let result = substitute(&expr, "x", &Expr::Lit(Literal::Int(99)));
379        match &result {
380            Expr::Let { value, body, .. } => {
381                // value is a literal 1, not a reference to x, so unchanged
382                assert_eq!(**value, Expr::Lit(Literal::Int(1)));
383                // body should still reference x (shadowed by let)
384                assert!(
385                    matches!(body.as_ref(), Expr::Builtin(_, args) if matches!(&args[0], Expr::Var(v) if &**v == "x"))
386                );
387            }
388            _ => panic!("expected Let"),
389        }
390    }
391
392    #[test]
393    fn free_vars_lambda_does_not_leak_into_siblings() {
394        // record { f = \x -> x, g = x }: the record's own `x` is free.
395        let expr = Expr::Record(vec![
396            (Arc::from("f"), Expr::lam("x", Expr::var("x"))),
397            (Arc::from("g"), Expr::var("x")),
398        ]);
399        let fv = free_vars(&expr);
400        assert!(
401            fv.contains("x"),
402            "sibling occurrence of x is free, got {fv:?}"
403        );
404    }
405
406    #[test]
407    fn free_vars_respects_shadowed_lambda_binder() {
408        // \x -> (\x -> x) x: closed, nothing free.
409        let expr = Expr::lam(
410            "x",
411            Expr::app(Expr::lam("x", Expr::var("x")), Expr::var("x")),
412        );
413        let fv = free_vars(&expr);
414        assert!(fv.is_empty(), "expression is closed, got {fv:?}");
415    }
416
417    #[test]
418    fn free_vars_let_does_not_leak_into_siblings() {
419        // record { f = let x = 1 in x, g = x }
420        let expr = Expr::Record(vec![
421            (
422                Arc::from("f"),
423                Expr::let_in("x", Expr::Lit(Literal::Int(1)), Expr::var("x")),
424            ),
425            (Arc::from("g"), Expr::var("x")),
426        ]);
427        let fv = free_vars(&expr);
428        assert!(
429            fv.contains("x"),
430            "sibling occurrence of x is free, got {fv:?}"
431        );
432    }
433
434    #[test]
435    fn free_vars_respects_shadowed_let_binder() {
436        // \x -> let x = 1 in x: closed.
437        let expr = Expr::lam(
438            "x",
439            Expr::let_in("x", Expr::Lit(Literal::Int(1)), Expr::var("x")),
440        );
441        let fv = free_vars(&expr);
442        assert!(fv.is_empty(), "expression is closed, got {fv:?}");
443    }
444
445    #[test]
446    fn substitute_avoids_capture_under_let() {
447        // (let x = 1 in z)[z := x] must not bind the incoming x to the let.
448        let expr = Expr::let_in("x", Expr::Lit(Literal::Int(1)), Expr::var("z"));
449        let result = substitute(&expr, "z", &Expr::var("x"));
450        let env = Env::new().extend(Arc::from("x"), Literal::Int(42));
451        let Ok(value) = crate::eval::eval(&result, &env, &EvalConfig::default()) else {
452            panic!("substituted expression must evaluate, got {result:?}");
453        };
454        assert_eq!(value, Literal::Int(42), "got {result:?}");
455    }
456
457    #[test]
458    fn substitute_avoids_capture_under_match_arm() {
459        // (match 1 with x -> z)[z := x] must not bind the incoming x to the arm.
460        let expr = Expr::Match {
461            scrutinee: Box::new(Expr::Lit(Literal::Int(1))),
462            arms: vec![(Pattern::Var(Arc::from("x")), Expr::var("z"))],
463        };
464        let result = substitute(&expr, "z", &Expr::var("x"));
465        let env = Env::new().extend(Arc::from("x"), Literal::Int(42));
466        let Ok(value) = crate::eval::eval(&result, &env, &EvalConfig::default()) else {
467            panic!("substituted expression must evaluate, got {result:?}");
468        };
469        assert_eq!(value, Literal::Int(42), "got {result:?}");
470    }
471
472    #[test]
473    fn substitute_fresh_name_avoids_body_free_variables() {
474        // (\y -> add(y', add(x, y)))[x := y] must not pick y' as the fresh
475        // binder, since y' already occurs free in the body.
476        let expr = Expr::lam(
477            "y",
478            Expr::builtin(
479                crate::BuiltinOp::Add,
480                vec![
481                    Expr::var("y'"),
482                    Expr::builtin(crate::BuiltinOp::Add, vec![Expr::var("x"), Expr::var("y")]),
483                ],
484            ),
485        );
486        let result = substitute(&expr, "x", &Expr::var("y"));
487        match &result {
488            Expr::Lam(param, _) => {
489                assert_ne!(&**param, "y", "binder must be renamed");
490                assert_ne!(&**param, "y'", "binder must not capture the body's y'");
491            }
492            other => panic!("expected Lam, got {other:?}"),
493        }
494    }
495}