beck_core/
frames.rs

1//! How many bindings a function body makes, so that a call can reserve room for them.
2//!
3//! A `let` used to allocate a scope of its own — a vector for the one binding, an `Arc` around it
4//! and an `Arc` around a clone of the enclosing environment, three allocations for one name. At
5//! about 126 ns each that is most of what a function call costs, paid again per binding, and a
6//! body of two dozen `let`s spent more on scopes than on the work
7//! ([`76`](../../../../../docs/76-the-record-and-the-read-report.md) is where the number came
8//! from).
9//!
10//! The count here is what removes them. A lambda's `Core::locals` is the number of bindings its
11//! body can make, so [`crate::core::Env::call_frame`] sizes one frame for the parameters and all
12//! of them together and a `let` writes into a slot that already exists.
13//!
14//! Two properties make that sound, and both are why this counts the way it does:
15//!
16//! * **Every binding that can be live at once gets its own slot.** The count sums what runs in
17//!   sequence and takes the *maximum* over what cannot: two arms of a `match` are exclusive, so
18//!   they share a reservation, and a slot is still never written twice within one call. That is
19//!   what makes it safe for a closure to hold the frame — nothing it captured can change
20//!   underneath it.
21//! * **A nested lambda contributes nothing.** Its body runs in a frame of its own, made when it is
22//!   called, so counting it here would reserve slots nothing writes.
23//!
24//! Miscounting is safe in one direction and merely slow in the other: too few slots and the
25//! evaluator falls back to chaining a scope, exactly as before this existed. That is also what
26//! happens to any program built by something that never runs this pass — a synthesised test body,
27//! a splitter's generated module — which is why the fallback is kept rather than asserted away.
28
29use crate::core::{children_mut, Arm, Core, CoreKind};
30
31/// Count and record every lambda's local bindings, across a whole program.
32pub fn reserve_program(program: &mut crate::check::Program) {
33    for def in program.defs.values_mut() {
34        reserve(&mut def.body);
35    }
36    for test in program.tests.iter_mut() {
37        for c in test.cores_mut() {
38            reserve(c);
39        }
40    }
41}
42
43/// How many bindings this expression makes before control leaves the frame it runs in.
44///
45/// The count [`reserve`] writes onto a lambda, for a caller holding an expression that is not one
46/// yet: the test runner wraps a clause in a lambda of its own at the moment it evaluates it, and
47/// this is how that lambda gets sized. `docs/79`.
48pub fn locals_of(c: &Core) -> u32 {
49    locals(c)
50}
51
52/// The same for one expression, whether or not it is a lambda.
53pub fn reserve(c: &mut Core) {
54    if let CoreKind::Lam { body, .. } = &mut c.kind {
55        let body = std::sync::Arc::make_mut(body);
56        c.locals = locals(body);
57        reserve(body);
58        return;
59    }
60    for child in children_mut(c) {
61        reserve(child);
62    }
63}
64
65/// How many bindings this expression makes before control leaves the frame it is running in.
66fn locals(c: &Core) -> u32 {
67    match &c.kind {
68        // A lambda's bindings belong to the frame its own call makes.
69        CoreKind::Lam { .. } => 0,
70        CoreKind::Let { value, body, .. } => 1 + locals(value) + locals(body),
71        // Exclusive: whichever branch runs, the other's slots are never written.
72        CoreKind::If { cond, then, alt } => locals(cond) + locals(then).max(locals(alt)),
73        CoreKind::Match { scrutinee, arms } => {
74            locals(scrutinee)
75                + arms
76                    .iter()
77                    // A guard runs before the body and both are inside this arm, so their slots
78                    // are summed rather than maximised — two things in sequence, not two branches.
79                    .map(|a| a.pattern.binders().len() as u32 + a.exprs().map(locals).sum::<u32>())
80                    .max()
81                    .unwrap_or(0)
82        }
83        _ => children(c).into_iter().map(locals).sum(),
84    }
85}
86
87fn children(c: &Core) -> Vec<&Core> {
88    match &c.kind {
89        CoreKind::Const(_) | CoreKind::Var(_) | CoreKind::Global(_) => Vec::new(),
90        CoreKind::Lam { body, .. } => vec![body],
91        CoreKind::App { func, args } => std::iter::once(&**func).chain(args).collect(),
92        CoreKind::Let { value, body, .. } => vec![value, body],
93        CoreKind::If { cond, then, alt } => vec![&**cond, &**then, &**alt],
94        CoreKind::Match { scrutinee, arms } => std::iter::once(&**scrutinee)
95            .chain(arms.iter().flat_map(|a: &Arm| a.exprs()))
96            .collect(),
97        CoreKind::Prim { args, .. } => args.iter().collect(),
98        CoreKind::Make { fields, .. } => fields.iter().map(|(_, f)| f).collect(),
99        CoreKind::Field { base, .. } => vec![base],
100        CoreKind::With { base, fields } => std::iter::once(&**base)
101            .chain(fields.iter().map(|(_, f)| f))
102            .collect(),
103        CoreKind::ListLit(items) => items.iter().collect(),
104        CoreKind::MapLit(kvs) => kvs.iter().flat_map(|(k, v)| [k, v]).collect(),
105    }
106}
107
108#[cfg(test)]
109mod tests {
110    use super::*;
111    use crate::core::{Const, Pattern, VarId};
112    use crate::ty::Ty;
113    use beck_diag::Span;
114
115    fn int(n: i64) -> Core {
116        Core::new(CoreKind::Const(Const::Int(n)), Ty::int(), Span::NONE)
117    }
118
119    fn var(v: VarId) -> Core {
120        Core::new(CoreKind::Var(v), Ty::int(), Span::NONE)
121    }
122
123    fn lam(params: Vec<VarId>, body: Core) -> Core {
124        Core::new(
125            CoreKind::Lam {
126                params: params.into(),
127                body: std::sync::Arc::new(body),
128            },
129            Ty::int(),
130            Span::NONE,
131        )
132    }
133
134    fn let_(v: VarId, body: Core) -> Core {
135        Core::new(
136            CoreKind::Let {
137                var: v,
138                value: Box::new(int(1)),
139                body: Box::new(body),
140            },
141            Ty::int(),
142            Span::NONE,
143        )
144    }
145
146    #[test]
147    fn a_chain_of_lets_reserves_one_slot_each() {
148        let mut f = lam(vec![0], let_(1, let_(2, let_(3, var(3)))));
149        reserve(&mut f);
150        assert_eq!(f.locals, 3);
151    }
152
153    /// The arms share a reservation, because only one of them can run — but the widest arm's
154    /// bindings all fit, which is what keeps a slot from being written twice in one call.
155    #[test]
156    fn the_arms_of_a_match_share_the_widest_reservation() {
157        let arms = vec![
158            Arm {
159                guard: None,
160                pattern: Pattern::Bind(1),
161                body: let_(2, var(2)),
162                span: Span::NONE,
163            },
164            Arm {
165                guard: None,
166                pattern: Pattern::Ctor {
167                    variant: "Node".into(),
168                    binds: vec![
169                        ("l".into(), Pattern::Bind(3)),
170                        ("r".into(), Pattern::Bind(4)),
171                    ],
172                },
173                body: var(3),
174                span: Span::NONE,
175            },
176        ];
177        let mut f = lam(
178            vec![0],
179            Core::new(
180                CoreKind::Match {
181                    scrutinee: Box::new(var(0)),
182                    arms,
183                },
184                Ty::int(),
185                Span::NONE,
186            ),
187        );
188        reserve(&mut f);
189        assert_eq!(f.locals, 2);
190    }
191
192    /// The inner lambda's `let` belongs to the inner lambda's own frame, and to no other.
193    #[test]
194    fn a_nested_lambda_counts_for_itself_and_not_for_its_parent() {
195        let inner = lam(vec![9], let_(8, var(8)));
196        let mut outer = lam(vec![0], let_(1, inner));
197        reserve(&mut outer);
198        assert_eq!(outer.locals, 1);
199        let CoreKind::Lam { body, .. } = &outer.kind else {
200            unreachable!()
201        };
202        let CoreKind::Let { body: inner, .. } = &body.kind else {
203            unreachable!()
204        };
205        assert_eq!(inner.locals, 1);
206    }
207}