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 /// **Int-representation tags (ETAP-2).** Populated ONLY by the
164 /// `bare_i64::rewrite_for_rust` MIR->MIR pass on the per-target clone
165 /// the Rust backend codegens from; default-empty everywhere else (so
166 /// the VM / wasm-gc / proof MIR keep all-`Int` representation). When a
167 /// slot is in [`MirFnRepr::bare_slots`] it is a raw machine `i64` for
168 /// its whole lifetime (its reads render native `i64`, arithmetic over
169 /// raw slots stays raw); every crossing into an `Int` context is an
170 /// explicit [`super::expr::MirExpr::Box`] node the rewrite inserted.
171 /// This is the per-value representation tag made explicit ON the IR --
172 /// backends read it off the rewritten `MirFn` instead of re-deriving
173 /// from the `BareI64Facts` side table.
174 pub repr: MirFnRepr,
175}
176
177/// Per-fn Int-representation summary made explicit on the rewritten MIR
178/// (ETAP-2 SLICE 1). Default-empty ⇒ everything is the arbitrary-precision
179/// `Int` (`aver_rt::AverInt`), the fail-closed baseline. Populated only by
180/// `bare_i64::rewrite_for_rust`.
181#[derive(Debug, Clone, Default)]
182pub struct MirFnRepr {
183 /// Locals (params + let bindings + match aliases) the rewrite tagged as
184 /// raw machine `i64`. A read of such a slot renders as a native `i64`
185 /// ident; arithmetic over raw slots stays raw. A missing slot is `Int`
186 /// (boxed) -- fail-closed.
187 pub bare_slots: std::collections::HashSet<LocalId>,
188 /// Per-param representation, indexed by declaration order (same indexing
189 /// `aliased_slots` / `own_param` use): `true` ⟺ the param's Rust
190 /// signature type is bare `i64` (and every caller `Box`/`Unbox`es at the
191 /// boundary).
192 pub bare_params: Vec<bool>,
193 /// `true` ⟺ the fn's Rust return type is bare `i64`.
194 pub bare_return: bool,
195 /// ETAP-2 carrier-`i64` (wasm-gc only): slots holding a BARE carrier value
196 /// — an eligible refinement-via-opaque carrier whose wasm storage IS a
197 /// native `i64`. A `Project(Local(slot), "value")` over such a slot reads
198 /// the i64 DIRECTLY (the codegen skips the `$AverInt` project bridge), so
199 /// the `.value` read is a raw-i64 leaf for the native arithmetic the
200 /// rewrite left raw. Empty on the Rust backend (carriers stay structs) and
201 /// whenever no eligible carrier is in scope — the byte-identical default.
202 pub carrier_slots: std::collections::HashSet<LocalId>,
203}
204
205impl MirFnRepr {
206 /// Is the value bound to `slot` represented as a raw machine `i64`?
207 pub fn slot_is_bare(&self, slot: LocalId) -> bool {
208 self.bare_slots.contains(&slot)
209 }
210
211 /// ETAP-2 carrier-`i64`: does `slot` hold a bare carrier whose `.value`
212 /// read renders as a raw native `i64` (no project bridge)?
213 pub fn slot_is_bare_carrier(&self, slot: LocalId) -> bool {
214 self.carrier_slots.contains(&slot)
215 }
216
217 /// Is param index `i` bare in the Rust signature?
218 pub fn param_is_bare(&self, i: usize) -> bool {
219 self.bare_params.get(i).copied().unwrap_or(false)
220 }
221}
222
223/// One formal parameter. The `LocalId` is assigned at lowering
224/// time and is the binding the function body refers to.
225#[derive(Debug, Clone)]
226pub struct MirParam {
227 /// Local binding introduced for this parameter. The function
228 /// body refers to it via `MirExpr::Local(LocalId)`.
229 pub local: LocalId,
230 /// Source-level parameter name. For dumps + diagnostics only.
231 pub name: String,
232 /// Source-level type annotation. Same caveat as `MirFn::return_type`.
233 pub ty: String,
234}
235
236/// Local binding identifier. Unique per function body (not per
237/// program) — backends look up the binding by walking the body's
238/// `Let` chain, so collision across functions isn't a concern.
239///
240/// Phase 2 picks "assign at MIR construction time" over "carry the
241/// HIR slot index" (the latter was the strawman alternative listed
242/// in the RFC). The carry-from-HIR option would have meant MIR's
243/// local space matches whatever the resolver chose — fine for the
244/// VM consumer, but the future inliner / monomorphizer needs the
245/// freedom to introduce fresh locals during optimization passes,
246/// and inheriting HIR's numbering would silently overlap with
247/// those.
248///
249/// **Seed during Phase 3 (waves 1-3b):** the lowerer seeds new
250/// `LocalId`s from the resolver's slot indices
251/// (`ResolvedExpr::Resolved { slot, .. }`, `FnResolution.local_slots`,
252/// `ResolvedMatchArm.binding_slots`) and grows synthetic locals
253/// from `local_count` upward for `Stmt::Expr` intermediates the
254/// resolver didn't see. The slot space is unique per function so
255/// reuse is safe. Later optimizer passes are still free to
256/// renumber — `LocalId` is just an opaque identity per body, not
257/// a promise about resolver lineage.
258#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)]
259pub struct LocalId(pub u32);
260
261impl std::fmt::Display for LocalId {
262 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
263 write!(f, "%{}", self.0)
264 }
265}