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