1use std::collections::{HashMap, HashSet};
14
15use serde::Serialize;
16
17use crate::ast::{DecisionBlock, FnDef, TopLevel, TypeDef, VerifyBlock};
18use crate::call_graph::{
19 direct_calls, find_recursive_fns, recursive_callsite_counts, recursive_scc_ids,
20};
21use crate::checker::expr_to_str;
22use crate::tco;
23use crate::types::checker::{TypeCheckResult, run_type_check_full};
24use crate::verify_law::canonical_spec_ref;
25
26#[derive(Clone)]
29pub struct FileContext {
30 pub source_file: String,
31 pub module_name: Option<String>,
32 pub intent: Option<String>,
33 pub depends: Vec<String>,
34 pub exposes: Vec<String>,
35 pub exposes_opaque: Vec<String>,
36 pub api_effects: Vec<String>,
37 pub module_effects: Vec<String>,
38 pub main_effects: Option<Vec<String>>,
39 pub fn_defs: Vec<FnDef>,
41 pub all_fn_defs: Vec<FnDef>,
43 pub fn_auto_memo: HashSet<String>,
44 pub fn_memo_qual: HashMap<String, Vec<String>>,
45 pub fn_auto_tco: HashSet<String>,
46 pub fn_recursive_callsites: HashMap<String, usize>,
47 pub fn_recursive_scc_id: HashMap<String, usize>,
48 pub fn_specs: HashMap<String, Vec<String>>,
49 pub fn_direct_calls: HashMap<String, Vec<String>>,
50 pub type_defs: Vec<TypeDef>,
51 pub verify_blocks: Vec<VerifyBlock>,
52 pub verify_counts: HashMap<String, usize>,
53 pub verify_samples: HashMap<String, Vec<String>>,
54 pub decisions: Vec<DecisionBlock>,
55}
56
57impl FileContext {
58 fn empty(source_file: impl Into<String>) -> Self {
59 Self {
60 source_file: source_file.into(),
61 module_name: None,
62 intent: None,
63 depends: vec![],
64 exposes: vec![],
65 exposes_opaque: vec![],
66 api_effects: vec![],
67 module_effects: vec![],
68 main_effects: None,
69 fn_defs: vec![],
70 all_fn_defs: vec![],
71 fn_auto_memo: HashSet::new(),
72 fn_memo_qual: HashMap::new(),
73 fn_auto_tco: HashSet::new(),
74 fn_recursive_callsites: HashMap::new(),
75 fn_recursive_scc_id: HashMap::new(),
76 fn_specs: HashMap::new(),
77 fn_direct_calls: HashMap::new(),
78 type_defs: vec![],
79 verify_blocks: vec![],
80 verify_counts: HashMap::new(),
81 verify_samples: HashMap::new(),
82 decisions: vec![],
83 }
84 }
85}
86
87pub fn build_context_for_items(
96 items: &[TopLevel],
97 _source: &str,
98 file_label: impl Into<String>,
99 module_root: Option<&str>,
100) -> FileContext {
101 let mut ctx = FileContext::empty(file_label);
102
103 let mut declared_module_effects: Option<Vec<String>> = None;
104 for item in items {
105 match item {
106 TopLevel::Module(m) => {
107 ctx.module_name = Some(m.name.clone());
108 ctx.intent = if m.intent.is_empty() {
109 None
110 } else {
111 Some(m.intent.clone())
112 };
113 ctx.depends = m.depends.clone();
114 ctx.exposes = m.exposes.clone();
115 ctx.exposes_opaque = m.exposes_opaque.clone();
116 declared_module_effects = m.effects.clone();
117 }
118 TopLevel::FnDef(fd) => {
119 ctx.fn_defs.push(fd.clone());
120 ctx.all_fn_defs.push(fd.clone());
121 }
122 TopLevel::TypeDef(td) => ctx.type_defs.push(td.clone()),
123 TopLevel::Verify(vb) => ctx.verify_blocks.push(vb.clone()),
124 TopLevel::Decision(db) => ctx.decisions.push(db.clone()),
125 _ => {}
126 }
127 }
128
129 let flags = compute_context_fn_flags(items, module_root);
130 let ContextFnFlags {
131 auto_memo,
132 auto_tco,
133 memo_qual,
134 recursive_callsites,
135 recursive_scc_id,
136 fn_sigs,
137 } = flags;
138 ctx.fn_auto_memo = auto_memo;
139 ctx.fn_auto_tco = auto_tco;
140 ctx.fn_memo_qual = memo_qual;
141 ctx.fn_recursive_callsites = recursive_callsites;
142 ctx.fn_recursive_scc_id = recursive_scc_id;
143 ctx.fn_direct_calls = direct_calls(items);
144
145 for vb in &ctx.verify_blocks {
146 let crate::ast::VerifyKind::Law(law) = &vb.kind else {
147 continue;
148 };
149 let Some(spec_ref) = canonical_spec_ref(&vb.fn_name, law, &fn_sigs) else {
150 continue;
151 };
152 ctx.fn_specs
153 .entry(vb.fn_name.clone())
154 .or_default()
155 .push(spec_ref.spec_fn_name);
156 }
157 for specs in ctx.fn_specs.values_mut() {
158 specs.sort();
159 specs.dedup();
160 }
161
162 let (verify_counts, verify_samples) = build_verify_summaries(&ctx.verify_blocks, &fn_sigs);
163 ctx.verify_counts = verify_counts;
164 ctx.verify_samples = verify_samples;
165
166 ctx.module_effects = if let Some(declared) = declared_module_effects {
173 unique_sorted_effects(declared.iter())
174 } else {
175 unique_sorted_effects(
176 ctx.fn_defs
177 .iter()
178 .flat_map(|fd| fd.effects.iter().map(|e| &e.node)),
179 )
180 };
181 ctx.api_effects = unique_sorted_effects(
182 ctx.fn_defs
183 .iter()
184 .filter(|fd| ctx.exposes.contains(&fd.name))
185 .flat_map(|fd| fd.effects.iter().map(|e| &e.node)),
186 );
187 ctx.main_effects = ctx
188 .fn_defs
189 .iter()
190 .find(|fd| fd.name == "main")
191 .map(|fd| unique_sorted_effects(fd.effects.iter().map(|e| &e.node)));
192
193 if !ctx.exposes.is_empty() {
194 let exposes = ctx.exposes.clone();
195 ctx.fn_defs.retain(|fd| exposes.contains(&fd.name));
196 }
197
198 ctx
199}
200
201#[derive(Clone, Debug, Serialize)]
204pub struct ContextSummary {
205 pub file_label: String,
206 pub module_name: Option<String>,
207 pub intent: Option<String>,
208 pub depends: Vec<String>,
209 pub exposes: Vec<String>,
210 pub exposes_opaque: Vec<String>,
211 pub api_effects: Vec<String>,
212 pub module_effects: Vec<String>,
213 #[serde(skip_serializing_if = "Option::is_none")]
214 pub main_effects: Option<Vec<String>>,
215 pub functions: Vec<ContextFnSummary>,
216 pub types: Vec<ContextTypeSummary>,
217 pub decisions: Vec<ContextDecisionSummary>,
218}
219
220#[derive(Clone, Debug, Serialize)]
221pub struct ContextFnSummary {
222 pub name: String,
223 pub signature: String,
224 #[serde(skip_serializing_if = "Option::is_none")]
225 pub description: Option<String>,
226 pub effects: Vec<String>,
227 pub qualifiers: Vec<String>,
228 pub auto_memo: bool,
229 pub auto_tco: bool,
230 pub recursive_callsites: usize,
231 pub verify_count: usize,
232 pub verify_samples: Vec<String>,
233 pub is_exposed: bool,
234 pub specs: Vec<String>,
235 pub direct_calls: Vec<String>,
236}
237
238#[derive(Clone, Debug, Serialize)]
239pub struct ContextTypeSummary {
240 pub name: String,
241 pub kind: &'static str,
242 pub fields_or_variants: Vec<String>,
243}
244
245#[derive(Clone, Debug, Serialize)]
246pub struct ContextDecisionSummary {
247 pub name: String,
248 pub date: String,
249 pub reason_prefix: String,
250 pub impacts: Vec<String>,
251}
252
253pub fn summarize(ctx: &FileContext) -> ContextSummary {
259 let functions = ctx
260 .all_fn_defs
261 .iter()
262 .map(|fd| {
263 let effects: Vec<String> = fd.effects.iter().map(|e| e.node.clone()).collect();
264 let qualifiers = ctx.fn_memo_qual.get(&fd.name).cloned().unwrap_or_default();
265 let description = fd.desc.clone();
266 let signature = render_signature(fd);
267 let is_exposed = ctx.exposes.is_empty() || ctx.exposes.contains(&fd.name);
268 let specs = ctx.fn_specs.get(&fd.name).cloned().unwrap_or_default();
269 let direct = ctx
270 .fn_direct_calls
271 .get(&fd.name)
272 .cloned()
273 .unwrap_or_default();
274 ContextFnSummary {
275 name: fd.name.clone(),
276 signature,
277 description,
278 effects,
279 qualifiers,
280 auto_memo: ctx.fn_auto_memo.contains(&fd.name),
281 auto_tco: ctx.fn_auto_tco.contains(&fd.name),
282 recursive_callsites: ctx
283 .fn_recursive_callsites
284 .get(&fd.name)
285 .copied()
286 .unwrap_or(0),
287 verify_count: ctx.verify_counts.get(&fd.name).copied().unwrap_or(0),
288 verify_samples: ctx
289 .verify_samples
290 .get(&fd.name)
291 .cloned()
292 .unwrap_or_default(),
293 is_exposed,
294 specs,
295 direct_calls: direct,
296 }
297 })
298 .collect();
299
300 let types = ctx
301 .type_defs
302 .iter()
303 .map(|td| match td {
304 TypeDef::Sum { name, variants, .. } => ContextTypeSummary {
305 name: name.clone(),
306 kind: "sum",
307 fields_or_variants: variants.iter().map(|v| v.name.clone()).collect(),
308 },
309 TypeDef::Product { name, fields, .. } => ContextTypeSummary {
310 name: name.clone(),
311 kind: "product",
312 fields_or_variants: fields.iter().map(|(n, _)| n.clone()).collect(),
313 },
314 })
315 .collect();
316
317 let decisions = ctx
318 .decisions
319 .iter()
320 .map(|d| {
321 let reason_prefix: String = d.reason.chars().take(60).collect();
322 let reason_prefix = if d.reason.len() > 60 {
323 format!("{}...", reason_prefix.trim_end())
324 } else {
325 reason_prefix
326 };
327 ContextDecisionSummary {
328 name: d.name.clone(),
329 date: d.date.clone(),
330 reason_prefix,
331 impacts: d
332 .impacts
333 .iter()
334 .map(|i| i.node.text().to_string())
335 .collect(),
336 }
337 })
338 .collect();
339
340 ContextSummary {
341 file_label: ctx.source_file.clone(),
342 module_name: ctx.module_name.clone(),
343 intent: ctx.intent.clone(),
344 depends: ctx.depends.clone(),
345 exposes: ctx.exposes.clone(),
346 exposes_opaque: ctx.exposes_opaque.clone(),
347 api_effects: ctx.api_effects.clone(),
348 module_effects: ctx.module_effects.clone(),
349 main_effects: ctx.main_effects.clone(),
350 functions,
351 types,
352 decisions,
353 }
354}
355
356pub fn render_context_md(summary: &ContextSummary) -> String {
361 let mut out = String::new();
362 out.push_str(&format!("# Aver Context — {}\n\n", summary.file_label));
363 out.push_str("_Generated by `aver context`_\n\n");
364
365 out.push_str("---\n\n");
366 if let Some(name) = &summary.module_name {
367 out.push_str(&format!("## Module: {}\n\n", name));
368 } else {
369 out.push_str(&format!("## {}\n\n", summary.file_label));
370 }
371
372 if let Some(intent) = &summary.intent {
373 out.push_str(&format!("> {}\n\n", intent));
374 }
375
376 if !summary.depends.is_empty() {
377 out.push_str(&format!("depends: `[{}]` \n", summary.depends.join(", ")));
378 }
379 if !summary.exposes.is_empty() {
380 out.push_str(&format!("exposes: `[{}]` \n", summary.exposes.join(", ")));
381 }
382 if !summary.exposes_opaque.is_empty() {
383 out.push_str(&format!(
384 "exposes opaque: `[{}]` \n",
385 summary.exposes_opaque.join(", ")
386 ));
387 }
388
389 if effects_equal(&summary.api_effects, &summary.module_effects) {
391 if !summary.module_effects.is_empty() {
392 out.push_str(&format!(
393 "effects: `[{}]`\n",
394 summary.module_effects.join(", ")
395 ));
396 }
397 } else {
398 out.push_str(&format!(
399 "api_effects: `[{}]` \nmodule_effects: `[{}]`\n",
400 summary.api_effects.join(", "),
401 summary.module_effects.join(", ")
402 ));
403 }
404 if let Some(main_effects) = &summary.main_effects {
405 out.push_str(&format!("main_effects: `[{}]`\n", main_effects.join(", ")));
406 }
407 out.push('\n');
408
409 for ty in &summary.types {
410 let header = match ty.kind {
411 "record" => format!("### record {}\n", ty.name),
412 _ => format!("### type {}\n", ty.name),
413 };
414 out.push_str(&header);
415 if !ty.fields_or_variants.is_empty() {
416 out.push_str(&format!("`{}`\n\n", ty.fields_or_variants.join("` | `")));
417 } else {
418 out.push('\n');
419 }
420 }
421
422 for fd in &summary.functions {
423 if fd.name == "main" {
424 continue;
425 }
426 out.push_str(&format!("### `{}`\n", fd.signature));
427 if !fd.effects.is_empty() {
428 out.push_str(&format!("effects: `[{}]` \n", fd.effects.join(", ")));
429 }
430 if fd.auto_memo {
431 out.push_str("memo: `true` \n");
432 }
433 if fd.auto_tco {
434 out.push_str("tco: `true` \n");
435 }
436 if !fd.specs.is_empty() {
437 let label = if fd.specs.len() == 1 { "spec" } else { "specs" };
438 out.push_str(&format!("{}: `[{}]` \n", label, fd.specs.join(", ")));
439 }
440 if !fd.is_exposed {
441 out.push_str("visibility: `private` \n");
442 }
443 if let Some(desc) = &fd.description {
444 out.push_str(&format!("> {}\n", desc));
445 }
446 if !fd.verify_samples.is_empty() {
447 let samples: Vec<String> = fd
448 .verify_samples
449 .iter()
450 .map(|s| format!("`{}`", s))
451 .collect();
452 out.push_str(&format!("verify: {}\n", samples.join(", ")));
453 }
454 out.push('\n');
455 }
456
457 if !summary.decisions.is_empty() {
458 out.push_str("---\n\n## Decisions\n\n");
459 for dec in &summary.decisions {
460 out.push_str(&format!("### {} ({})\n", dec.name, dec.date));
461 if !dec.reason_prefix.is_empty() {
462 out.push_str(&format!("> {}\n", dec.reason_prefix));
463 }
464 if !dec.impacts.is_empty() {
465 out.push_str(&format!("impacts: `{}`\n", dec.impacts.join("`, `")));
466 }
467 out.push('\n');
468 }
469 }
470
471 out
472}
473
474fn effects_equal(a: &[String], b: &[String]) -> bool {
475 if a.len() != b.len() {
476 return false;
477 }
478 let mut a = a.to_vec();
479 let mut b = b.to_vec();
480 a.sort();
481 b.sort();
482 a == b
483}
484
485fn render_signature(fd: &FnDef) -> String {
486 let params = fd
490 .params
491 .iter()
492 .map(|(name, type_str)| format!("{}: {}", name, type_str))
493 .collect::<Vec<_>>()
494 .join(", ");
495 format!("fn {}({}) -> {}", fd.name, params, fd.return_type)
496}
497
498pub fn compute_memo_fns(items: &[TopLevel], tc_result: &TypeCheckResult) -> HashSet<String> {
503 let recursive = find_recursive_fns(items);
504 let recursive_calls = recursive_callsite_counts(items);
505 let mut memo = HashSet::new();
506 for fn_name in &recursive {
507 if let Some((params, _ret, effects)) = tc_result.fn_sigs.get(fn_name) {
508 if !effects.is_empty() {
509 continue;
510 }
511 if recursive_calls.get(fn_name).copied().unwrap_or(0) < 2 {
512 continue;
513 }
514 let all_safe = params
515 .iter()
516 .all(|ty| is_memo_safe_type(ty, &tc_result.memo_safe_types));
517 if all_safe {
518 memo.insert(fn_name.clone());
519 }
520 }
521 }
522 memo
523}
524
525pub fn is_memo_safe_type(ty: &crate::types::Type, safe_named: &HashSet<String>) -> bool {
526 use crate::types::Type;
527 match ty {
528 Type::Int | Type::Float | Type::Bool | Type::Unit => true,
529 Type::Str => false,
530 Type::Tuple(items) => items.iter().all(|item| is_memo_safe_type(item, safe_named)),
531 Type::List(_) | Type::Vector(_) | Type::Map(_, _) | Type::Fn(_, _, _) | Type::Unknown => {
532 false
533 }
534 Type::Result(_, _) | Type::Option(_) => false,
535 Type::Named(name) => safe_named.contains(name),
536 }
537}
538
539const VERIFY_SAMPLE_LIMIT: usize = 3;
542const VERIFY_CASE_MAX_LEN: usize = 150;
543
544fn unique_sorted_effects<'a, I>(effects: I) -> Vec<String>
545where
546 I: Iterator<Item = &'a String>,
547{
548 let mut uniq = effects
549 .cloned()
550 .collect::<HashSet<_>>()
551 .into_iter()
552 .collect::<Vec<_>>();
553 uniq.sort();
554 uniq
555}
556
557fn classify_verify_case(lhs: &str, rhs: &str, ret_category: Option<&str>) -> Vec<String> {
558 let combined = format!("{lhs} -> {rhs}");
559 let mut categories = Vec::new();
560
561 match ret_category {
562 Some("result") => {
563 if rhs.contains("Result.Ok(") || rhs.contains("Ok(") {
564 categories.push("ok".to_string());
565 }
566 if rhs.contains("Result.Err(") || rhs.contains("Err(") {
567 categories.push("err".to_string());
568 }
569 }
570 Some("option") => {
571 if rhs.contains("Option.Some(") || rhs.contains("Some(") {
572 categories.push("some".to_string());
573 }
574 if rhs.contains("Option.None") || rhs == "None" {
575 categories.push("none".to_string());
576 }
577 }
578 Some("bool") => {
579 if rhs == "true" {
580 categories.push("true".to_string());
581 }
582 if rhs == "false" {
583 categories.push("false".to_string());
584 }
585 }
586 _ => {}
587 }
588
589 if combined.contains("[]") || combined.contains("{}") {
590 categories.push("empty".to_string());
591 }
592 if combined.contains("-1") || combined.contains("(0 - ") {
593 categories.push("negative".to_string());
594 }
595 if combined.contains("(0)") || rhs == "0" {
596 categories.push("zero".to_string());
597 }
598 if combined.contains("\"\"") {
599 categories.push("empty-string".to_string());
600 }
601
602 if ret_category == Some("named")
603 && let Some(dot_pos) = rhs.find('.')
604 {
605 let after_dot = &rhs[dot_pos + 1..];
606 let ctor = after_dot.split('(').next().unwrap_or(after_dot);
607 categories.push(format!("ctor:{ctor}"));
608 }
609
610 categories.sort();
611 categories.dedup();
612 categories
613}
614
615fn base_verify_case_score(lhs: &str, rhs: &str) -> i32 {
616 let combined_len = lhs.len() + rhs.len();
617 let mut score = 400 - combined_len as i32;
618 let combined = format!("{lhs} -> {rhs}");
619 if rhs.contains("Result.Err(")
620 || rhs.contains("ParseResult.Err(")
621 || rhs.contains("Option.None")
622 {
623 score += 120;
624 }
625 if combined.contains("[]") || combined.contains("{}") {
626 score += 60;
627 }
628 if combined.contains("\"\"") {
629 score += 45;
630 }
631 if combined.contains("-1") || combined.contains("(0 - ") {
632 score += 45;
633 }
634 if combined.contains(", 0") || combined.contains("(0)") || rhs == "0" {
635 score += 30;
636 }
637 if rhs == "true" || rhs == "false" {
638 score += 20;
639 }
640 score
641}
642
643fn scored_verify_samples(cases: &[(String, String)], ret_category: Option<&str>) -> Vec<String> {
644 #[derive(Clone)]
645 struct ScoredVerifyCase {
646 rendered: String,
647 base_score: i32,
648 categories: Vec<String>,
649 original_index: usize,
650 }
651
652 let mut scored = cases
653 .iter()
654 .enumerate()
655 .filter_map(|(original_index, (lhs_text, rhs_text))| {
656 if lhs_text.len() + rhs_text.len() > VERIFY_CASE_MAX_LEN {
657 return None;
658 }
659 Some(ScoredVerifyCase {
660 rendered: format!("{lhs_text} => {rhs_text}"),
661 base_score: base_verify_case_score(lhs_text, rhs_text),
662 categories: classify_verify_case(lhs_text, rhs_text, ret_category),
663 original_index,
664 })
665 })
666 .collect::<Vec<_>>();
667
668 let mut selected = Vec::new();
669 let mut seen_categories: HashSet<String> = HashSet::new();
670 while selected.len() < VERIFY_SAMPLE_LIMIT && !scored.is_empty() {
671 let best_idx = scored
672 .iter()
673 .enumerate()
674 .max_by_key(|(_, case)| {
675 let novelty = case
676 .categories
677 .iter()
678 .filter(|cat| !seen_categories.contains(cat.as_str()))
679 .count() as i32;
680 (
681 case.base_score + novelty * 35,
682 case.base_score,
683 -(case.original_index as i32),
684 )
685 })
686 .map(|(idx, _)| idx)
687 .expect("verify samples should be non-empty");
688 let chosen = scored.swap_remove(best_idx);
689 for category in &chosen.categories {
690 seen_categories.insert(category.clone());
691 }
692 selected.push(chosen.rendered);
693 }
694 selected
695}
696
697fn return_type_category(
698 fn_name: &str,
699 fn_sigs: &HashMap<String, (Vec<crate::types::Type>, crate::types::Type, Vec<String>)>,
700) -> Option<&'static str> {
701 let (_, ret, _) = fn_sigs.get(fn_name)?;
702 match ret {
703 crate::types::Type::Result(_, _) => Some("result"),
704 crate::types::Type::Option(_) => Some("option"),
705 crate::types::Type::Bool => Some("bool"),
706 crate::types::Type::List(_) => Some("list"),
707 crate::types::Type::Named(_) => Some("named"),
708 _ => None,
709 }
710}
711
712fn build_verify_summaries(
713 verify_blocks: &[VerifyBlock],
714 fn_sigs: &HashMap<String, (Vec<crate::types::Type>, crate::types::Type, Vec<String>)>,
715) -> (HashMap<String, usize>, HashMap<String, Vec<String>>) {
716 let mut cases_by_fn: HashMap<String, Vec<(String, String)>> = HashMap::new();
717 for block in verify_blocks {
718 let entry = cases_by_fn.entry(block.fn_name.clone()).or_default();
719 for (lhs, rhs) in &block.cases {
720 entry.push((expr_to_str(lhs), expr_to_str(rhs)));
721 }
722 }
723
724 let verify_counts = cases_by_fn
725 .iter()
726 .map(|(fn_name, cases)| (fn_name.clone(), cases.len()))
727 .collect::<HashMap<_, _>>();
728 let verify_samples = cases_by_fn
729 .into_iter()
730 .map(|(fn_name, cases)| {
731 let ret_cat = return_type_category(&fn_name, fn_sigs);
732 (fn_name, scored_verify_samples(&cases, ret_cat))
733 })
734 .collect::<HashMap<_, _>>();
735
736 (verify_counts, verify_samples)
737}
738
739struct ContextFnFlags {
740 auto_memo: HashSet<String>,
741 auto_tco: HashSet<String>,
742 memo_qual: HashMap<String, Vec<String>>,
743 recursive_callsites: HashMap<String, usize>,
744 recursive_scc_id: HashMap<String, usize>,
745 fn_sigs: HashMap<String, (Vec<crate::types::Type>, crate::types::Type, Vec<String>)>,
746}
747
748fn expr_has_tail_call(expr: &crate::ast::Spanned<crate::ast::Expr>) -> bool {
749 use crate::ast::Expr;
750 match &expr.node {
751 Expr::TailCall(_) => true,
752 Expr::Literal(_) | Expr::Ident(_) | Expr::Resolved { .. } => false,
753 Expr::Attr(obj, _) => expr_has_tail_call(obj),
754 Expr::FnCall(f, args) => expr_has_tail_call(f) || args.iter().any(expr_has_tail_call),
755 Expr::BinOp(_, l, r) => expr_has_tail_call(l) || expr_has_tail_call(r),
756 Expr::Match { subject, arms, .. } => {
757 expr_has_tail_call(subject) || arms.iter().any(|arm| expr_has_tail_call(&arm.body))
758 }
759 Expr::Constructor(_, arg) => arg.as_ref().is_some_and(|a| expr_has_tail_call(a)),
760 Expr::ErrorProp(inner) => expr_has_tail_call(inner),
761 Expr::InterpolatedStr(parts) => parts.iter().any(|part| match part {
762 crate::ast::StrPart::Literal(_) => false,
763 crate::ast::StrPart::Parsed(e) => expr_has_tail_call(e),
764 }),
765 Expr::List(items) | Expr::Tuple(items) | Expr::IndependentProduct(items, _) => {
766 items.iter().any(expr_has_tail_call)
767 }
768 Expr::MapLiteral(entries) => entries
769 .iter()
770 .any(|(k, v)| expr_has_tail_call(k) || expr_has_tail_call(v)),
771 Expr::RecordCreate { fields, .. } => fields.iter().any(|(_, e)| expr_has_tail_call(e)),
772 Expr::RecordUpdate { base, updates, .. } => {
773 expr_has_tail_call(base) || updates.iter().any(|(_, e)| expr_has_tail_call(e))
774 }
775 }
776}
777
778fn fn_has_tail_call(fd: &FnDef) -> bool {
779 fd.body.stmts().iter().any(|stmt| match stmt {
780 crate::ast::Stmt::Binding(_, _, expr) | crate::ast::Stmt::Expr(expr) => {
781 expr_has_tail_call(expr)
782 }
783 })
784}
785
786fn compute_context_fn_flags(items: &[TopLevel], module_root: Option<&str>) -> ContextFnFlags {
787 let mut transformed = items.to_vec();
788 tco::transform_program(&mut transformed);
789 let tco_fns = transformed
790 .iter()
791 .filter_map(|item| match item {
792 TopLevel::FnDef(fd) if fn_has_tail_call(fd) => Some(fd.name.clone()),
793 _ => None,
794 })
795 .collect::<HashSet<_>>();
796 let recursive = find_recursive_fns(&transformed);
797 let recursive_callsites = recursive_callsite_counts(&transformed);
798 let recursive_scc_id = recursive_scc_ids(&transformed);
799 let mut memo_qual = HashMap::new();
800
801 let tc_result = run_type_check_full(&transformed, module_root);
802 if !tc_result.errors.is_empty() {
803 for item in &transformed {
804 if let TopLevel::FnDef(fd) = item {
805 let mut qual = Vec::new();
806 if fd.effects.is_empty() {
807 qual.push("PURE".to_string());
808 }
809 if recursive.contains(&fd.name) {
810 qual.push("RECURSIVE".to_string());
811 }
812 memo_qual.insert(fd.name.clone(), qual);
813 }
814 }
815 return ContextFnFlags {
816 auto_memo: HashSet::new(),
817 auto_tco: tco_fns,
818 memo_qual,
819 recursive_callsites,
820 recursive_scc_id,
821 fn_sigs: tc_result.fn_sigs,
822 };
823 }
824
825 for item in &transformed {
826 if let TopLevel::FnDef(fd) = item {
827 let mut qual = Vec::new();
828 if let Some((params, _ret, effects)) = tc_result.fn_sigs.get(&fd.name) {
829 if effects.is_empty() {
830 qual.push("PURE".to_string());
831 }
832 if recursive.contains(&fd.name) {
833 qual.push("RECURSIVE".to_string());
834 }
835 let safe_args = params
836 .iter()
837 .all(|ty| is_memo_safe_type(ty, &tc_result.memo_safe_types));
838 if safe_args {
839 qual.push("SAFE_ARGS".to_string());
840 }
841 }
842 memo_qual.insert(fd.name.clone(), qual);
843 }
844 }
845
846 ContextFnFlags {
847 auto_memo: compute_memo_fns(&transformed, &tc_result),
848 auto_tco: tco_fns,
849 memo_qual,
850 recursive_callsites,
851 recursive_scc_id,
852 fn_sigs: tc_result.fn_sigs,
853 }
854}