Skip to main content

aver/ir/mir/
expr.rs

1//! `MirExpr` + supporting node types.
2//!
3//! Phase 2a of #252. Expression-based (every `MirExpr` is a value),
4//! structured (no terminator-style basic blocks), and identity-typed
5//! (declaration refs go through `FnId` / `TypeId` / `CtorId`).
6//!
7//! The shape decisions pinned in the Phase 1 RFC live here as code:
8//!
9//! - `Try` is its own variant — *not* desugared to `Match`.
10//! - `Match` is structured with `Vec<MirMatchArm>` — no flat
11//!   switch / jump representation.
12//! - Constructors carry `CtorId`, record types carry `TypeId`,
13//!   tail-call targets carry `FnId`.
14//! - Each node carries a `Span` for diagnostics + future
15//!   correlation with `ProofIR`.
16
17use crate::ast::{BinOp, Literal, Spanned};
18use crate::ir::hir::{BuiltinCtor, BuiltinIntrinsic};
19use crate::ir::{BuiltinId, CtorId, FnId, TypeId};
20
21use super::program::LocalId;
22
23/// Local-read with last-use annotation. Phase 6 wave 4. Slot is
24/// the binding identity; `last_use = true` when this is the
25/// final read of that slot in the enclosing fn body, mirroring
26/// HIR's `AnnotBool` last-use stamp.
27/// Phase 5 prep: `name` carries the source-level binding name
28/// (param name, let binding name, pattern binding name). VM
29/// backends ignore it (dispatch by slot). Rust / wasm-gc
30/// backends use it as the emitted Rust ident / export name.
31/// Empty for synthetic locals.
32#[derive(Debug, Clone, PartialEq, Eq)]
33pub struct MirLocal {
34    pub slot: LocalId,
35    pub last_use: bool,
36    pub name: String,
37}
38
39impl MirLocal {
40    /// Construct a `MirLocal` with `last_use = false` and empty
41    /// name. Convenience for hand-built test fixtures.
42    pub fn at(slot: LocalId) -> Self {
43        Self {
44            slot,
45            last_use: false,
46            name: String::new(),
47        }
48    }
49}
50
51impl std::fmt::Display for MirLocal {
52    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
53        if self.last_use {
54            write!(f, "{}*", self.slot)
55        } else {
56            write!(f, "{}", self.slot)
57        }
58    }
59}
60
61/// Typed identity for a constructor reference inside MIR. Two
62/// flavors: user-declared variants identified by stable `CtorId`,
63/// and language-level built-in constructors (`Result.Ok` /
64/// `Result.Err` / `Option.Some` / `Option.None`) that don't get
65/// user-program ids because they're not user-declared.
66///
67/// Wave 3c-i pin: built-in ctors travel through the same
68/// `MirConstruct` / `MirPattern::Ctor` shape as user ctors, so
69/// every consumer reading constructor identity goes through one
70/// matchable enum — no separate "is this Result.Ok" string check
71/// scattered across backends.
72#[derive(Debug, Clone, Copy, PartialEq, Eq)]
73pub enum MirCtor {
74    /// User-declared sum-type variant or record constructor.
75    User(CtorId),
76    /// Language-level built-in (`Result.Ok` / `Result.Err` /
77    /// `Option.Some` / `Option.None`). Same `BuiltinCtor` enum
78    /// the resolver pass uses, re-exported by `super::*`.
79    Builtin(BuiltinCtor),
80}
81
82/// One MIR expression. Every variant is a value — there's no
83/// separate statement form at this phase. Sequencing happens via
84/// `Let`.
85#[derive(Debug, Clone)]
86pub enum MirExpr {
87    /// A literal value (`Int`, `Float`, `Bool`, `String`, `Unit`).
88    /// Same vocabulary the existing typed AST already uses.
89    Literal(Spanned<Literal>),
90    /// Read a previously-bound local. The `LocalId` was introduced
91    /// either as a function parameter (`MirParam::local`) or via a
92    /// `Let` in this body's lexical scope. Phase 6 wave 4: carries
93    /// a `last_use` flag the lowerer propagates from HIR's
94    /// `ResolvedExpr::Resolved { last_use, .. }` so backends can
95    /// emit `MOVE_LOCAL` (skipping ref-count bumps) on the final
96    /// read of a slot.
97    Local(Spanned<MirLocal>),
98    /// `let binding = value; body` — sequence two expressions and
99    /// surface the second's value. Phase 2a's only sequencing
100    /// primitive; everything else inside a function body composes
101    /// from this + the value-form variants below.
102    Let(Spanned<MirLet>),
103    /// Apply a callee (user fn / builtin) to arguments. The callee
104    /// kind discriminates so backends know whether to look up via
105    /// `FnId` (typed identity) or via the named-builtin registry.
106    Call(Spanned<MirCall>),
107    /// Tail call to a user fn — same SCC as the surrounding fn.
108    /// Backends decide the final shape (wasm-gc tail-call insn,
109    /// VM tail dispatch, Rust loop rewrite).
110    TailCall(Spanned<MirTailCall>),
111    /// Binary operator over numeric / boolean operands. Same set as
112    /// `ast::BinOp` — MIR doesn't normalize arithmetic here; that's
113    /// a Phase 6 optimizer concern.
114    BinOp(Spanned<MirBinOp>),
115    /// Unary numeric negation. Distinct from `BinOp(Sub, 0, x)` so
116    /// IEEE-754 `-0.0` semantics are preserved on `Float`.
117    Neg(Box<Spanned<MirExpr>>),
118    /// `match <subject> { arm₁ ; arm₂ ; … }` — structured. Phase 4
119    /// VM walks arms in order, picks the first matching pattern.
120    Match(Spanned<MirMatch>),
121    /// Construct a sum-type variant by `CtorId`. The variant's
122    /// declared fields are filled in argument order.
123    Construct(Spanned<MirConstruct>),
124    /// Build a fresh record of a named product type. Fields are
125    /// (`field_name`, value) pairs to keep the dump readable; the
126    /// declared field order is determined by `TypeId` and
127    /// validated at lowering time.
128    RecordCreate(Spanned<MirRecordCreate>),
129    /// `T.update(base, field = v, …)` — produce a new record that
130    /// matches `base` except for the named field overrides.
131    RecordUpdate(Spanned<MirRecordUpdate>),
132    /// Field access (`base.field`) on a record value.
133    Project(Spanned<MirProject>),
134    /// `if cond { then } else { else }` — direct conditional
135    /// shape introduced by Phase 6 wave 9's `bool_match_to_if`
136    /// pass. Lowering never produces this node directly; the
137    /// optimizer pass rewrites qualifying two-arm `Bool` match
138    /// expressions into it so every backend gets a uniform
139    /// if/else node instead of re-implementing the recognition.
140    IfThenElse(Spanned<MirIfThenElse>),
141    /// `value?` — the canonical `?` propagation. Phase 1's
142    /// most-important pin: this stays a node. Lowering to nested
143    /// `Match` is a per-backend choice, not a pipeline-wide
144    /// transform. Rust will eventually emit `?` native, VM emits
145    /// tag-check + early return.
146    ///
147    /// The bound form `let x = step()?; body` is expressed as
148    /// `MirExpr::Let { binding: x, value: MirExpr::Try(step()),
149    /// body }` — no dedicated `TryBind` variant. The original
150    /// design had one, but it duplicated the semantics of
151    /// `Let { value: Try(_), ... }` exactly. Consumers that need
152    /// to recognize the `?-bind` pattern do so by walking
153    /// `Let` and inspecting `value.node` for `MirExpr::Try`.
154    Try(Box<Spanned<MirExpr>>),
155    /// `[a, b, c]` — list literal. Elements lower to MIR
156    /// expressions; the resulting value is `List<T>` with `T`
157    /// inferred at type-check time.
158    List(Vec<Spanned<MirExpr>>),
159    /// `(a, b, c)` — tuple literal.
160    Tuple(Vec<Spanned<MirExpr>>),
161    /// `{"k" => v, …}` — map literal. Keys + values lower as MIR
162    /// expressions; the resulting value is `Map<K, V>`.
163    MapLiteral(Vec<(Spanned<MirExpr>, Spanned<MirExpr>)>),
164    /// `"…{expr}…"` — interpolated string. Each part is either a
165    /// literal text segment or an embedded MIR expression whose
166    /// value gets stringified at runtime.
167    InterpolatedStr(Vec<MirStrPart>),
168    /// Independent product: `(a, b, c)!` or `(a, b, c)?!`. The
169    /// `unwrap_results` flag captures the `?` form (every element
170    /// must be `Result<…>`; `Err` short-circuits with the first
171    /// error). Schedule (`complete` / `cancel` / `sequential`) is
172    /// an aver.toml runtime policy and is NOT carried in MIR.
173    IndependentProduct(Spanned<MirIndependentProduct>),
174    /// Early `return value;` — used in lowered bodies that have a
175    /// natural early-return shape (the `?` propagation lowering
176    /// inside a backend is the canonical example). Functions that
177    /// don't return early end their body with the final expression
178    /// itself; `Return` is only for the explicit early-exit case.
179    Return(Box<Spanned<MirExpr>>),
180    /// A fn referenced as a *value* (not called): `callWith(dbl)` passes
181    /// `dbl`. Carries the canonical fn / builtin name; the backend
182    /// resolves it to a symbol reference (the VM pushes a `symbol_ref`
183    /// constant, mirroring the HIR walker's `StaticRef` leaf-op). The
184    /// walker falls back to HIR if the name doesn't resolve (a genuinely
185    /// unresolved ident — typecheck-rejected input).
186    FnValue(String),
187    /// **Int-representation boundary (ETAP-2): raw `i64` → `Int`.** The
188    /// inner expression evaluates to a raw machine `i64` (a value the Int
189    /// "unboxing" analysis proved bare); `Box` wraps it back into the
190    /// arbitrary-precision `Int` (`aver_rt::AverInt`) at every escape (a
191    /// return-as-Int, a boxed-callee-param arg, an aggregate/record/map
192    /// store, a stringify, a boxed `Let` crossing, a boxed-arithmetic
193    /// operand).
194    ///
195    /// Inserted ONLY by the `bare_i64::rewrite_for_rust` MIR→MIR pass,
196    /// which runs LATE (after proof export + shape recognition) on a
197    /// per-target clone — so the VM / wasm-gc / proof MIR never contains
198    /// this node. Backends that may see it (the Rust codegen) lower it to
199    /// `aver_rt::AverInt::from_i64(<inner raw>)`; every other walker treats
200    /// it as a transparent pass-through to `inner` (it never appears on
201    /// their path, the arm exists only for exhaustiveness).
202    Box(std::boxed::Box<Spanned<MirExpr>>),
203    /// **Int-representation boundary (ETAP-2): `Int` → raw `i64`.** The dual
204    /// of [`MirExpr::Box`]: the inner expression evaluates to an `Int`
205    /// (`aver_rt::AverInt`) and `Unbox` narrows it to a raw machine `i64`
206    /// via the checked `to_i64()` (the analysis only marks a slot bare when
207    /// it provably fits `i64`, so the narrowing never loses information).
208    /// Inserted by the same rewrite; the Rust codegen lowers it to
209    /// `<inner boxed>.to_i64().expect(...)`.
210    Unbox(std::boxed::Box<Spanned<MirExpr>>),
211}
212
213/// `let binding = value; body`.
214///
215/// Phase 5 wave-Let foundation: `binding_name` carries the
216/// source-level binder when the let came from `Stmt::Binding`,
217/// or stays empty for synthetic locals introduced by stmt-chain
218/// lowering (`Stmt::Expr` at non-tail position). Same propagation
219/// shape `MirLocal { name }` uses on the read side, so Rust /
220/// wasm-gc backends can emit `let x = …` for source-named
221/// bindings and fall back to HIR for the unnamed synthetics.
222#[derive(Debug, Clone)]
223pub struct MirLet {
224    pub binding: LocalId,
225    pub binding_name: String,
226    pub value: Box<Spanned<MirExpr>>,
227    pub body: Box<Spanned<MirExpr>>,
228}
229
230/// Apply `callee` to `args`.
231#[derive(Debug, Clone)]
232pub struct MirCall {
233    pub callee: MirCallee,
234    pub args: Vec<Spanned<MirExpr>>,
235}
236
237/// What we're calling. Two flavors during Phase 2–3; richer
238/// Built-in callees lift to `BuiltinId` since Phase 6 wave 11
239/// — backends look up the canonical name via
240/// `SymbolTable::builtin_entry(id)` when they need to dispatch
241/// against the existing string-keyed runtime registries.
242///
243/// Not `Copy`: the `LocalSlot` variant carries the source param
244/// `name` (re-added in W6/Stage-0 so the Rust walker can emit the
245/// fn-pointer call by name), and `String` isn't `Copy`. The
246/// slot-/id-keyed consumers (VM, wasm-gc) never copied the callee
247/// out by value — they pattern-match through a `&MirCall` — so the
248/// loss of `Copy` is inert there.
249#[derive(Debug, Clone, PartialEq, Eq)]
250pub enum MirCallee {
251    /// User-defined function (any module, including the current
252    /// one). Resolved at HIR → MIR lowering — never a string.
253    Fn(FnId),
254    /// Built-in registered in the runtime's builtin table
255    /// (`Console.print`, `List.prepend`, …). Interned via
256    /// `SymbolTable::intern_builtin` at lowering time; consumers
257    /// resolve the canonical name through
258    /// `SymbolTable::builtin_entry(id).name`.
259    Builtin(BuiltinId),
260    /// Synthesis-only intrinsic — the buffer-build / stringify ops the
261    /// deforestation pass (`interp_lower`) emits for `String.join` and
262    /// interpolation chains (`__buf_new` / `__buf_append` /
263    /// `__buf_append_sep_unless_first` / `__buf_finalize` / `__to_str`).
264    /// Never user-visible; carried so the MIR walker emits the same
265    /// `BUFFER_*` / CONCAT opcodes the HIR walker does instead of
266    /// dropping the whole fn to the HIR fallback.
267    Intrinsic(BuiltinIntrinsic),
268    /// First-class fn value held in a local slot — calling a `Fn(..)`
269    /// parameter or a let-bound fn value (`f(x)` where `f` is a slot).
270    /// The backend pushes the slot value (the callee) then the args and
271    /// dispatches dynamically (the VM's `CALL_VALUE`). `last_use` lets
272    /// the read use `MOVE_LOCAL` over `LOAD_LOCAL`.
273    ///
274    /// `name` carries the source-level binder of the slot (the `Fn(..)`
275    /// param name). The slot-addressed VM ignores it; the Rust walker
276    /// needs it to emit the fn-pointer call by name (`name(args…)`),
277    /// mirroring HIR's `ResolvedCallee::LocalSlot { name, .. }`. Threaded
278    /// through verbatim from HIR at lowering (`lower.rs`).
279    LocalSlot {
280        slot: u16,
281        name: String,
282        last_use: bool,
283    },
284}
285
286/// `target(args…)` in tail position — same SCC as the surrounding fn.
287#[derive(Debug, Clone)]
288pub struct MirTailCall {
289    pub target: FnId,
290    pub args: Vec<Spanned<MirExpr>>,
291}
292
293/// `lhs <op> rhs`.
294#[derive(Debug, Clone)]
295pub struct MirBinOp {
296    pub op: BinOp,
297    pub lhs: Box<Spanned<MirExpr>>,
298    pub rhs: Box<Spanned<MirExpr>>,
299}
300
301/// Structured match expression.
302#[derive(Debug, Clone)]
303pub struct MirMatch {
304    pub subject: Box<Spanned<MirExpr>>,
305    pub arms: Vec<MirMatchArm>,
306}
307
308/// `if cond { then_branch } else { else_branch }` — direct
309/// conditional, no pattern dispatch. Phase 6 wave 9 introduces
310/// this so every backend gets it for free instead of
311/// re-implementing the "two-arm bool match → if/else"
312/// recognition (HIR's `try_emit_bool_if_else` etc). The
313/// optimizer pass `bool_match_to_if` rewrites qualifying
314/// `Match` nodes into `IfThenElse`; backends consume only the
315/// rewritten form.
316#[derive(Debug, Clone)]
317pub struct MirIfThenElse {
318    pub cond: Box<Spanned<MirExpr>>,
319    pub then_branch: Box<Spanned<MirExpr>>,
320    pub else_branch: Box<Spanned<MirExpr>>,
321}
322
323/// One arm of a `match`. Pattern picks the variant; `body` is the
324/// value produced when this arm fires.
325#[derive(Debug, Clone)]
326pub struct MirMatchArm {
327    pub pattern: MirPattern,
328    pub body: Spanned<MirExpr>,
329}
330
331/// Pattern shape for `match` arms. Identity-typed where applicable
332/// (constructor patterns reference `CtorId`); `LocalId` is the
333/// fresh local introduced by the binding form.
334#[derive(Debug, Clone)]
335pub enum MirPattern {
336    /// `_` — catch-all, binds nothing.
337    Wildcard,
338    /// Literal arm: `0`, `true`, `"foo"`.
339    Literal(Literal),
340    /// Identifier binding — captures the matched value into a
341    /// fresh local accessible in the arm body. The `String`
342    /// carries the source-level binder name (Phase 5 prep) so
343    /// Rust / wasm-gc walkers can emit the binding ident.
344    Bind(LocalId, String),
345    /// `[]` — empty-list pattern.
346    EmptyList,
347    /// `[head, ..tail]` — cons pattern; both bindings fresh.
348    /// `head_name` / `tail_name` carry the source idents (Phase 5
349    /// prep) for backends that emit named locals.
350    Cons {
351        head: LocalId,
352        head_name: String,
353        tail: LocalId,
354        tail_name: String,
355    },
356    /// `(a, b, c)` — tuple pattern; each component is a sub-pattern.
357    Tuple(Vec<MirPattern>),
358    /// `Module.Variant(b1, b2, …)` / `Result.Ok(b)` / `Option.None`
359    /// — constructor pattern. `ctor` discriminates user vs built-in
360    /// variant via `MirCtor`; `bindings` are the fresh locals for
361    /// the variant's fields in declaration order (empty for
362    /// nullary variants like `Option.None`). `binding_names` is a
363    /// parallel array of source idents (Phase 5 prep) — same
364    /// length as `bindings`.
365    Ctor {
366        ctor: MirCtor,
367        bindings: Vec<LocalId>,
368        binding_names: Vec<String>,
369    },
370}
371
372/// Construct a sum-type variant. `ctor` discriminates user vs
373/// built-in via `MirCtor` (wave 3c-i — built-in ctors now ride
374/// the same node as user ctors instead of being dropped from the
375/// MIR program).
376#[derive(Debug, Clone)]
377pub struct MirConstruct {
378    pub ctor: MirCtor,
379    pub args: Vec<Spanned<MirExpr>>,
380}
381
382/// Build a fresh record. `type_id` is `Some` for user-declared
383/// records; built-in product types (`HttpResponse`, `Header`,
384/// `Buffer`, …) carry no user `TypeId`, so they ride `type_name`
385/// alone (the canonical builtin name the arena registers them under).
386#[derive(Debug, Clone)]
387pub struct MirRecordCreate {
388    pub type_id: Option<TypeId>,
389    pub type_name: String,
390    pub fields: Vec<MirRecordField>,
391}
392
393/// `T.update(base, …)` — produce a record matching `base` except
394/// for the named field overrides. `type_id` / `type_name` follow the
395/// same user-vs-built-in split as [`MirRecordCreate`].
396#[derive(Debug, Clone)]
397pub struct MirRecordUpdate {
398    pub base: Box<Spanned<MirExpr>>,
399    pub type_id: Option<TypeId>,
400    pub type_name: String,
401    pub updates: Vec<MirRecordField>,
402}
403
404/// One `field = value` pair inside a record create or update.
405#[derive(Debug, Clone)]
406pub struct MirRecordField {
407    pub name: String,
408    pub value: Spanned<MirExpr>,
409}
410
411/// `base.field` projection.
412#[derive(Debug, Clone)]
413pub struct MirProject {
414    pub base: Box<Spanned<MirExpr>>,
415    pub field: String,
416}
417
418/// `(a, b, c)!` or `(a, b, c)?!`.
419#[derive(Debug, Clone)]
420pub struct MirIndependentProduct {
421    pub items: Vec<Spanned<MirExpr>>,
422    /// `true` for `?!` (unwrap `Ok`, propagate first `Err`);
423    /// `false` for `!` (raw tuple of `Result`s).
424    pub unwrap_results: bool,
425}
426
427/// Part of an interpolated string.
428#[derive(Debug, Clone)]
429pub enum MirStrPart {
430    /// Literal text between interpolation slots.
431    Literal(String),
432    /// `{expr}` — value gets stringified at runtime.
433    Expr(Spanned<MirExpr>),
434}
435
436/// Declared effect on a `MirFn`. Carries the source name
437/// (`"Console.print"`, `"Disk.readText"`) for now; a later phase
438/// may swap in `EffectId` for typed identity once the effect
439/// registry grows ids.
440#[derive(Debug, Clone)]
441pub struct MirEffectAnnotation {
442    pub name: String,
443}
444
445// `Spanned<T>` is re-used from `crate::ast::Spanned` — it already
446// carries a `SourceLine` plus a `OnceLock<Type>` type stamp, which
447// matches MIR's need to surface type-check results into the
448// executable substrate for later optimization passes. Defining a
449// second MIR-private `Spanned` would mean either dropping the type
450// stamp (losing information) or duplicating the OnceLock wiring;
451// reusing the AST helper keeps lowering trivial and the dump
452// output uniform across IR layers.