Skip to main content

aver/ir/
analyze.rs

1//! IR-level analysis pass — derives policy-independent (body classification,
2//! locals count) and policy-parametrized (alloc info) facts about each
3//! `FnDef`. Runs as the last stage of the canonical pipeline so consumers
4//! (codegen, dump, future inliner) can read derived metadata from one
5//! place instead of recomputing it.
6//!
7//! Why a unified analysis stage instead of ad-hoc calls scattered through
8//! VM compilation and WASM emission: every backend that wanted "is this
9//! fn no-alloc" or "what's its body shape" had its own call into
10//! `compute_alloc_info` / `classify_thin_fn_def`. Same input, same
11//! computation, repeated. Centralising means: one walk per program, one
12//! place to extend with new analyses, and `aver compile --emit-ir` /
13//! `--explain-passes` see the same numbers the codegen does.
14//!
15//! Backend-specific bits stay backend-specific: `AllocPolicy` is provided
16//! by the caller (`VmAllocPolicy`, `WasmAllocPolicy`, or the dump's
17//! conservative `DumpAllocPolicy`). Policy-independent facts (`body_shape`,
18//! `thin_kind`, `local_count`) are computed unconditionally.
19//!
20//! ## Scope: per-module by design (Aver module DAG invariant)
21//!
22//! `analyze` runs on one module at a time — the entry items, or a single
23//! dep module loaded from disk. It does NOT cross module boundaries to
24//! discover, say, a mutual-TCO SCC that spans two modules. This is correct
25//! by Aver's module-graph invariant, not a limitation:
26//!
27//! - `module M depends [A, B, ...]` — depends is a static list of names,
28//!   no dynamic resolution.
29//! - Module loading (`load_module_recursive`) enforces a DAG via the
30//!   `loaded: HashSet` cycle guard — a re-entered module name returns
31//!   early, so the dep graph is acyclic by construction.
32//! - In a DAG, all calls flow from dependents to dependencies: module A
33//!   calls into module B if A depends on B (transitively); B never calls
34//!   back into A. Mutually-recursive call chains require a cycle in the
35//!   call graph; a cycle requires either (i) a self-call within one
36//!   module, or (ii) a cycle in the module DAG — and the latter is
37//!   forbidden.
38//!
39//! Consequence: every SCC of mutually-recursive functions lives entirely
40//! within a single module. Per-module analysis covers every real case;
41//! "cross-module mutual TCO" is mathematically impossible in Aver.
42//! Backends that want a global `mutual_tco_members` set just union the
43//! per-module sets they get from each module's `AnalysisResult`.
44//!
45//! Same logic for `recursive_fns`, `recursive_call_count`, and the
46//! tail-call SCC computation that drives trampoline emission. None of
47//! these need to see cross-module data.
48
49use std::collections::{HashMap, HashSet};
50
51use crate::ast::{FnBody, FnDef, TopLevel};
52use crate::call_graph::{find_recursive_fns, tailcall_scc_components};
53use crate::ir::{
54    AllocPolicy, BodyExprPlan, BodyPlan, CallLowerCtx, ThinKind, classify_thin_fn_def,
55};
56
57/// Backend-neutral allocation policy useful for diagnostic dumps. Mirrors
58/// the VM/WASM "pure non-alloc builtins" whitelist exactly so the
59/// `[no_alloc]` annotation reads the same on both runtime backends.
60/// Constructors with payloads are treated as allocating; nullary
61/// constructors (`Option.None`, `Result.Ok` with primitive seed) are not.
62///
63/// Codegen pipelines should use the backend-specific policy
64/// (`VmAllocPolicy`, `WasmAllocPolicy`); this type is for `--emit-ir`,
65/// `aver bench`, and other tools that want a sensible answer without
66/// committing to a target.
67pub struct NeutralAllocPolicy;
68
69impl AllocPolicy for NeutralAllocPolicy {
70    fn builtin_allocates(&self, name: &str) -> bool {
71        !matches!(
72            name,
73            "Int.abs"
74                | "Int.min"
75                | "Int.max"
76                | "Float.fromInt"
77                | "Float.abs"
78                | "Float.floor"
79                | "Float.ceil"
80                | "Float.round"
81                | "Float.min"
82                | "Float.max"
83                | "Float.sin"
84                | "Float.cos"
85                | "Float.sqrt"
86                | "Float.pow"
87                | "Float.atan2"
88                | "Float.pi"
89                | "Char.toCode"
90                | "String.len"
91                | "String.byteLength"
92                | "String.startsWith"
93                | "String.endsWith"
94                | "String.contains"
95                | "List.len"
96                | "List.contains"
97                | "Vector.len"
98                | "Map.size"
99                | "Map.contains"
100                | "Set.size"
101                | "Set.contains"
102                | "Bool.and"
103                | "Bool.or"
104                | "Bool.not"
105        )
106    }
107    fn constructor_allocates(&self, _name: &str, has_payload: bool) -> bool {
108        has_payload
109    }
110}
111
112#[derive(Debug, Clone, Default)]
113pub struct AnalysisResult {
114    pub fn_analyses: HashMap<String, FnAnalysis>,
115    /// Names of fns that participate in a multi-member tail-call SCC
116    /// (mutual recursion). Backends emit a trampoline + thin wrappers
117    /// for each member; singleton self-recursive fns go through plain
118    /// TCO and aren't in this set.
119    pub mutual_tco_members: HashSet<String>,
120    /// Names of fns that call themselves directly or transitively.
121    /// Used by the type checker's flow analysis and the proof
122    /// exporters.
123    pub recursive_fns: HashSet<String>,
124}
125
126#[derive(Debug, Clone)]
127pub struct FnAnalysis {
128    /// `Some(true)` = proven to allocate under the supplied `AllocPolicy`;
129    /// `Some(false)` = proven not to; `None` = no policy was configured.
130    pub allocates: Option<bool>,
131    pub thin_kind: Option<ThinKind>,
132    pub body_shape: BodyShape,
133    pub local_count: Option<u16>,
134    /// `true` if this fn participates in a mutual-TCO SCC (not a singleton
135    /// self-recursive fn — those are plain TCO and don't need trampoline
136    /// codegen). Backends use this to gate trampoline emission.
137    pub mutual_tco_member: bool,
138    /// `true` if the fn calls itself directly or transitively. Used by
139    /// the type checker's flow analysis and the proof exporters.
140    pub recursive: bool,
141    /// Number of recursive call sites in the fn body. `0` for non-recursive
142    /// fns. Surfaced in the IR dump's `recursive×N` annotation.
143    pub recursive_call_count: usize,
144}
145
146#[derive(Debug, Clone, Copy, PartialEq, Eq)]
147pub enum BodyShape {
148    /// `BodyPlan::SingleExpr` whose head is a leaf op (literal, ident,
149    /// resolved local, etc.) — the fastest possible body shape.
150    LeafExpr,
151    /// `BodyPlan::SingleExpr` of any other kind (call, match, ctor, …).
152    SingleExpr,
153    /// `BodyPlan::Block { stmts: N, .. }` — a multi-stmt block (let
154    /// bindings + tail expr).
155    Block(usize),
156    /// Body didn't match any of the classifier's recognised shapes;
157    /// usually means the body is a `match` or some other top-level
158    /// expression the thin-body classifier doesn't model.
159    Unclassified(usize),
160}
161
162/// Run the analysis on all `TopLevel::FnDef` items in `items`. The
163/// `alloc_policy` is optional — when `None`, `FnAnalysis::allocates`
164/// stays `None` for every fn (every other field is still computed).
165pub fn analyze(
166    items: &[TopLevel],
167    alloc_policy: Option<&dyn AllocPolicy>,
168    ctx: &impl CallLowerCtx,
169) -> AnalysisResult {
170    let fn_defs: Vec<&FnDef> = items
171        .iter()
172        .filter_map(|i| match i {
173            TopLevel::FnDef(fd) => Some(fd),
174            _ => None,
175        })
176        .collect();
177
178    let alloc_info = alloc_policy.map(|policy| {
179        // The trait object can't be passed straight to the generic
180        // `compute_alloc_info<P: AllocPolicy>`, so wrap it in a
181        // monomorphic adapter that re-dispatches through `dyn`.
182        struct PolicyRef<'a>(&'a dyn AllocPolicy);
183        impl AllocPolicy for PolicyRef<'_> {
184            fn builtin_allocates(&self, name: &str) -> bool {
185                self.0.builtin_allocates(name)
186            }
187            fn constructor_allocates(&self, name: &str, has_payload: bool) -> bool {
188                self.0.constructor_allocates(name, has_payload)
189            }
190        }
191        crate::ir::compute_alloc_info(&fn_defs, &PolicyRef(policy))
192    });
193
194    // Mutual-TCO membership — split out from `find_tco_groups` because
195    // backends want to know "is this fn in a multi-member SCC" specifically
196    // (singletons go through plain TCO and don't need trampoline codegen).
197    // Identical computation to what every codegen backend (`codegen::mod`,
198    // `codegen::rust::mod`) was running on its own; centralising means
199    // they read from `FnAnalysis` instead.
200    let entry_fns_for_scc: Vec<&FnDef> = fn_defs
201        .iter()
202        .filter(|fd| fd.name != "main")
203        .copied()
204        .collect();
205    let mut mutual_tco_set: HashSet<String> = HashSet::new();
206    for group in tailcall_scc_components(&entry_fns_for_scc) {
207        if group.len() < 2 {
208            continue; // singleton self-recursive fn — handled by plain TCO
209        }
210        for fd in group {
211            mutual_tco_set.insert(fd.name.clone());
212        }
213    }
214
215    let recursive_set = find_recursive_fns(items);
216    let recursive_call_counts = crate::call_graph::recursive_callsite_counts(items);
217
218    let mut fn_analyses: HashMap<String, FnAnalysis> = HashMap::with_capacity(fn_defs.len());
219    for fd in &fn_defs {
220        let plan = classify_thin_fn_def(fd, ctx);
221        let body_shape = match &plan {
222            Some(p) => match &p.body {
223                BodyPlan::SingleExpr(BodyExprPlan::Leaf(_)) => BodyShape::LeafExpr,
224                BodyPlan::SingleExpr(_) => BodyShape::SingleExpr,
225                BodyPlan::Block { stmts, .. } => BodyShape::Block(stmts.len()),
226            },
227            None => {
228                let FnBody::Block(stmts) = fd.body.as_ref();
229                BodyShape::Unclassified(stmts.len())
230            }
231        };
232        let analysis = FnAnalysis {
233            allocates: alloc_info.as_ref().and_then(|m| m.get(&fd.name).copied()),
234            thin_kind: plan.as_ref().map(|p| p.kind),
235            body_shape,
236            local_count: fd.resolution.as_ref().map(|r| r.local_count),
237            mutual_tco_member: mutual_tco_set.contains(&fd.name),
238            recursive: recursive_set.contains(&fd.name),
239            recursive_call_count: recursive_call_counts.get(&fd.name).copied().unwrap_or(0),
240        };
241        fn_analyses.insert(fd.name.clone(), analysis);
242    }
243
244    AnalysisResult {
245        fn_analyses,
246        mutual_tco_members: mutual_tco_set,
247        recursive_fns: recursive_set,
248    }
249}