Skip to main content

aver/ir/mir/
program.rs

1//! `MirProgram` + `MirFn` — the top-level shape of Core MIR.
2//!
3//! Phase 2a of #252. The data model is structured (not flat /
4//! basic-block), expression-based (every `MirExpr` is a value), and
5//! identity-typed (declaration refs go through `FnId` / `TypeId` /
6//! `CtorId`). See [`super::RFC.md`] for the full rationale.
7
8use std::collections::HashMap;
9
10use crate::ir::{FnId, ModuleId};
11
12use crate::ast::Spanned;
13
14use super::expr::{MirEffectAnnotation, MirExpr};
15use super::stats::LowerStats;
16
17/// A whole compiled program in Core MIR. Keyed by `FnId` (stable
18/// across compilation units; same identity layer the rest of the
19/// pipeline consumes). Phase 2a builds an empty `MirProgram`
20/// directly; Phase 3 grows the lowering that populates it from
21/// `ResolvedProgramView`.
22#[derive(Debug, Clone, Default)]
23pub struct MirProgram {
24    /// All compiled functions, keyed by their `FnId`.
25    pub fns: HashMap<FnId, MirFn>,
26    /// Optional module identity for tooling that wants to walk
27    /// `MirProgram` per source module (LSP outline, future
28    /// per-module IR dump). Empty when the program was assembled
29    /// from a single entry file with no `depends [...]`.
30    pub modules: Vec<ModuleId>,
31    /// Coverage telemetry for the lowering that produced this
32    /// program. `lowered + skipped.values().sum()` equals the
33    /// number of `ResolvedFnDef` items the lowerer saw — see
34    /// [`super::stats::LowerStats`] for the coverage gate that
35    /// Phase 4 (VM slice) consumes.
36    pub stats: LowerStats,
37    /// Phase 6 wave 11 — interned built-in fn names. Indexed by
38    /// `BuiltinId`. Lowering grows this lazily as it encounters
39    /// each unique `ResolvedCallee::Builtin(name)` shape; MIR
40    /// consumers (VM walker, Rust walker, optimize passes) look
41    /// up the canonical string via `program.builtin_name(id)`
42    /// instead of carrying the string inline on every
43    /// `MirCallee::Builtin`.
44    pub builtins: Vec<String>,
45    /// `true` when this program is a *fragment* of a larger build whose
46    /// other parts are compiled separately — specifically a dependency
47    /// module under the VM's per-module compile (the VM lowers each
48    /// `depends [...]` module to its own `MirProgram`). Such a fragment
49    /// does NOT see the entry/sibling call sites, so whole-program
50    /// analyses that rely on "every caller is visible" (notably
51    /// `own_param_refine`) must bail. `false` for a whole-program compile
52    /// (single entry, or the flattened wasm-gc / Rust builds), where all
53    /// callers are present and graduation is sound.
54    pub external_callers_possible: bool,
55}
56
57impl MirProgram {
58    /// Construct an empty program. Phase 3 lowering will populate
59    /// `fns` and `modules` as it walks `ResolvedProgramView`.
60    pub fn empty() -> Self {
61        Self::default()
62    }
63
64    /// Lookup a function by its `FnId`. Returns `None` if the id
65    /// wasn't part of this compilation (e.g. a stale id surviving
66    /// from a previous program view).
67    pub fn fn_by_id(&self, id: FnId) -> Option<&MirFn> {
68        self.fns.get(&id)
69    }
70
71    /// All functions in arbitrary (`HashMap`) order. Callers that
72    /// need a stable walk (snapshot tests, dump output) should sort
73    /// by `FnId` themselves.
74    pub fn iter(&self) -> impl Iterator<Item = (&FnId, &MirFn)> {
75        self.fns.iter()
76    }
77
78    /// Phase 6 wave 11 — look up the canonical name behind a
79    /// `BuiltinId` minted by this program's lowering. Returns
80    /// `""` (empty slice) if the id is out of range — a defensive
81    /// fallback for diagnostic paths; consumers in the hot path
82    /// should hold valid ids by construction.
83    pub fn builtin_name(&self, id: crate::ir::BuiltinId) -> &str {
84        self.builtins
85            .get(id.0 as usize)
86            .map(String::as_str)
87            .unwrap_or("")
88    }
89
90    /// Phase 6 wave 11 — intern a built-in fn name into this
91    /// program's table. Returns the stable `BuiltinId`, reusing
92    /// the existing slot when the name has already been
93    /// registered. Lowering calls this; downstream optimizer
94    /// passes inherit the existing ids when they construct new
95    /// `MirCallee::Builtin` instances (e.g. from inlining).
96    pub fn intern_builtin(&mut self, name: &str) -> crate::ir::BuiltinId {
97        for (idx, existing) in self.builtins.iter().enumerate() {
98            if existing == name {
99                return crate::ir::BuiltinId(idx as u32);
100            }
101        }
102        let id = crate::ir::BuiltinId(self.builtins.len() as u32);
103        self.builtins.push(name.to_string());
104        id
105    }
106}
107
108/// One function in Core MIR. Body is a single `MirExpr` — MIR is
109/// expression-based, so the function's body is the expression that
110/// produces its return value. `Let` chains within the body bind
111/// intermediate results; there's no separate "block" or
112/// "terminator" concept at this phase.
113#[derive(Debug, Clone)]
114pub struct MirFn {
115    /// Stable identity for this function. Matches the `FnId` issued
116    /// by `SymbolTable` during the existing pipeline; downstream
117    /// consumers can cross-reference `ProofIR` / `ProgramShape` by
118    /// the same id.
119    pub fn_id: FnId,
120    /// Source-level name. Carried for dumps + diagnostics; backends
121    /// must not key off this string for identity — that's what
122    /// `fn_id` is for.
123    pub name: String,
124    /// Parameters in declaration order. Each gets a fresh `LocalId`
125    /// at lowering time so the body can refer to it.
126    pub params: Vec<MirParam>,
127    /// Source-level return type annotation as it appears on the fn
128    /// signature. Phase 4 backends consume this for VM stack-slot
129    /// layout; later phases may replace it with a richer type
130    /// representation when the typechecker's `Type` enum becomes
131    /// the universal vocabulary.
132    pub return_type: String,
133    /// Declared effects (`! [Namespace.method, ...]`). Function-
134    /// level only — per-call-site effect annotation is deferred to
135    /// the Phase 6 optimizer track per the RFC.
136    pub effects: Vec<MirEffectAnnotation>,
137    /// The single expression that, when evaluated, produces the
138    /// function's return value. Wrapped in `Spanned` so the body's
139    /// own source location stays available to dumps / diagnostics
140    /// (the surrounding `MirFn` has its own identity layer via
141    /// `fn_id`, but the body span is needed for sub-expression
142    /// errors that don't carry a closer one).
143    pub body: Spanned<MirExpr>,
144    /// Number of frame slots the lowered body needs: the resolver's
145    /// `local_count` plus any synthetic slots minted for opaque-let
146    /// temps during stmt-chain lowering. The VM walker reserves this
147    /// many slots; using the resolver's count alone underruns the
148    /// frame when the body stores into a synthetic slot.
149    pub local_count: u32,
150    /// Per-slot alias-proneness, indexed by `LocalId`/slot: `true`
151    /// when the resolver's alias analysis flagged the slot as possibly
152    /// sharing its backing engine array/struct with another binding.
153    /// Backends combine it with a node's `last_use` to gate the
154    /// owned-mutate fast path (`owned = last_use && !aliased`) — the
155    /// VM's owned-mask and the wasm-gc clone-on-write skip. Carried on
156    /// the MIR fn so MIR consumers read this ownership fact off MIR
157    /// instead of reaching back into the AST `FnResolution`
158    /// side-channel. Cloned from the resolver at lowering today; a
159    /// later phase recomputes it as a MIR analysis pass. An
160    /// out-of-range slot reads `false` (not aliased → fast path sound),
161    /// matching the resolver tables.
162    pub aliased_slots: std::sync::Arc<Vec<bool>>,
163}
164
165/// One formal parameter. The `LocalId` is assigned at lowering
166/// time and is the binding the function body refers to.
167#[derive(Debug, Clone)]
168pub struct MirParam {
169    /// Local binding introduced for this parameter. The function
170    /// body refers to it via `MirExpr::Local(LocalId)`.
171    pub local: LocalId,
172    /// Source-level parameter name. For dumps + diagnostics only.
173    pub name: String,
174    /// Source-level type annotation. Same caveat as `MirFn::return_type`.
175    pub ty: String,
176}
177
178/// Local binding identifier. Unique per function body (not per
179/// program) — backends look up the binding by walking the body's
180/// `Let` chain, so collision across functions isn't a concern.
181///
182/// Phase 2 picks "assign at MIR construction time" over "carry the
183/// HIR slot index" (the latter was the strawman alternative listed
184/// in the RFC). The carry-from-HIR option would have meant MIR's
185/// local space matches whatever the resolver chose — fine for the
186/// VM consumer, but the future inliner / monomorphizer needs the
187/// freedom to introduce fresh locals during optimization passes,
188/// and inheriting HIR's numbering would silently overlap with
189/// those.
190///
191/// **Seed during Phase 3 (waves 1-3b):** the lowerer seeds new
192/// `LocalId`s from the resolver's slot indices
193/// (`ResolvedExpr::Resolved { slot, .. }`, `FnResolution.local_slots`,
194/// `ResolvedMatchArm.binding_slots`) and grows synthetic locals
195/// from `local_count` upward for `Stmt::Expr` intermediates the
196/// resolver didn't see. The slot space is unique per function so
197/// reuse is safe. Later optimizer passes are still free to
198/// renumber — `LocalId` is just an opaque identity per body, not
199/// a promise about resolver lineage.
200#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)]
201pub struct LocalId(pub u32);
202
203impl std::fmt::Display for LocalId {
204    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
205        write!(f, "%{}", self.0)
206    }
207}