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.