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    /// **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}