1use std::collections::HashMap;
26
27use crate::ast::{Expr, FnBody, FnDef, Stmt, StrPart};
28
29use super::calls::{expr_to_dotted_name, is_builtin_namespace};
30
31pub trait AllocPolicy {
37 fn builtin_allocates(&self, name: &str) -> bool;
40
41 fn constructor_allocates(&self, name: &str, has_payload: bool) -> bool;
46}
47
48fn expr_allocates<P: AllocPolicy>(
52 expr: &Expr,
53 user_allocates: &HashMap<String, bool>,
54 policy: &P,
55) -> bool {
56 match expr {
57 Expr::Literal(_) | Expr::Ident(_) | Expr::Resolved { .. } => false,
58 Expr::Constructor(_, None) => false,
59
60 Expr::List(_)
63 | Expr::Tuple(_)
64 | Expr::MapLiteral(_)
65 | Expr::RecordCreate { .. }
66 | Expr::RecordUpdate { .. }
67 | Expr::IndependentProduct(_, _) => true,
68 Expr::InterpolatedStr(parts) => {
69 parts.iter().any(|p| matches!(p, StrPart::Parsed(_)))
73 || expr_children_allocate(expr, user_allocates, policy)
74 }
75 Expr::Constructor(name, Some(payload)) => {
76 policy.constructor_allocates(name, true)
77 || expr_allocates(&payload.node, user_allocates, policy)
78 }
79
80 Expr::FnCall(callee, args) => {
82 if let Some(name) = expr_to_dotted_name(&callee.node) {
83 let ns = name.split('.').next().unwrap_or("");
84 if is_builtin_namespace(ns) {
85 if policy.builtin_allocates(&name) {
86 return true;
87 }
88 } else if let Some(&true) = user_allocates.get(&name) {
89 return true;
90 }
91 }
92 args.iter()
93 .any(|a| expr_allocates(&a.node, user_allocates, policy))
94 }
95 Expr::TailCall(data) => {
96 if let Some(&true) = user_allocates.get(&data.target) {
97 return true;
98 }
99 data.args
100 .iter()
101 .any(|a| expr_allocates(&a.node, user_allocates, policy))
102 }
103
104 Expr::Attr(base, _) | Expr::ErrorProp(base) => {
106 expr_allocates(&base.node, user_allocates, policy)
107 }
108 Expr::BinOp(_, l, r) => {
109 expr_allocates(&l.node, user_allocates, policy)
110 || expr_allocates(&r.node, user_allocates, policy)
111 }
112 Expr::Neg(inner) => expr_allocates(&inner.node, user_allocates, policy),
113 Expr::Match { subject, arms } => {
114 expr_allocates(&subject.node, user_allocates, policy)
115 || arms
116 .iter()
117 .any(|a| expr_allocates(&a.body.node, user_allocates, policy))
118 }
119 }
120}
121
122fn expr_children_allocate<P: AllocPolicy>(
124 expr: &Expr,
125 user_allocates: &HashMap<String, bool>,
126 policy: &P,
127) -> bool {
128 if let Expr::InterpolatedStr(parts) = expr {
129 return parts.iter().any(|p| match p {
130 StrPart::Literal(_) => false,
131 StrPart::Parsed(e) => expr_allocates(&e.node, user_allocates, policy),
132 });
133 }
134 false
135}
136
137fn body_allocates<P: AllocPolicy>(
139 body: &FnBody,
140 user_allocates: &HashMap<String, bool>,
141 policy: &P,
142) -> bool {
143 body.stmts().iter().any(|s| match s {
144 Stmt::Binding(_, _, e) | Stmt::Expr(e) => expr_allocates(&e.node, user_allocates, policy),
145 })
146}
147
148pub fn count_alloc_sites_in_fn<P: AllocPolicy>(fd: &FnDef, policy: &P) -> usize {
159 let FnBody::Block(stmts) = fd.body.as_ref();
160 let mut acc = 0;
161 for stmt in stmts {
162 match stmt {
163 Stmt::Binding(_, _, e) | Stmt::Expr(e) => {
164 count_expr_alloc_sites(&e.node, policy, &mut acc)
165 }
166 }
167 }
168 acc
169}
170
171fn count_expr_alloc_sites<P: AllocPolicy>(expr: &Expr, policy: &P, acc: &mut usize) {
172 match expr {
173 Expr::Literal(_) | Expr::Ident(_) | Expr::Resolved { .. } => {}
174 Expr::Constructor(_, None) => {}
175
176 Expr::List(items) | Expr::Tuple(items) | Expr::IndependentProduct(items, _) => {
177 *acc += 1;
178 for item in items {
179 count_expr_alloc_sites(&item.node, policy, acc);
180 }
181 }
182 Expr::MapLiteral(entries) => {
183 *acc += 1;
184 for (k, v) in entries {
185 count_expr_alloc_sites(&k.node, policy, acc);
186 count_expr_alloc_sites(&v.node, policy, acc);
187 }
188 }
189 Expr::RecordCreate { fields, .. } => {
190 *acc += 1;
191 for (_, v) in fields {
192 count_expr_alloc_sites(&v.node, policy, acc);
193 }
194 }
195 Expr::RecordUpdate { base, updates, .. } => {
196 *acc += 1;
197 count_expr_alloc_sites(&base.node, policy, acc);
198 for (_, v) in updates {
199 count_expr_alloc_sites(&v.node, policy, acc);
200 }
201 }
202 Expr::InterpolatedStr(parts) => {
203 if parts.iter().any(|p| matches!(p, StrPart::Parsed(_))) {
207 *acc += 1;
208 }
209 for part in parts {
210 if let StrPart::Parsed(e) = part {
211 count_expr_alloc_sites(&e.node, policy, acc);
212 }
213 }
214 }
215 Expr::Constructor(name, Some(payload)) => {
216 if policy.constructor_allocates(name, true) {
217 *acc += 1;
218 }
219 count_expr_alloc_sites(&payload.node, policy, acc);
220 }
221 Expr::FnCall(callee, args) => {
222 if let Some(name) = expr_to_dotted_name(&callee.node) {
223 let ns = name.split('.').next().unwrap_or("");
224 if is_builtin_namespace(ns) && policy.builtin_allocates(&name) {
225 *acc += 1;
226 }
227 }
228 count_expr_alloc_sites(&callee.node, policy, acc);
229 for a in args {
230 count_expr_alloc_sites(&a.node, policy, acc);
231 }
232 }
233 Expr::TailCall(data) => {
234 for a in &data.args {
235 count_expr_alloc_sites(&a.node, policy, acc);
236 }
237 }
238 Expr::Attr(base, _) | Expr::ErrorProp(base) => {
239 count_expr_alloc_sites(&base.node, policy, acc);
240 }
241 Expr::BinOp(_, l, r) => {
242 count_expr_alloc_sites(&l.node, policy, acc);
243 count_expr_alloc_sites(&r.node, policy, acc);
244 }
245 Expr::Neg(inner) => count_expr_alloc_sites(&inner.node, policy, acc),
246 Expr::Match { subject, arms } => {
247 count_expr_alloc_sites(&subject.node, policy, acc);
248 for arm in arms {
249 count_expr_alloc_sites(&arm.body.node, policy, acc);
250 }
251 }
252 }
253}
254
255pub fn count_alloc_sites_in_program<P: AllocPolicy>(
258 items: &[crate::ast::TopLevel],
259 policy: &P,
260) -> usize {
261 items
262 .iter()
263 .filter_map(|it| match it {
264 crate::ast::TopLevel::FnDef(fd) => Some(count_alloc_sites_in_fn(fd, policy)),
265 _ => None,
266 })
267 .sum()
268}
269
270pub fn compute_alloc_info<P: AllocPolicy>(fns: &[&FnDef], policy: &P) -> HashMap<String, bool> {
281 let mut info: HashMap<String, bool> = fns
282 .iter()
283 .map(|fd| {
284 (fd.name.clone(), !fd.effects.is_empty())
286 })
287 .collect();
288
289 loop {
290 let mut changed = false;
291 for fd in fns {
292 if *info.get(&fd.name).unwrap_or(&false) {
293 continue;
294 }
295 if body_allocates(&fd.body, &info, policy) {
296 info.insert(fd.name.clone(), true);
297 changed = true;
298 }
299 }
300 if !changed {
301 break;
302 }
303 }
304
305 info
306}
307
308#[cfg(test)]
309mod tests {
310 use super::*;
311 use crate::ast::{BinOp, FnDef, Literal, Spanned};
312 use std::sync::Arc;
313
314 struct TestPolicy;
318
319 impl AllocPolicy for TestPolicy {
320 fn builtin_allocates(&self, name: &str) -> bool {
321 name.starts_with("Map.") || name == "String.fromInt"
322 }
323 fn constructor_allocates(&self, _name: &str, _has_payload: bool) -> bool {
324 false
325 }
326 }
327
328 fn sp<T>(value: T) -> Spanned<T> {
329 Spanned::new(value, 1)
330 }
331
332 fn lit_int(n: i64) -> Spanned<Expr> {
333 sp(Expr::Literal(Literal::Int(n)))
334 }
335
336 fn fn_def_pure(name: &str, body: Expr) -> FnDef {
337 FnDef {
338 name: name.to_string(),
339 line: 1,
340 params: vec![],
341 return_type: "Int".into(),
342 effects: vec![],
343 desc: None,
344 body: Arc::new(FnBody::from_expr(sp(body))),
345 resolution: None,
346 }
347 }
348
349 #[test]
350 fn pure_arithmetic_does_not_allocate() {
351 let fd = fn_def_pure(
352 "addOne",
353 Expr::BinOp(BinOp::Add, Box::new(lit_int(1)), Box::new(lit_int(2))),
354 );
355 let info = compute_alloc_info(&[&fd], &TestPolicy);
356 assert_eq!(info.get("addOne"), Some(&false));
357 }
358
359 #[test]
360 fn list_literal_allocates() {
361 let fd = fn_def_pure("makeList", Expr::List(vec![lit_int(1), lit_int(2)]));
362 let info = compute_alloc_info(&[&fd], &TestPolicy);
363 assert_eq!(info.get("makeList"), Some(&true));
364 }
365
366 #[test]
367 fn allocating_builtin_call_allocates() {
368 let call = Expr::FnCall(
370 Box::new(sp(Expr::Attr(
371 Box::new(sp(Expr::Ident("String".into()))),
372 "fromInt".into(),
373 ))),
374 vec![lit_int(42)],
375 );
376 let fd = fn_def_pure("stringify", call);
377 let info = compute_alloc_info(&[&fd], &TestPolicy);
378 assert_eq!(info.get("stringify"), Some(&true));
379 }
380
381 #[test]
382 fn pure_builtin_call_does_not_allocate() {
383 let call = Expr::FnCall(
385 Box::new(sp(Expr::Attr(
386 Box::new(sp(Expr::Ident("Int".into()))),
387 "abs".into(),
388 ))),
389 vec![lit_int(-5)],
390 );
391 let fd = fn_def_pure("absVal", call);
392 let info = compute_alloc_info(&[&fd], &TestPolicy);
393 assert_eq!(info.get("absVal"), Some(&false));
394 }
395
396 #[test]
397 fn effects_force_allocating() {
398 let mut fd = fn_def_pure("logIt", Expr::Literal(Literal::Int(0)));
399 fd.effects = vec![sp("Console.print".into())];
400 let info = compute_alloc_info(&[&fd], &TestPolicy);
401 assert_eq!(info.get("logIt"), Some(&true));
402 }
403
404 #[test]
405 fn transitive_user_call_propagates() {
406 let inner = fn_def_pure("makeListInner", Expr::List(vec![lit_int(1)]));
409
410 let call = Expr::FnCall(Box::new(sp(Expr::Ident("makeListInner".into()))), vec![]);
411 let wrapper = fn_def_pure("wrapperFn", call);
412
413 let info = compute_alloc_info(&[&inner, &wrapper], &TestPolicy);
414 assert_eq!(info.get("makeListInner"), Some(&true));
415 assert_eq!(info.get("wrapperFn"), Some(&true));
416 }
417
418 #[test]
419 fn mutual_recursion_pure_stays_pure() {
420 let f = fn_def_pure(
422 "f",
423 Expr::FnCall(Box::new(sp(Expr::Ident("g".into()))), vec![lit_int(1)]),
424 );
425 let g = fn_def_pure(
426 "g",
427 Expr::FnCall(Box::new(sp(Expr::Ident("f".into()))), vec![lit_int(2)]),
428 );
429 let info = compute_alloc_info(&[&f, &g], &TestPolicy);
430 assert_eq!(info.get("f"), Some(&false));
431 assert_eq!(info.get("g"), Some(&false));
432 }
433
434 #[test]
435 fn mutual_recursion_one_allocates_taints_the_group() {
436 let f = fn_def_pure(
438 "f",
439 Expr::FnCall(Box::new(sp(Expr::Ident("g".into()))), vec![lit_int(1)]),
440 );
441 let g = fn_def_pure("g", Expr::List(vec![lit_int(0)]));
442 let info = compute_alloc_info(&[&f, &g], &TestPolicy);
443 assert_eq!(info.get("f"), Some(&true));
444 assert_eq!(info.get("g"), Some(&true));
445 }
446}