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}
188
189/// `let binding = value; body`.
190///
191/// Phase 5 wave-Let foundation: `binding_name` carries the
192/// source-level binder when the let came from `Stmt::Binding`,
193/// or stays empty for synthetic locals introduced by stmt-chain
194/// lowering (`Stmt::Expr` at non-tail position). Same propagation
195/// shape `MirLocal { name }` uses on the read side, so Rust /
196/// wasm-gc backends can emit `let x = …` for source-named
197/// bindings and fall back to HIR for the unnamed synthetics.
198#[derive(Debug, Clone)]
199pub struct MirLet {
200    pub binding: LocalId,
201    pub binding_name: String,
202    pub value: Box<Spanned<MirExpr>>,
203    pub body: Box<Spanned<MirExpr>>,
204}
205
206/// Apply `callee` to `args`.
207#[derive(Debug, Clone)]
208pub struct MirCall {
209    pub callee: MirCallee,
210    pub args: Vec<Spanned<MirExpr>>,
211}
212
213/// What we're calling. Two flavors during Phase 2–3; richer
214/// Built-in callees lift to `BuiltinId` since Phase 6 wave 11
215/// — backends look up the canonical name via
216/// `SymbolTable::builtin_entry(id)` when they need to dispatch
217/// against the existing string-keyed runtime registries.
218///
219/// Not `Copy`: the `LocalSlot` variant carries the source param
220/// `name` (re-added in W6/Stage-0 so the Rust walker can emit the
221/// fn-pointer call by name), and `String` isn't `Copy`. The
222/// slot-/id-keyed consumers (VM, wasm-gc) never copied the callee
223/// out by value — they pattern-match through a `&MirCall` — so the
224/// loss of `Copy` is inert there.
225#[derive(Debug, Clone, PartialEq, Eq)]
226pub enum MirCallee {
227    /// User-defined function (any module, including the current
228    /// one). Resolved at HIR → MIR lowering — never a string.
229    Fn(FnId),
230    /// Built-in registered in the runtime's builtin table
231    /// (`Console.print`, `List.prepend`, …). Interned via
232    /// `SymbolTable::intern_builtin` at lowering time; consumers
233    /// resolve the canonical name through
234    /// `SymbolTable::builtin_entry(id).name`.
235    Builtin(BuiltinId),
236    /// Synthesis-only intrinsic — the buffer-build / stringify ops the
237    /// deforestation pass (`interp_lower`) emits for `String.join` and
238    /// interpolation chains (`__buf_new` / `__buf_append` /
239    /// `__buf_append_sep_unless_first` / `__buf_finalize` / `__to_str`).
240    /// Never user-visible; carried so the MIR walker emits the same
241    /// `BUFFER_*` / CONCAT opcodes the HIR walker does instead of
242    /// dropping the whole fn to the HIR fallback.
243    Intrinsic(BuiltinIntrinsic),
244    /// First-class fn value held in a local slot — calling a `Fn(..)`
245    /// parameter or a let-bound fn value (`f(x)` where `f` is a slot).
246    /// The backend pushes the slot value (the callee) then the args and
247    /// dispatches dynamically (the VM's `CALL_VALUE`). `last_use` lets
248    /// the read use `MOVE_LOCAL` over `LOAD_LOCAL`.
249    ///
250    /// `name` carries the source-level binder of the slot (the `Fn(..)`
251    /// param name). The slot-addressed VM ignores it; the Rust walker
252    /// needs it to emit the fn-pointer call by name (`name(args…)`),
253    /// mirroring HIR's `ResolvedCallee::LocalSlot { name, .. }`. Threaded
254    /// through verbatim from HIR at lowering (`lower.rs`).
255    LocalSlot {
256        slot: u16,
257        name: String,
258        last_use: bool,
259    },
260}
261
262/// `target(args…)` in tail position — same SCC as the surrounding fn.
263#[derive(Debug, Clone)]
264pub struct MirTailCall {
265    pub target: FnId,
266    pub args: Vec<Spanned<MirExpr>>,
267}
268
269/// `lhs <op> rhs`.
270#[derive(Debug, Clone)]
271pub struct MirBinOp {
272    pub op: BinOp,
273    pub lhs: Box<Spanned<MirExpr>>,
274    pub rhs: Box<Spanned<MirExpr>>,
275}
276
277/// Structured match expression.
278#[derive(Debug, Clone)]
279pub struct MirMatch {
280    pub subject: Box<Spanned<MirExpr>>,
281    pub arms: Vec<MirMatchArm>,
282}
283
284/// `if cond { then_branch } else { else_branch }` — direct
285/// conditional, no pattern dispatch. Phase 6 wave 9 introduces
286/// this so every backend gets it for free instead of
287/// re-implementing the "two-arm bool match → if/else"
288/// recognition (HIR's `try_emit_bool_if_else` etc). The
289/// optimizer pass `bool_match_to_if` rewrites qualifying
290/// `Match` nodes into `IfThenElse`; backends consume only the
291/// rewritten form.
292#[derive(Debug, Clone)]
293pub struct MirIfThenElse {
294    pub cond: Box<Spanned<MirExpr>>,
295    pub then_branch: Box<Spanned<MirExpr>>,
296    pub else_branch: Box<Spanned<MirExpr>>,
297}
298
299/// One arm of a `match`. Pattern picks the variant; `body` is the
300/// value produced when this arm fires.
301#[derive(Debug, Clone)]
302pub struct MirMatchArm {
303    pub pattern: MirPattern,
304    pub body: Spanned<MirExpr>,
305}
306
307/// Pattern shape for `match` arms. Identity-typed where applicable
308/// (constructor patterns reference `CtorId`); `LocalId` is the
309/// fresh local introduced by the binding form.
310#[derive(Debug, Clone)]
311pub enum MirPattern {
312    /// `_` — catch-all, binds nothing.
313    Wildcard,
314    /// Literal arm: `0`, `true`, `"foo"`.
315    Literal(Literal),
316    /// Identifier binding — captures the matched value into a
317    /// fresh local accessible in the arm body. The `String`
318    /// carries the source-level binder name (Phase 5 prep) so
319    /// Rust / wasm-gc walkers can emit the binding ident.
320    Bind(LocalId, String),
321    /// `[]` — empty-list pattern.
322    EmptyList,
323    /// `[head, ..tail]` — cons pattern; both bindings fresh.
324    /// `head_name` / `tail_name` carry the source idents (Phase 5
325    /// prep) for backends that emit named locals.
326    Cons {
327        head: LocalId,
328        head_name: String,
329        tail: LocalId,
330        tail_name: String,
331    },
332    /// `(a, b, c)` — tuple pattern; each component is a sub-pattern.
333    Tuple(Vec<MirPattern>),
334    /// `Module.Variant(b1, b2, …)` / `Result.Ok(b)` / `Option.None`
335    /// — constructor pattern. `ctor` discriminates user vs built-in
336    /// variant via `MirCtor`; `bindings` are the fresh locals for
337    /// the variant's fields in declaration order (empty for
338    /// nullary variants like `Option.None`). `binding_names` is a
339    /// parallel array of source idents (Phase 5 prep) — same
340    /// length as `bindings`.
341    Ctor {
342        ctor: MirCtor,
343        bindings: Vec<LocalId>,
344        binding_names: Vec<String>,
345    },
346}
347
348/// Construct a sum-type variant. `ctor` discriminates user vs
349/// built-in via `MirCtor` (wave 3c-i — built-in ctors now ride
350/// the same node as user ctors instead of being dropped from the
351/// MIR program).
352#[derive(Debug, Clone)]
353pub struct MirConstruct {
354    pub ctor: MirCtor,
355    pub args: Vec<Spanned<MirExpr>>,
356}
357
358/// Build a fresh record. `type_id` is `Some` for user-declared
359/// records; built-in product types (`HttpResponse`, `Header`,
360/// `Buffer`, …) carry no user `TypeId`, so they ride `type_name`
361/// alone (the canonical builtin name the arena registers them under).
362#[derive(Debug, Clone)]
363pub struct MirRecordCreate {
364    pub type_id: Option<TypeId>,
365    pub type_name: String,
366    pub fields: Vec<MirRecordField>,
367}
368
369/// `T.update(base, …)` — produce a record matching `base` except
370/// for the named field overrides. `type_id` / `type_name` follow the
371/// same user-vs-built-in split as [`MirRecordCreate`].
372#[derive(Debug, Clone)]
373pub struct MirRecordUpdate {
374    pub base: Box<Spanned<MirExpr>>,
375    pub type_id: Option<TypeId>,
376    pub type_name: String,
377    pub updates: Vec<MirRecordField>,
378}
379
380/// One `field = value` pair inside a record create or update.
381#[derive(Debug, Clone)]
382pub struct MirRecordField {
383    pub name: String,
384    pub value: Spanned<MirExpr>,
385}
386
387/// `base.field` projection.
388#[derive(Debug, Clone)]
389pub struct MirProject {
390    pub base: Box<Spanned<MirExpr>>,
391    pub field: String,
392}
393
394/// `(a, b, c)!` or `(a, b, c)?!`.
395#[derive(Debug, Clone)]
396pub struct MirIndependentProduct {
397    pub items: Vec<Spanned<MirExpr>>,
398    /// `true` for `?!` (unwrap `Ok`, propagate first `Err`);
399    /// `false` for `!` (raw tuple of `Result`s).
400    pub unwrap_results: bool,
401}
402
403/// Part of an interpolated string.
404#[derive(Debug, Clone)]
405pub enum MirStrPart {
406    /// Literal text between interpolation slots.
407    Literal(String),
408    /// `{expr}` — value gets stringified at runtime.
409    Expr(Spanned<MirExpr>),
410}
411
412/// Declared effect on a `MirFn`. Carries the source name
413/// (`"Console.print"`, `"Disk.readText"`) for now; a later phase
414/// may swap in `EffectId` for typed identity once the effect
415/// registry grows ids.
416#[derive(Debug, Clone)]
417pub struct MirEffectAnnotation {
418    pub name: String,
419}
420
421// `Spanned<T>` is re-used from `crate::ast::Spanned` — it already
422// carries a `SourceLine` plus a `OnceLock<Type>` type stamp, which
423// matches MIR's need to surface type-check results into the
424// executable substrate for later optimization passes. Defining a
425// second MIR-private `Spanned` would mean either dropping the type
426// stamp (losing information) or duplicating the OnceLock wiring;
427// reusing the AST helper keeps lowering trivial and the dump
428// output uniform across IR layers.