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