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}