Skip to main content

aver/vm/compiler/
mir.rs

1//! MIR → VM bytecode lowering (Phase 4 vertical slice).
2//!
3//! Parallel to the existing `super::expr` module which walks
4//! `ResolvedExpr` (HIR), this module walks `crate::ir::mir::MirExpr`
5//! and emits the same opcodes. The point is to prove the VM can
6//! consume MIR identically — same `FnChunk`, same `NanValue`
7//! results on the parity corpus.
8//!
9//! ## Scope (Phase 4 PoC)
10//!
11//! Subset of `MirExpr` covered here:
12//! - `Literal` — same opcodes as `super::expr::compile_literal`.
13//! - `Local(LocalId)` — `LOAD_LOCAL` (no `MOVE_LOCAL` / last-use
14//!   optimization yet; MIR doesn't carry last-use bits at this
15//!   wave).
16//! - `BinOp` — typed dispatch via `emit_binop_typed`; falls back
17//!   to the generic opcode when MIR doesn't carry a type stamp.
18//! - `Neg` — same untyped fallback for now.
19//! - `Let { binding, value, body }` — value first, `STORE_LOCAL`
20//!   into the binding slot, body next; body becomes the fn's
21//!   return value.
22//! - `Call { callee, args }` — `MirCallee::Fn(FnId)` resolves
23//!   through the entry's `SymbolTable` (the same path the HIR
24//!   compiler uses).
25//! - `Return(inner)` — explicit early-return form.
26//!
27//! After Phase 4h, every `MirExpr` variant has walker coverage.
28//! Match accepts the flat-pattern subset (4g-1…5); nested
29//! structural subpatterns inside a Tuple still drop to HIR.
30//! Bytecode parity holds on the generic-emit path; HIR's
31//! specialised opcodes (MATCH_DISPATCH_CONST table fast-path,
32//! per-builtin opcodes like LIST_LEN / MAP_GET / UNWRAP_OR)
33//! produce different bytes but identical runtime — that's
34//! Phase 6 work with type-stamp propagation.
35
36use crate::ast::Literal;
37use crate::ast::Spanned;
38use crate::ir::hir::BuiltinCtor;
39use crate::ir::mir::{
40    LocalId, MirCall, MirCallee, MirCtor, MirExpr, MirFn, MirLet, MirPattern, MirProgram,
41    MirStrPart,
42};
43use crate::nan_value::NanValue;
44use crate::vm::builtin::VmBuiltin;
45use crate::vm::opcode::*;
46
47use super::VmSymbolTable;
48use super::resolve_helpers::buffer_intrinsic_opcode;
49
50use super::{CompileError, FnCompiler};
51
52/// Reasons the MIR vertical slice can't compile a given MIR fn yet.
53/// The Phase 4 callers fall back to the HIR path (`super::compile_fn`)
54/// when this fires.
55#[derive(Debug)]
56pub enum MirVmUnsupported {
57    /// Hit a `MirExpr` variant outside the Phase 4 subset.
58    UnsupportedExpr(&'static str),
59    /// Callee shape not yet covered (builtin / non-FnId).
60    UnsupportedCallee,
61    /// Underlying `FnCompiler` reported a compile error mid-emit.
62    InnerError(CompileError),
63}
64
65impl From<CompileError> for MirVmUnsupported {
66    fn from(e: CompileError) -> Self {
67        MirVmUnsupported::InnerError(e)
68    }
69}
70
71impl MirVmUnsupported {
72    /// Surface a walker rejection as a `CompileError`. `compile_top_level`
73    /// pre-checks every lowered top-level expression with
74    /// `mir_expr_compilable` before committing to the MIR walk, so this
75    /// only fires on the optimistic edge cases `can_compile` reports as
76    /// compilable (e.g. an unknown builtin name) — a real internal error
77    /// rather than a routine fallback.
78    pub(super) fn into_compile_error(self, context: &str) -> CompileError {
79        match self {
80            MirVmUnsupported::InnerError(e) => e,
81            MirVmUnsupported::UnsupportedExpr(what) => CompileError {
82                msg: format!("internal error: VM backend cannot lower `{what}` in {context}"),
83            },
84            MirVmUnsupported::UnsupportedCallee => CompileError {
85                msg: format!(
86                    "internal error: VM backend hit an unsupported callee shape in {context}"
87                ),
88            },
89        }
90    }
91}
92
93/// Walk a `MirExpr` and emit VM bytecode into the supplied
94/// `FnCompiler`. Returns `Err(MirVmUnsupported)` for any MirExpr
95/// variant outside the Phase 4 subset — the caller drops back to
96/// HIR compilation in that case.
97pub(super) fn compile_mir_expr(
98    fc: &mut FnCompiler<'_>,
99    expr: &Spanned<MirExpr>,
100) -> Result<(), MirVmUnsupported> {
101    fc.note_line(expr.line);
102    match &expr.node {
103        MirExpr::Literal(lit) => {
104            fc.compile_literal(&lit.node)?;
105            Ok(())
106        }
107        MirExpr::Local(spanned_local) => {
108            let local = &spanned_local.node;
109            // Phase 6 wave 4: emit MOVE_LOCAL when this is the
110            // last read of the slot in the enclosing fn body.
111            // Mirrors HIR's last-use revival; skips ref-count
112            // bumps + lets the VM yard reclaim the slot's heap
113            // value immediately.
114            fc.emit_op(if local.last_use {
115                MOVE_LOCAL
116            } else {
117                LOAD_LOCAL
118            });
119            fc.emit_u8(local.slot.0 as u8);
120            Ok(())
121        }
122        MirExpr::BinOp(spanned_binop) => {
123            let bop = &spanned_binop.node;
124            compile_mir_expr(fc, &bop.lhs)?;
125            compile_mir_expr(fc, &bop.rhs)?;
126            // Phase 6 wave 1: type stamps now propagate from HIR
127            // into MIR sub-nodes via `lower::wrap`. When both
128            // operands carry a primitive numeric stamp, emit the
129            // typed opcode (ADD_INT / ADD_FLOAT / …); otherwise
130            // fall back to the generic dispatcher.
131            emit_binop_typed(fc, bop.op, bop.lhs.ty(), bop.rhs.ty());
132            Ok(())
133        }
134        MirExpr::Neg(inner) => {
135            compile_mir_expr(fc, inner)?;
136            // Same wave: pick the typed NEG when the operand
137            // carries a primitive numeric stamp. Float's typed
138            // path preserves `-0.0` IEEE-754 semantics (the
139            // generic NEG bounces through `0 - x`).
140            let op = match inner.ty() {
141                Some(crate::ast::Type::Int) => NEG_INT,
142                Some(crate::ast::Type::Float) => NEG_FLOAT,
143                _ => NEG,
144            };
145            fc.emit_op(op);
146            Ok(())
147        }
148        MirExpr::Let(spanned_let) => {
149            let MirLet {
150                binding,
151                binding_name: _,
152                value,
153                body,
154            } = &spanned_let.node;
155            compile_mir_expr(fc, value)?;
156            fc.emit_op(STORE_LOCAL);
157            fc.emit_u8(binding.0 as u8);
158            compile_mir_expr(fc, body)
159        }
160        MirExpr::Call(spanned_call) => {
161            let MirCall { callee, args } = &spanned_call.node;
162            match callee {
163                MirCallee::Fn(fn_id) => {
164                    for arg in args {
165                        compile_mir_expr(fc, arg)?;
166                    }
167                    let name = fc.canonical_fn_name(*fn_id)?;
168                    let vm_fn_id = fc.resolve_fn_id_by_name(&name).ok_or_else(|| {
169                        MirVmUnsupported::InnerError(CompileError {
170                            msg: format!(
171                                "MIR-VM: unresolved fn `{name}` (FnId={fn_id:?}) — \
172                                 module not loaded?"
173                            ),
174                        })
175                    })?;
176                    // Always plain CALL_KNOWN, matching the HIR walker
177                    // exactly. The owned mask is inert for a known
178                    // user-fn call at the call boundary (the callee
179                    // derives its parameter ownership from its own alias
180                    // analysis, and the runtime reads the trailing owned
181                    // byte but ignores it), so emitting CALL_KNOWN_OWNED
182                    // bought nothing — and it desynced the leaf /
183                    // parent-thin classifier, which only recognizes
184                    // CALL_KNOWN as a call. A fn calling out via
185                    // CALL_KNOWN_OWNED was wrongly flagged `leaf=true`,
186                    // its callers' plain CALL_KNOWN got upgraded to the
187                    // frameless CALL_LEAF, and the non-leaf fn was then
188                    // invoked without a CallFrame → VM out-of-bounds
189                    // crash on the shipped default path. A future
190                    // cross-call ownership pass that re-enables the owned
191                    // variant must also teach the classifier about it
192                    // (CALL_KNOWN_OWNED is recognized there now as
193                    // defense, but no longer emitted here).
194                    fc.emit_op(CALL_KNOWN);
195                    fc.emit_u16(vm_fn_id as u16);
196                    fc.emit_u8(args.len() as u8);
197                    Ok(())
198                }
199                MirCallee::Builtin(id) => {
200                    let name = fc.mir_program.map(|p| p.builtin_name(*id)).unwrap_or("");
201                    let builtin =
202                        lookup_vm_builtin(name).ok_or(MirVmUnsupported::UnsupportedCallee)?;
203                    // Compound leaf-op recognition — parity with the HIR
204                    // walker's fused VECTOR_GET_OR / VECTOR_SET_OR_KEEP.
205                    // Without it `Option.withDefault(Vector.set(v, i, x), v)`
206                    // emits a generic VECTOR_SET + UNWRAP_OR pair that
207                    // allocates the intermediate `Option<Vector>` and skips
208                    // the in-place mutate / OOB-keep fast path (~+25% on the
209                    // vector_ops accumulator loop once MIR became the default).
210                    if builtin == VmBuiltin::OptionWithDefault
211                        && try_emit_vector_compound(fc, args)?
212                    {
213                        return Ok(());
214                    }
215                    for arg in args {
216                        compile_mir_expr(fc, arg)?;
217                    }
218                    // Phase 6 wave 3 — pick the specialised
219                    // single-opcode emit when the builtin has
220                    // one (mirror of HIR's `emit_builtin_after_args`
221                    // dispatch). Owned/CALL_BUILTIN_OWNED variants
222                    // wait for wave 4's last-use revival; until
223                    // then `owned_mask = 0` everywhere on the
224                    // MIR path, same as Phase 4e.
225                    let owned_mask = compute_builtin_owned_mask(args, fc);
226                    match builtin {
227                        VmBuiltin::ListLen => fc.emit_op(LIST_LEN),
228                        VmBuiltin::ListPrepend => fc.emit_op(LIST_PREPEND),
229                        VmBuiltin::VectorGet => fc.emit_op(VECTOR_GET),
230                        VmBuiltin::VectorSet if owned_mask != 0 => {
231                            // Phase 6 wave 4: HIR routes the
232                            // owned-VectorSet through CALL_BUILTIN_OWNED
233                            // (with take optimization).
234                            let symbol_id = fc.symbols.intern_builtin(builtin).map_err(|e| {
235                                MirVmUnsupported::InnerError(CompileError {
236                                    msg: format!("MIR-VM: intern_builtin failed: {e:?}"),
237                                })
238                            })?;
239                            fc.emit_op(CALL_BUILTIN_OWNED);
240                            fc.emit_u32(symbol_id);
241                            fc.emit_u8(args.len() as u8);
242                            fc.emit_u8(owned_mask);
243                        }
244                        VmBuiltin::VectorSet => fc.emit_op(VECTOR_SET),
245                        VmBuiltin::OptionWithDefault => fc.emit_op(UNWRAP_OR),
246                        VmBuiltin::ResultWithDefault => fc.emit_op(UNWRAP_RESULT_OR),
247                        _ => {
248                            let symbol_id = fc.symbols.intern_builtin(builtin).map_err(|e| {
249                                MirVmUnsupported::InnerError(CompileError {
250                                    msg: format!("MIR-VM: intern_builtin failed: {e:?}"),
251                                })
252                            })?;
253                            if owned_mask != 0 {
254                                fc.emit_op(CALL_BUILTIN_OWNED);
255                                fc.emit_u32(symbol_id);
256                                fc.emit_u8(args.len() as u8);
257                                fc.emit_u8(owned_mask);
258                            } else {
259                                fc.emit_op(CALL_BUILTIN);
260                                fc.emit_u32(symbol_id);
261                                fc.emit_u8(args.len() as u8);
262                            }
263                        }
264                    }
265                    Ok(())
266                }
267                MirCallee::Intrinsic(intrinsic) => {
268                    // Mirror of the HIR walker's `compile_intrinsic_call`:
269                    // the deforestation pass's buffer-build ops map to
270                    // dedicated BUFFER_* opcodes; `__to_str` lowers to the
271                    // CONCAT-against-empty stringify trick.
272                    match buffer_intrinsic_opcode(*intrinsic) {
273                        Some((opcode, arity)) => {
274                            if args.len() != arity {
275                                return Err(MirVmUnsupported::InnerError(CompileError {
276                                    msg: format!(
277                                        "intrinsic {} expects {arity} arg(s), got {}",
278                                        intrinsic.name(),
279                                        args.len()
280                                    ),
281                                }));
282                            }
283                            for arg in args {
284                                compile_mir_expr(fc, arg)?;
285                            }
286                            fc.emit_op(opcode);
287                        }
288                        None => {
289                            // `__to_str(x)`: CONCAT against an empty string
290                            // — CONCAT calls `NanValue::repr` on both sides,
291                            // so any value lowers to its display string.
292                            if args.len() != 1 {
293                                return Err(MirVmUnsupported::InnerError(CompileError {
294                                    msg: format!(
295                                        "intrinsic {} expects 1 arg, got {}",
296                                        intrinsic.name(),
297                                        args.len()
298                                    ),
299                                }));
300                            }
301                            compile_mir_expr(fc, &args[0])?;
302                            let empty = fc.nan_literal(&Literal::Str(String::new()));
303                            let idx = fc.add_constant(empty);
304                            fc.emit_op(LOAD_CONST);
305                            fc.emit_u16(idx);
306                            fc.emit_op(CONCAT);
307                        }
308                    }
309                    Ok(())
310                }
311                MirCallee::LocalSlot {
312                    slot,
313                    last_use,
314                    name: _,
315                } => {
316                    // First-class fn value: push the slot (the callee),
317                    // then the args, then dynamic-dispatch via CALL_VALUE.
318                    // Mirror of the HIR walker's compile_call fallback +
319                    // compile_callee_as_value(LocalSlot). CALL_VALUE reads
320                    // the callee at `stack.len() - 1 - argc`, so it must be
321                    // pushed before the args.
322                    fc.emit_op(if *last_use { MOVE_LOCAL } else { LOAD_LOCAL });
323                    fc.emit_u8(*slot as u8);
324                    for arg in args {
325                        compile_mir_expr(fc, arg)?;
326                    }
327                    fc.emit_op(CALL_VALUE);
328                    fc.emit_u8(args.len() as u8);
329                    Ok(())
330                }
331            }
332        }
333        MirExpr::Return(inner) => {
334            compile_mir_expr(fc, inner)?;
335            fc.emit_op(RETURN);
336            Ok(())
337        }
338
339        // ── Phase 6: fn referenced as a value ───────────────────
340        // `callWith(dbl)` passes `dbl` — a top-level fn / builtin in
341        // value position. Reuse the HIR walker's `compile_ident`
342        // verbatim: it tries local slot → global → module-scope fn
343        // (symbol_ref of the qualified name) → interned symbol with a
344        // kind. Sharing the path guarantees byte-identical emit with
345        // the HIR walker, and an unresolved name surfaces as the same
346        // `CompileError` (folded to `InnerError`, so a malformed input
347        // still drops to the HIR fallback rather than miscompiling).
348        MirExpr::FnValue(name) => {
349            fc.compile_ident(name)?;
350            Ok(())
351        }
352
353        // ── Phase 4c: ctor construction ─────────────────────────
354        MirExpr::Construct(spanned_construct) => {
355            let c = &spanned_construct.node;
356            match c.ctor {
357                MirCtor::Builtin(BuiltinCtor::ResultOk) => {
358                    emit_constructor_arg(fc, c.args.first())?;
359                    fc.emit_op(WRAP);
360                    fc.emit_u8(0);
361                    Ok(())
362                }
363                MirCtor::Builtin(BuiltinCtor::ResultErr) => {
364                    emit_constructor_arg(fc, c.args.first())?;
365                    fc.emit_op(WRAP);
366                    fc.emit_u8(1);
367                    Ok(())
368                }
369                MirCtor::Builtin(BuiltinCtor::OptionSome) => {
370                    emit_constructor_arg(fc, c.args.first())?;
371                    fc.emit_op(WRAP);
372                    fc.emit_u8(2);
373                    Ok(())
374                }
375                MirCtor::Builtin(BuiltinCtor::OptionNone) => {
376                    let idx = fc.add_constant(NanValue::NONE);
377                    fc.emit_op(LOAD_CONST);
378                    fc.emit_u16(idx);
379                    Ok(())
380                }
381                MirCtor::User(ctor_id) => {
382                    // CtorEntry → (owning_type, variant_name) →
383                    // canonical type name → arena type_id +
384                    // variant_id. Same path the HIR walker uses
385                    // for `ResolvedCtor::User`.
386                    let entry = fc.symbol_table.ctor_entry(ctor_id);
387                    let owning_type = entry.owning_type;
388                    let variant_name = entry.name.clone();
389                    let qualified_type_name = fc.canonical_type_name(owning_type)?;
390                    let arena_type_id =
391                        fc.resolve_type_id(&qualified_type_name).ok_or_else(|| {
392                            MirVmUnsupported::InnerError(CompileError {
393                                msg: format!(
394                                    "MIR-VM: unknown arena type for `{qualified_type_name}` \
395                                     (CtorId={ctor_id:?})"
396                                ),
397                            })
398                        })?;
399                    let variant_id =
400                        fc.arena.find_variant_id(arena_type_id, &variant_name).ok_or_else(
401                            || {
402                                MirVmUnsupported::InnerError(CompileError {
403                                    msg: format!(
404                                        "MIR-VM: unknown variant `{variant_name}` on `{qualified_type_name}`"
405                                    ),
406                                })
407                            },
408                        )?;
409                    for arg in &c.args {
410                        compile_mir_expr(fc, arg)?;
411                    }
412                    fc.emit_op(VARIANT_NEW);
413                    fc.emit_u16(arena_type_id as u16);
414                    fc.emit_u16(variant_id);
415                    fc.emit_u8(c.args.len() as u8);
416                    Ok(())
417                }
418            }
419        }
420
421        // ── Phase 4c: record field access ───────────────────────
422        MirExpr::Project(spanned_proj) => {
423            let p = &spanned_proj.node;
424            // RECORD_GET_NAMED is the universal path — VM resolves
425            // the field by symbol id at runtime. The HIR walker
426            // sometimes specializes to RECORD_GET when it can
427            // infer field index statically; that's a Phase 6
428            // optimization we skip here.
429            compile_mir_expr(fc, &p.base)?;
430            let field_symbol_id = fc.symbols.intern_name(&p.field);
431            fc.emit_op(RECORD_GET_NAMED);
432            fc.emit_u32(field_symbol_id);
433            Ok(())
434        }
435
436        // ── Phase 4d: `?` propagation ───────────────────────────
437        MirExpr::Try(inner) => {
438            compile_mir_expr(fc, inner)?;
439            fc.emit_op(PROPAGATE_ERR);
440            Ok(())
441        }
442
443        // ETAP-2 representation boundaries are inserted only by the Rust
444        // codegen rewrite (`bare_i64::rewrite_for_rust`) on a per-target
445        // clone — the VM compiles the SHARED, un-rewritten MIR, so these
446        // never reach here. The VM models every Int as arbitrary-precision,
447        // so `Box`/`Unbox` are representation identities: compile the inner
448        // transparently (defensive — keeps the walker total).
449        MirExpr::Box(inner) | MirExpr::Unbox(inner) => compile_mir_expr(fc, inner),
450
451        // ── Phase 4d: tail-call dispatch ────────────────────────
452        MirExpr::TailCall(spanned_tail) => {
453            let tc = &spanned_tail.node;
454            // Phase 6 wave 4: derive owned_mask from args' last-
455            // use flags BEFORE compiling them (compute looks at
456            // the MirExpr tree, not stack state).
457            let owned_mask = compute_owned_mask(&tc.args, fc);
458            for arg in &tc.args {
459                compile_mir_expr(fc, arg)?;
460            }
461            let target_name = fc.canonical_fn_name(tc.target)?;
462            if target_name == fc.name() {
463                fc.emit_op(TAIL_CALL_SELF);
464                fc.emit_u8(tc.args.len() as u8);
465                fc.emit_u8(owned_mask);
466            } else {
467                let vm_fn_id = fc.resolve_fn_id_by_name(&target_name).ok_or_else(|| {
468                    MirVmUnsupported::InnerError(CompileError {
469                        msg: format!(
470                            "MIR-VM: unresolved tail-call target `{target_name}` \
471                             (FnId={:?})",
472                            tc.target
473                        ),
474                    })
475                })?;
476                fc.emit_op(TAIL_CALL_KNOWN);
477                fc.emit_u16(vm_fn_id as u16);
478                fc.emit_u8(tc.args.len() as u8);
479                fc.emit_u8(owned_mask);
480            }
481            Ok(())
482        }
483
484        // ── Phase 4f: record + list + tuple builders ────────────
485        MirExpr::List(items) => {
486            if items.is_empty() {
487                fc.emit_op(LIST_NIL);
488                return Ok(());
489            }
490            for item in items {
491                compile_mir_expr(fc, item)?;
492            }
493            fc.emit_op(LIST_NEW);
494            fc.emit_u8(items.len() as u8);
495            Ok(())
496        }
497        MirExpr::Tuple(items) => {
498            for item in items {
499                compile_mir_expr(fc, item)?;
500            }
501            fc.emit_op(TUPLE_NEW);
502            fc.emit_u8(items.len() as u8);
503            Ok(())
504        }
505        MirExpr::RecordCreate(spanned_rc) => {
506            let rc = &spanned_rc.node;
507            // Resolve to the arena type id + field order. User records
508            // carry a `TypeId` → canonical name; built-in records
509            // (`HttpResponse`, …) carry no `TypeId` and ride their
510            // canonical `type_name` directly (the arena registers
511            // built-in record types by that name). The arena's field
512            // order is declaration order, which is what RECORD_NEW
513            // expects on the stack.
514            let qualified_type_name = match rc.type_id {
515                Some(id) => fc.canonical_type_name(id)?,
516                None => rc.type_name.clone(),
517            };
518            let arena_type_id = fc.resolve_type_id(&qualified_type_name).ok_or_else(|| {
519                MirVmUnsupported::InnerError(CompileError {
520                    msg: format!(
521                        "MIR-VM: unknown arena type `{qualified_type_name}` for \
522                         RecordCreate (TypeId={:?})",
523                        rc.type_id
524                    ),
525                })
526            })?;
527            let field_names = fc.arena.get_field_names(arena_type_id).to_vec();
528            // Push fields in declared order.
529            for expected_name in &field_names {
530                let field = rc.fields.iter().find(|f| f.name == *expected_name).ok_or_else(
531                    || {
532                        MirVmUnsupported::InnerError(CompileError {
533                            msg: format!(
534                                "MIR-VM: missing field `{expected_name}` in record `{qualified_type_name}`"
535                            ),
536                        })
537                    },
538                )?;
539                compile_mir_expr(fc, &field.value)?;
540            }
541            fc.emit_op(RECORD_NEW);
542            fc.emit_u16(arena_type_id as u16);
543            fc.emit_u8(field_names.len() as u8);
544            Ok(())
545        }
546        MirExpr::RecordUpdate(spanned_ru) => {
547            let ru = &spanned_ru.node;
548            let qualified_type_name = match ru.type_id {
549                Some(id) => fc.canonical_type_name(id)?,
550                None => ru.type_name.clone(),
551            };
552            let arena_type_id = fc.resolve_type_id(&qualified_type_name).ok_or_else(|| {
553                MirVmUnsupported::InnerError(CompileError {
554                    msg: format!(
555                        "MIR-VM: unknown arena type `{qualified_type_name}` for \
556                         RecordUpdate (TypeId={:?})",
557                        ru.type_id
558                    ),
559                })
560            })?;
561            let field_names = fc.arena.get_field_names(arena_type_id).to_vec();
562            let mut updated_indices = Vec::with_capacity(ru.updates.len());
563
564            compile_mir_expr(fc, &ru.base)?;
565
566            for (field_idx, field_name) in field_names.iter().enumerate() {
567                if let Some(field) = ru.updates.iter().find(|f| f.name == *field_name) {
568                    compile_mir_expr(fc, &field.value)?;
569                    updated_indices.push(field_idx as u8);
570                }
571            }
572            fc.emit_op(RECORD_UPDATE);
573            fc.emit_u16(arena_type_id as u16);
574            fc.emit_u8(updated_indices.len() as u8);
575            for idx in updated_indices {
576                fc.emit_u8(idx);
577            }
578            Ok(())
579        }
580
581        // ── Phase 4g-1: match with Wildcard + Literal(Int) arms ──
582        // The HIR walker's `compile_match` is 756 lines and
583        // includes fast-paths (MATCH_DISPATCH_CONST, bool-branch
584        // optimization) we don't replicate here. This sub-PR
585        // handles the smallest reviewable subset: arm patterns
586        // restricted to `Wildcard` and `Literal(Int)`. Any other
587        // pattern variant in any arm falls back to HIR.
588        //
589        // Emit shape (linear fallback, mirrors HIR's tail of
590        // `compile_match`):
591        //   <subject>
592        //   per arm (except last):
593        //     [MATCH_INT_LITERAL imm fail]  // skipped for Wildcard
594        //     POP
595        //     <body>
596        //     JUMP end
597        //     fail: <next arm>
598        //   last arm: skip pattern check entirely (exhaustive),
599        //             POP, <body>
600        //   end:
601        MirExpr::IfThenElse(spanned_ite) => {
602            // Phase 6 wave 9: direct conditional shape.
603            // Emit:
604            //   compile_mir_expr(cond)
605            //   JUMP_IF_FALSE → else_start
606            //   compile_mir_expr(then_branch)
607            //   JUMP → end
608            //   else_start: compile_mir_expr(else_branch)
609            //   end:
610            let ite = &spanned_ite.node;
611            compile_mir_expr(fc, &ite.cond)?;
612            let else_patch = fc.emit_jump(JUMP_IF_FALSE);
613            compile_mir_expr(fc, &ite.then_branch)?;
614            let end_patch = fc.emit_jump(JUMP);
615            fc.patch_jump(else_patch);
616            compile_mir_expr(fc, &ite.else_branch)?;
617            fc.patch_jump(end_patch);
618            Ok(())
619        }
620        MirExpr::Match(spanned_match) => {
621            let m = &spanned_match.node;
622            // A match with no arms can't produce a value — type-check
623            // rejects it as non-exhaustive, so reaching codegen with zero
624            // arms is an invariant violation, not user-reachable on valid
625            // input. Bail with a compile error rather than underflowing
626            // `arms.len() - 1` below (the `last_idx` computation). Found by
627            // the verify-runner fuzzer on a mutated program that slips a
628            // zero-arm match past the front-end.
629            if m.arms.is_empty() {
630                return Err(CompileError {
631                    msg: "match expression has no arms (non-exhaustive match reached codegen)"
632                        .to_string(),
633                }
634                .into());
635            }
636            // Phase 6 wave 2 — try the MATCH_DISPATCH_CONST table
637            // fast-path before falling back to the linear arm-by-
638            // arm emit. When every non-last arm has a literal
639            // pattern + literal body and the last arm is a
640            // wildcard/bind default, HIR's `compile_match` emits
641            // a single jump-table dispatch (MATCH_DISPATCH_CONST)
642            // with inline (kind, expected_bits, result_bits)
643            // entries — one VM op for the whole match. Mirror it
644            // here so MIR matches HIR byte-for-byte on the
645            // common shape.
646            if try_emit_match_dispatch_const(fc, &m.subject, &m.arms)?.is_some() {
647                return Ok(());
648            }
649            compile_mir_expr(fc, &m.subject)?;
650
651            let mut end_jumps: Vec<usize> = Vec::new();
652            let last_idx = m.arms.len() - 1;
653            for (i, arm) in m.arms.iter().enumerate() {
654                let is_last = i == last_idx;
655                let fail_patches: Vec<usize> = if is_last {
656                    // Last arm — exhaustive, no pattern check.
657                    // Bindings still need extracting (value on
658                    // stack, shape known from preceding arm
659                    // failures).
660                    emit_last_arm_bindings(fc, &arm.pattern)?;
661                    Vec::new()
662                } else {
663                    emit_pattern_check(fc, &arm.pattern)?
664                };
665                fc.emit_op(POP);
666                compile_mir_expr(fc, &arm.body)?;
667                if !is_last {
668                    end_jumps.push(fc.emit_jump(JUMP));
669                    let next_arm_start = fc.offset();
670                    for patch in fail_patches {
671                        fc.patch_jump_to(patch, next_arm_start);
672                    }
673                }
674            }
675            for patch in end_jumps {
676                fc.patch_jump(patch);
677            }
678            Ok(())
679        }
680        // ── Phase 4h: remaining MirExpr variants ────────────────
681        MirExpr::MapLiteral(entries) => {
682            // Mirror HIR's `compile_map`: LOAD_CONST an empty
683            // `PersistentMap` from the arena, then per entry
684            // compile key + value and emit a `MapSet` call.
685            let empty_map = fc.arena.push_map(crate::nan_value::PersistentMap::new());
686            let nv = NanValue::new_map(empty_map);
687            let idx = fc.add_constant(nv);
688            fc.emit_op(LOAD_CONST);
689            fc.emit_u16(idx);
690            for (k, v) in entries {
691                compile_mir_expr(fc, k)?;
692                compile_mir_expr(fc, v)?;
693                let symbol_id = fc.symbols.intern_builtin(VmBuiltin::MapSet).map_err(|e| {
694                    MirVmUnsupported::InnerError(CompileError {
695                        msg: format!("MIR-VM: intern_builtin(MapSet) failed: {e:?}"),
696                    })
697                })?;
698                fc.emit_op(CALL_BUILTIN);
699                fc.emit_u32(symbol_id);
700                fc.emit_u8(3);
701            }
702            Ok(())
703        }
704        MirExpr::InterpolatedStr(parts) => {
705            // `interp_lower` upstream of MIR usually desugars
706            // these into buffer-build calls before MIR sees
707            // them, so this branch is rarely reached in
708            // practice — kept for completeness so the walker
709            // covers every `MirExpr` variant.
710            if parts.is_empty() {
711                let nv = NanValue::new_string_value("", fc.arena);
712                let cidx = fc.add_constant(nv);
713                fc.emit_op(LOAD_CONST);
714                fc.emit_u16(cidx);
715                return Ok(());
716            }
717            let mut first = true;
718            for part in parts {
719                match part {
720                    MirStrPart::Literal(s) => {
721                        let nv = NanValue::new_string_value(s, fc.arena);
722                        let cidx = fc.add_constant(nv);
723                        fc.emit_op(LOAD_CONST);
724                        fc.emit_u16(cidx);
725                    }
726                    MirStrPart::Expr(e) => {
727                        compile_mir_expr(fc, e)?;
728                        let empty_nv = NanValue::new_string_value("", fc.arena);
729                        let empty_const = fc.add_constant(empty_nv);
730                        fc.emit_op(LOAD_CONST);
731                        fc.emit_u16(empty_const);
732                        fc.emit_op(CONCAT);
733                    }
734                }
735                if !first {
736                    fc.emit_op(CONCAT);
737                }
738                first = false;
739            }
740            Ok(())
741        }
742        MirExpr::IndependentProduct(spanned_ip) => {
743            let ip = &spanned_ip.node;
744            // Mirror HIR's `compile_independent_product`. Each
745            // item is expected to be a Call (or Try-wrapped
746            // Call) so we push the callee as a value followed
747            // by its args; the dispatcher then emits CALL_PAR
748            // with per-branch arities. If any item shape isn't
749            // call-like, fall back to a sequential TUPLE_NEW
750            // (the same safety net HIR uses).
751            let call_ready = ip.items.iter().all(|item| {
752                let inner = match &item.node {
753                    MirExpr::Try(boxed) => &boxed.node,
754                    other => other,
755                };
756                matches!(inner, MirExpr::Call(_))
757            });
758            if !call_ready {
759                for item in &ip.items {
760                    compile_mir_expr(fc, item)?;
761                }
762                fc.emit_op(TUPLE_NEW);
763                fc.emit_u8(ip.items.len() as u8);
764                return Ok(());
765            }
766            let mut arg_counts: Vec<u8> = Vec::with_capacity(ip.items.len());
767            for item in &ip.items {
768                let inner_call = match &item.node {
769                    MirExpr::Try(boxed) => &boxed.node,
770                    other => other,
771                };
772                let MirExpr::Call(spanned_call) = inner_call else {
773                    unreachable!("call_ready preflight just confirmed");
774                };
775                let call = &spanned_call.node;
776                match &call.callee {
777                    MirCallee::Fn(fn_id) => {
778                        let name = fc.canonical_fn_name(*fn_id)?;
779                        let symbol_id = fc.symbols.find(&name).ok_or_else(|| {
780                            MirVmUnsupported::InnerError(CompileError {
781                                msg: format!("MIR-VM: missing VM symbol for fn `{name}`"),
782                            })
783                        })?;
784                        let idx = fc.add_constant(VmSymbolTable::symbol_ref(symbol_id));
785                        fc.emit_op(LOAD_CONST);
786                        fc.emit_u16(idx);
787                    }
788                    MirCallee::Builtin(id) => {
789                        let name = fc.mir_program.map(|p| p.builtin_name(*id)).unwrap_or("");
790                        let symbol_id = fc.symbols.find(name).ok_or_else(|| {
791                            MirVmUnsupported::InnerError(CompileError {
792                                msg: format!("MIR-VM: missing VM symbol for builtin `{name}`"),
793                            })
794                        })?;
795                        let idx = fc.add_constant(VmSymbolTable::symbol_ref(symbol_id));
796                        fc.emit_op(LOAD_CONST);
797                        fc.emit_u16(idx);
798                    }
799                    // A synthesis intrinsic can't appear as an
800                    // independent-product branch callee (intrinsics are
801                    // never first-class values); a first-class-fn-valued
802                    // IP branch is rare — both fall back conservatively.
803                    MirCallee::Intrinsic(_) | MirCallee::LocalSlot { .. } => {
804                        return Err(MirVmUnsupported::UnsupportedCallee);
805                    }
806                }
807                for arg in &call.args {
808                    compile_mir_expr(fc, arg)?;
809                }
810                arg_counts.push(call.args.len() as u8);
811            }
812            fc.emit_op(CALL_PAR);
813            fc.emit_u8(ip.items.len() as u8);
814            fc.emit_u8(if ip.unwrap_results { 1 } else { 0 });
815            for argc in arg_counts {
816                fc.emit_u8(argc);
817            }
818            Ok(())
819        }
820    }
821}
822
823/// Emit a MIR fn's body into the supplied `FnCompiler` and finish
824/// with `RETURN`. Caller has already constructed `fc` with the
825/// right arity / local_count / local_slots — same path the HIR
826/// compiler takes through `compile_fn_with_scope`.
827pub(super) fn compile_mir_fn_body(
828    fc: &mut FnCompiler<'_>,
829    mir_fn: &MirFn,
830) -> Result<(), MirVmUnsupported> {
831    compile_mir_expr(fc, &mir_fn.body)?;
832    fc.emit_op(RETURN);
833    Ok(())
834}
835
836/// Convenience: walk a `MirProgram` and report which fns the
837/// Phase 4 subset can handle vs which still need HIR fallback.
838/// Useful for parity tests + Phase 4 coverage tracking.
839pub fn classify_mir_program_coverage(mir: &MirProgram) -> MirVmCoverage {
840    let mut covered = 0u32;
841    let mut needs_hir_fallback = 0u32;
842    for mir_fn in mir.fns.values() {
843        if can_compile(&mir_fn.body) {
844            covered += 1;
845        } else {
846            needs_hir_fallback += 1;
847        }
848    }
849    MirVmCoverage {
850        covered,
851        needs_hir_fallback,
852    }
853}
854
855#[derive(Debug, Clone, Copy, Default)]
856pub struct MirVmCoverage {
857    pub covered: u32,
858    pub needs_hir_fallback: u32,
859}
860
861/// Whether the MIR walker can emit bytecode for `expr` without hitting
862/// an `MirVmUnsupported`. Used by `compile_top_level` to pre-check a
863/// batch of lowered top-level value expressions before committing to
864/// the MIR walk, so a mid-emit rejection can't leave half-written
865/// bytecode — it falls back to a clean alternative path instead.
866pub(super) fn mir_expr_compilable(expr: &Spanned<MirExpr>) -> bool {
867    can_compile(expr)
868}
869
870fn can_compile(expr: &Spanned<MirExpr>) -> bool {
871    match &expr.node {
872        MirExpr::Literal(_) => true,
873        MirExpr::Local(_) => true,
874        MirExpr::BinOp(b) => can_compile(&b.node.lhs) && can_compile(&b.node.rhs),
875        MirExpr::Neg(inner) => can_compile(inner),
876        MirExpr::Let(l) => can_compile(&l.node.value) && can_compile(&l.node.body),
877        MirExpr::Call(c) => {
878            let callee_ok = match &c.node.callee {
879                MirCallee::Fn(_) => true,
880                // Phase 6 wave 11: optimistic — the `BuiltinId →
881                // name` resolution lives behind `MirProgram` and
882                // `can_compile` is a standalone walker. The hot
883                // path in `compile_mir_expr` still surfaces
884                // `MirVmUnsupported::UnsupportedCallee` when the
885                // name isn't in `VmBuiltin::ALL`, and the caller
886                // drops back to HIR — so reporting `true` here at
887                // worst costs one rejected compilation attempt
888                // rather than silently mis-emitting.
889                MirCallee::Builtin(_) => true,
890                // Buffer-build / stringify intrinsics emit dedicated
891                // BUFFER_* / CONCAT opcodes — always compilable.
892                MirCallee::Intrinsic(_) => true,
893                // First-class fn value → CALL_VALUE dynamic dispatch.
894                MirCallee::LocalSlot { .. } => true,
895            };
896            callee_ok && c.node.args.iter().all(can_compile)
897        }
898        MirExpr::Return(inner) => can_compile(inner),
899        // Phase 6: fn-as-value. Optimistic, same rationale as the
900        // `Builtin` callee above — `compile_ident` resolves the
901        // symbol on the hot path and surfaces a real error (→ HIR
902        // fallback) if the name doesn't resolve.
903        MirExpr::FnValue(_) => true,
904        // Phase 4c additions:
905        MirExpr::Construct(c) => c.node.args.iter().all(can_compile),
906        MirExpr::Project(p) => can_compile(&p.node.base),
907        // Phase 4d additions:
908        MirExpr::Try(inner) => can_compile(inner),
909        // ETAP-2 boundaries are Rust-codegen-only; never on the VM path.
910        MirExpr::Box(inner) | MirExpr::Unbox(inner) => can_compile(inner),
911        MirExpr::TailCall(t) => t.node.args.iter().all(can_compile),
912        // Phase 4f additions:
913        MirExpr::List(items) => items.iter().all(can_compile),
914        MirExpr::Tuple(items) => items.iter().all(can_compile),
915        MirExpr::RecordCreate(rc) => rc.node.fields.iter().all(|f| can_compile(&f.value)),
916        MirExpr::RecordUpdate(ru) => {
917            can_compile(&ru.node.base) && ru.node.updates.iter().all(|f| can_compile(&f.value))
918        }
919        // Phase 4h additions:
920        MirExpr::MapLiteral(entries) => entries
921            .iter()
922            .all(|(k, v)| can_compile(k) && can_compile(v)),
923        MirExpr::InterpolatedStr(parts) => parts.iter().all(|p| match p {
924            MirStrPart::Literal(_) => true,
925            MirStrPart::Expr(e) => can_compile(e),
926        }),
927        MirExpr::IndependentProduct(ip) => ip.node.items.iter().all(can_compile),
928        // Phase 4i: all `MirPattern` variants supported (Tuple
929        // recurses into subpatterns).
930        // Phase 6 wave 9: direct conditional shape introduced
931        // by `bool_match_to_if`. Every backend (incl. VM)
932        // supports it natively as branch + branch.
933        MirExpr::IfThenElse(ite) => {
934            can_compile(&ite.node.cond)
935                && can_compile(&ite.node.then_branch)
936                && can_compile(&ite.node.else_branch)
937        }
938        MirExpr::Match(m) => {
939            can_compile(&m.node.subject)
940                && m.node
941                    .arms
942                    .iter()
943                    .all(|arm| pattern_supported(&arm.pattern) && can_compile(&arm.body))
944        }
945    }
946}
947
948/// Sentinel `LocalId` value used by the resolver for wildcard
949/// bindings (`_`) inside patterns. The HIR walker treats this
950/// as "no slot to write — POP the value off the stack instead
951/// of `STORE_LOCAL`". MIR carries the sentinel as the same
952/// `u32::from(u16::MAX)` so we can detect it here.
953const WILDCARD_SLOT_SENTINEL: u32 = u16::MAX as u32;
954
955/// Emit either `STORE_LOCAL slot` for a real binding or `POP`
956/// for the wildcard sentinel — mirroring HIR's
957/// `bind_top_to_local`.
958fn emit_store_or_pop(fc: &mut FnCompiler<'_>, local: LocalId) {
959    if local.0 == WILDCARD_SLOT_SENTINEL {
960        fc.emit_op(POP);
961    } else {
962        fc.emit_op(STORE_LOCAL);
963        fc.emit_u8(local.0 as u8);
964    }
965}
966
967/// DUP + (`STORE_LOCAL` or `POP`) — mirror of HIR's
968/// `dup_and_bind_top_to_local`. When the binding is the
969/// wildcard sentinel, DUP + POP is a no-op, so we emit nothing.
970fn emit_dup_and_bind(fc: &mut FnCompiler<'_>, local: LocalId) {
971    if local.0 == WILDCARD_SLOT_SENTINEL {
972        return;
973    }
974    fc.emit_op(DUP);
975    fc.emit_op(STORE_LOCAL);
976    fc.emit_u8(local.0 as u8);
977}
978
979/// Recursive predicate: every `MirPattern` variant is now
980/// walker-supported. Tuple subpatterns recurse so nested
981/// structural patterns work after Phase 4i landed
982/// `emit_nested_subpattern`.
983fn pattern_supported(p: &MirPattern) -> bool {
984    match p {
985        MirPattern::Wildcard
986        | MirPattern::Bind(_, _)
987        | MirPattern::Literal(_)
988        | MirPattern::EmptyList
989        | MirPattern::Cons { .. }
990        | MirPattern::Ctor { .. } => true,
991        MirPattern::Tuple(items) => items.iter().all(pattern_supported),
992    }
993}
994
995/// Emit the pattern check for a non-last arm. Returns the
996/// list of `fail_offset` patch positions the caller will fill
997/// in to point at the next arm's start. Empty `Vec` = pattern
998/// always matches (Wildcard / Bind).
999fn emit_pattern_check(
1000    fc: &mut FnCompiler<'_>,
1001    pattern: &MirPattern,
1002) -> Result<Vec<usize>, MirVmUnsupported> {
1003    match pattern {
1004        MirPattern::Wildcard => Ok(Vec::new()),
1005        MirPattern::Bind(local, _name) => {
1006            // Always matches; DUP the value and bind. Mirror of
1007            // HIR's `dup_and_bind_top_to_local` on a top-level
1008            // ident pattern. Wildcard sentinel collapses the
1009            // DUP+POP to no emit.
1010            emit_dup_and_bind(fc, *local);
1011            Ok(Vec::new())
1012        }
1013        MirPattern::Literal(Literal::Int(v)) => {
1014            fc.emit_op(MATCH_INT_LITERAL);
1015            fc.emit_i64(*v);
1016            let patch = fc.offset();
1017            fc.emit_i16(0);
1018            Ok(vec![patch])
1019        }
1020        MirPattern::Literal(lit) => {
1021            // Generic literal path — DUP + LOAD_CONST + EQ +
1022            // JUMP_IF_FALSE. Mirror of HIR's fallback for non-Int
1023            // literals (Bool / Float / Str / Unit).
1024            fc.emit_op(DUP);
1025            fc.compile_literal(lit)?;
1026            fc.emit_op(EQ);
1027            let patch = fc.emit_jump(JUMP_IF_FALSE);
1028            Ok(vec![patch])
1029        }
1030        MirPattern::EmptyList => {
1031            fc.emit_op(MATCH_NIL);
1032            let patch = fc.offset();
1033            fc.emit_i16(0);
1034            Ok(vec![patch])
1035        }
1036        MirPattern::Cons { head, tail, .. } => {
1037            fc.emit_op(MATCH_CONS);
1038            let patch = fc.offset();
1039            fc.emit_i16(0);
1040            // Successful match: extract head/tail and bind into
1041            // the resolver-assigned slots. The HIR walker does
1042            // the same shape; MIR's `LocalId` directly carries
1043            // the slot.
1044            fc.emit_op(DUP);
1045            fc.emit_op(LIST_HEAD_TAIL);
1046            emit_store_or_pop(fc, *head);
1047            emit_store_or_pop(fc, *tail);
1048            Ok(vec![patch])
1049        }
1050        MirPattern::Ctor {
1051            ctor: MirCtor::User(ctor_id),
1052            bindings,
1053            ..
1054        } => {
1055            // Resolve `CtorId → arena ctor_id` via the same path
1056            // the HIR walker uses (symbol table → canonical name
1057            // → arena type id → variant id → arena ctor id).
1058            let entry = fc.symbol_table.ctor_entry(*ctor_id);
1059            let owning_type = entry.owning_type;
1060            let variant_name = entry.name.clone();
1061            let qualified_type_name = fc.canonical_type_name(owning_type)?;
1062            let arena_type_id = fc.resolve_type_id(&qualified_type_name).ok_or_else(|| {
1063                MirVmUnsupported::InnerError(CompileError {
1064                    msg: format!(
1065                        "MIR-VM: unknown arena type `{qualified_type_name}` for ctor pattern"
1066                    ),
1067                })
1068            })?;
1069            let variant_id = fc
1070                .arena
1071                .find_variant_id(arena_type_id, &variant_name)
1072                .ok_or_else(|| {
1073                    MirVmUnsupported::InnerError(CompileError {
1074                        msg: format!(
1075                            "MIR-VM: unknown variant `{variant_name}` on `{qualified_type_name}`"
1076                        ),
1077                    })
1078                })?;
1079            let arena_ctor_id =
1080                fc.arena.find_ctor_id(arena_type_id, variant_id).ok_or_else(|| {
1081                    MirVmUnsupported::InnerError(CompileError {
1082                        msg: format!(
1083                            "MIR-VM: unknown arena ctor id for `{qualified_type_name}.{variant_name}`"
1084                        ),
1085                    })
1086                })?;
1087            if arena_ctor_id > u16::MAX as u32 {
1088                return Err(MirVmUnsupported::InnerError(CompileError {
1089                    msg: format!(
1090                        "MIR-VM: ctor id too large for MATCH_VARIANT: {qualified_type_name}.{variant_name}"
1091                    ),
1092                }));
1093            }
1094            fc.emit_op(MATCH_VARIANT);
1095            fc.emit_u16(arena_ctor_id as u16);
1096            let patch = fc.offset();
1097            fc.emit_i16(0);
1098            // EXTRACT_FIELD doesn't consume the subject — value
1099            // stays on the stack between field extractions.
1100            // Wildcard `_` bindings carry the sentinel slot;
1101            // `emit_store_or_pop` collapses to POP for those.
1102            for (i, b) in bindings.iter().enumerate() {
1103                fc.emit_op(EXTRACT_FIELD);
1104                fc.emit_u8(i as u8);
1105                emit_store_or_pop(fc, *b);
1106            }
1107            Ok(vec![patch])
1108        }
1109        MirPattern::Tuple(items) => {
1110            // Phase 4i — tuple pattern with arbitrarily-nested
1111            // subpatterns. Emit MATCH_TUPLE for the arity check,
1112            // then walk each subpattern through
1113            // `emit_nested_subpattern` (analog of HIR's
1114            // `compile_extracted_subpattern`) so nested
1115            // structural patterns (Cons / Ctor / Tuple / …) get
1116            // a cleanup-jump if they fail mid-extraction.
1117            fc.emit_op(MATCH_TUPLE);
1118            fc.emit_u8(items.len() as u8);
1119            let tuple_fail = fc.offset();
1120            fc.emit_i16(0);
1121            let mut all_patches = vec![tuple_fail];
1122            for (i, sub) in items.iter().enumerate() {
1123                let nested = emit_nested_subpattern(
1124                    fc,
1125                    |fc| {
1126                        fc.emit_op(EXTRACT_TUPLE_ITEM);
1127                        fc.emit_u8(i as u8);
1128                    },
1129                    sub,
1130                )?;
1131                all_patches.extend(nested);
1132            }
1133            Ok(all_patches)
1134        }
1135        MirPattern::Ctor {
1136            ctor: MirCtor::Builtin(bc),
1137            bindings,
1138            ..
1139        } => {
1140            // Built-in wrapper variants (Result.Ok / Result.Err /
1141            // Option.Some). Option.None is the nullary case —
1142            // dispatched via the NONE constant compare.
1143            match bc {
1144                BuiltinCtor::ResultOk | BuiltinCtor::ResultErr | BuiltinCtor::OptionSome => {
1145                    let kind: u8 = match bc {
1146                        BuiltinCtor::ResultOk => 0,
1147                        BuiltinCtor::ResultErr => 1,
1148                        BuiltinCtor::OptionSome => 2,
1149                        BuiltinCtor::OptionNone => unreachable!(),
1150                    };
1151                    fc.emit_op(MATCH_UNWRAP);
1152                    fc.emit_u8(kind);
1153                    let patch = fc.offset();
1154                    fc.emit_i16(0);
1155                    // MATCH_UNWRAP replaces TOS with the inner
1156                    // value; the binding (when present) takes a
1157                    // DUP + store-or-pop — same shape the HIR
1158                    // walker uses (wildcard `_` collapses to no
1159                    // emit at all, mirroring HIR's `dup_and_bind`
1160                    // on `_`).
1161                    if let Some(b) = bindings.first() {
1162                        emit_dup_and_bind(fc, *b);
1163                    }
1164                    Ok(vec![patch])
1165                }
1166                BuiltinCtor::OptionNone => {
1167                    // Nullary: DUP + LOAD_CONST NONE + EQ +
1168                    // JUMP_IF_FALSE. No bindings to extract.
1169                    fc.emit_op(DUP);
1170                    let none_const = fc.add_constant(NanValue::NONE);
1171                    fc.emit_op(LOAD_CONST);
1172                    fc.emit_u16(none_const);
1173                    fc.emit_op(EQ);
1174                    let patch = fc.emit_jump(JUMP_IF_FALSE);
1175                    Ok(vec![patch])
1176                }
1177            }
1178        }
1179    }
1180}
1181
1182/// Phase 6 wave 2 — try the MATCH_DISPATCH_CONST table
1183/// fast-path. Returns `Ok(Some(()))` when the table was emitted
1184/// (caller skips the linear emit), `Ok(None)` when the arm
1185/// shape doesn't fit the fast-path (caller proceeds with linear
1186/// emit). Mirrors HIR's `emit_match_dispatch_const`.
1187///
1188/// Fast-path requirements (mirrors HIR's classifier):
1189/// - At least one dispatchable arm (otherwise the table is
1190///   pointless).
1191/// - Every non-last arm: pattern is `Literal(Int | Bool | Unit)`
1192///   (the bit-comparable subset) with a body that itself
1193///   reduces to a `Literal` whose `NanValue::bits()` we can
1194///   inline as the result.
1195/// - The last arm is the default: `Wildcard` / `Bind`. We do
1196///   not require its body to be a literal — the opcode pushes
1197///   the subject back on miss and falls through to the default
1198///   arm body, which we compile normally.
1199fn try_emit_match_dispatch_const(
1200    fc: &mut FnCompiler<'_>,
1201    subject: &Spanned<MirExpr>,
1202    arms: &[crate::ir::mir::MirMatchArm],
1203) -> Result<Option<()>, MirVmUnsupported> {
1204    if arms.len() < 2 {
1205        return Ok(None);
1206    }
1207    let last_idx = arms.len() - 1;
1208    let default_arm = &arms[last_idx];
1209    let default_local = match &default_arm.pattern {
1210        MirPattern::Wildcard => None,
1211        MirPattern::Bind(local, _name) => Some(*local),
1212        _ => return Ok(None),
1213    };
1214
1215    // Classify all non-default arms — every one must be a
1216    // const-dispatchable literal pattern with a const literal
1217    // body. Anything else aborts the fast-path.
1218    let mut entries: Vec<(u8, u64, u64)> = Vec::with_capacity(last_idx);
1219    for arm in &arms[..last_idx] {
1220        let pattern_lit = match &arm.pattern {
1221            MirPattern::Literal(lit) => lit,
1222            _ => return Ok(None),
1223        };
1224        let body_lit = match &arm.body.node {
1225            MirExpr::Literal(spanned_lit) => &spanned_lit.node,
1226            _ => return Ok(None),
1227        };
1228        // Strings (and big-ints) would need arena-side interning to compare;
1229        // keep this wave focused on bit-equal dispatch (Int / Bool / Unit /
1230        // Float-as-bits). Non-bit-comparable literals abort the fast-path here —
1231        // BEFORE `literal_dispatch_bits`, whose bits are only meaningful for
1232        // dispatchable literals (an arena-backed value's bits are an index).
1233        if !(pattern_is_dispatchable_bits(pattern_lit) && body_is_dispatchable_bits(body_lit)) {
1234            return Ok(None);
1235        }
1236        let expected = literal_dispatch_bits(fc, pattern_lit);
1237        let result = literal_dispatch_bits(fc, body_lit);
1238        entries.push((0u8, expected, result)); // DISPATCH_KIND_EXACT = 0
1239    }
1240
1241    if entries.is_empty() {
1242        return Ok(None);
1243    }
1244
1245    // ── Emit ────────────────────────────────────────────────
1246    compile_mir_expr(fc, subject)?;
1247
1248    fc.emit_op(MATCH_DISPATCH_CONST);
1249    fc.emit_u8(entries.len() as u8);
1250    let default_offset_patch = fc.offset();
1251    fc.emit_i16(0); // default_offset — patched after the table
1252
1253    for (kind, expected, result) in &entries {
1254        fc.emit_u8(*kind);
1255        fc.emit_u64(*expected);
1256        fc.emit_u64(*result);
1257    }
1258    let table_end = fc.offset();
1259
1260    // On hit: opcode pushes the result and ip lands right after
1261    // the table — emit a JUMP to skip past the default arm body.
1262    let hit_skip = fc.emit_jump(JUMP);
1263
1264    // Default arm starts here. Default offset is relative to
1265    // `table_end` (the opcode handler adds `default_offset` to
1266    // ip-after-table-end).
1267    let default_start = fc.offset();
1268    let default_rel = (default_start as isize - table_end as isize) as i16;
1269    let bytes = (default_rel as u16).to_be_bytes();
1270    fc.code_mut()[default_offset_patch] = bytes[0];
1271    fc.code_mut()[default_offset_patch + 1] = bytes[1];
1272
1273    // Default arm body — subject was popped+repushed by the
1274    // opcode on miss. Bind it if the pattern is `Bind(local)`,
1275    // then drop it via POP before the body.
1276    if let Some(local) = default_local {
1277        emit_dup_and_bind(fc, local);
1278    }
1279    fc.emit_op(POP);
1280    compile_mir_expr(fc, &default_arm.body)?;
1281
1282    // Patch the hit-skip JUMP to land after the default body.
1283    fc.patch_jump(hit_skip);
1284    Ok(Some(()))
1285}
1286
1287/// `true` when a literal's runtime bits are dispatch-comparable
1288/// (Int / Bool / Unit / Float). Strings need a different
1289/// dispatch kind (interning) so they abort the fast-path.
1290///
1291/// An `Int` only qualifies when it fits the 45-bit inline NaN-box: the table
1292/// compares raw `NanValue` bits, but an out-of-range int's bits hold an *arena
1293/// index*, not its value (and a compile-time index would be meaningless at
1294/// run time anyway). Such ints abort the fast-path and route through the
1295/// value-aware `MATCH_INT_LITERAL` linear path instead.
1296fn pattern_is_dispatchable_bits(lit: &Literal) -> bool {
1297    match lit {
1298        Literal::Int(i) => is_inline_int(*i),
1299        Literal::Bool(_) | Literal::Unit | Literal::Float(_) => true,
1300        // A big-int (like a string, and like an out-of-inline-range int) is
1301        // arena-backed: its `NanValue` bits hold an arena index, not the value,
1302        // so it can never key the raw-bits dispatch table. Route it through the
1303        // value-aware `DUP + LOAD_CONST + EQ` path instead.
1304        Literal::Str(_) | Literal::BigInt(_) => false,
1305    }
1306}
1307
1308/// `true` when `i` fits the inline NaN-box int range, so its `NanValue::bits()`
1309/// carry the value itself rather than an arena index.
1310#[inline]
1311fn is_inline_int(i: i64) -> bool {
1312    (crate::nan_value::INT_INLINE_MIN..=crate::nan_value::INT_INLINE_MAX).contains(&i)
1313}
1314
1315fn body_is_dispatchable_bits(lit: &Literal) -> bool {
1316    // String *bodies* could in principle be supported by
1317    // interning + inline result bits, but HIR's fast-path also
1318    // skips them. Mirror that.
1319    pattern_is_dispatchable_bits(lit)
1320}
1321
1322fn literal_dispatch_bits(fc: &mut FnCompiler<'_>, lit: &Literal) -> u64 {
1323    let nv = match lit {
1324        Literal::Int(i) => NanValue::new_int(*i, fc.arena),
1325        Literal::Float(f) => NanValue::new_float(*f),
1326        Literal::Bool(b) => NanValue::new_bool(*b),
1327        Literal::Unit => NanValue::UNIT,
1328        Literal::Str(s) => NanValue::new_string_value(s, fc.arena),
1329        // Unreachable: `pattern_is_dispatchable_bits` returns `false` for a
1330        // big-int, so it never enters the bit-dispatch table whose keys this
1331        // function computes (its arena-index bits would be a meaningless key).
1332        Literal::BigInt(_) => {
1333            unreachable!("BigInt is not bit-dispatchable; excluded by pattern_is_dispatchable_bits")
1334        }
1335    };
1336    nv.bits()
1337}
1338
1339/// Last-arm exhaustive binding extraction. The pattern is
1340/// guaranteed to match (preceding arms exhausted everything
1341/// else) so we skip the match-check opcode and just bind
1342/// whatever the pattern names. Mirror of HIR's last-arm logic
1343/// in `compile_match`.
1344fn emit_last_arm_bindings(
1345    fc: &mut FnCompiler<'_>,
1346    pattern: &MirPattern,
1347) -> Result<(), MirVmUnsupported> {
1348    match pattern {
1349        MirPattern::Wildcard | MirPattern::Literal(_) | MirPattern::EmptyList => Ok(()),
1350        MirPattern::Bind(local, _name) => {
1351            emit_dup_and_bind(fc, *local);
1352            Ok(())
1353        }
1354        MirPattern::Cons { head, tail, .. } => {
1355            fc.emit_op(DUP);
1356            fc.emit_op(LIST_HEAD_TAIL);
1357            emit_store_or_pop(fc, *head);
1358            emit_store_or_pop(fc, *tail);
1359            Ok(())
1360        }
1361        MirPattern::Ctor {
1362            ctor: MirCtor::User(_),
1363            bindings,
1364            ..
1365        } => {
1366            for (i, b) in bindings.iter().enumerate() {
1367                fc.emit_op(EXTRACT_FIELD);
1368                fc.emit_u8(i as u8);
1369                emit_store_or_pop(fc, *b);
1370            }
1371            Ok(())
1372        }
1373        MirPattern::Ctor {
1374            ctor: MirCtor::Builtin(bc),
1375            bindings,
1376            ..
1377        } => {
1378            if let Some(b) = bindings.first() {
1379                let kind: u8 = match bc {
1380                    BuiltinCtor::ResultOk => 0,
1381                    BuiltinCtor::ResultErr => 1,
1382                    BuiltinCtor::OptionSome => 2,
1383                    BuiltinCtor::OptionNone => {
1384                        // Option.None has no bindings — nothing
1385                        // to extract.
1386                        return Ok(());
1387                    }
1388                };
1389                fc.emit_op(MATCH_UNWRAP);
1390                fc.emit_u8(kind);
1391                fc.emit_i16(0); // no-fail (shape known)
1392                emit_dup_and_bind(fc, *b);
1393            }
1394            Ok(())
1395        }
1396        MirPattern::Tuple(items) => {
1397            // Last-arm Tuple: shape known — for each item emit
1398            // EXTRACT_TUPLE_ITEM then recurse into the subpattern
1399            // as if it were itself a last-arm pattern (the outer
1400            // exhaustiveness propagates inward — every nested
1401            // subpattern is also guaranteed to match).
1402            for (i, sub) in items.iter().enumerate() {
1403                fc.emit_op(EXTRACT_TUPLE_ITEM);
1404                fc.emit_u8(i as u8);
1405                emit_last_arm_bindings(fc, sub)?;
1406                fc.emit_op(POP);
1407            }
1408            Ok(())
1409        }
1410    }
1411}
1412
1413/// Compile a subpattern that operates on an extracted value,
1414/// with proper cleanup if the inner pattern fails. Mirrors HIR's
1415/// `compile_extracted_subpattern`:
1416///
1417///   emit_subject(fc);             // e.g. EXTRACT_TUPLE_ITEM i
1418///   <subpattern emit>             // fail_patches
1419///   POP                           // drop subpattern subject on success
1420///   if any fail patches:
1421///     JUMP success_skip
1422///     cleanup: POP + JUMP outer_fail
1423///     patch fail_patches → cleanup
1424///     patch success_skip → here
1425///     return vec![outer_fail]
1426///   else:
1427///     return empty Vec
1428///
1429/// The outer-fail patch lets the surrounding match arm jump to
1430/// the next arm if any nested subpattern fails, without
1431/// leaking the cleanup state.
1432fn emit_nested_subpattern<F>(
1433    fc: &mut FnCompiler<'_>,
1434    emit_subject: F,
1435    pattern: &MirPattern,
1436) -> Result<Vec<usize>, MirVmUnsupported>
1437where
1438    F: FnOnce(&mut FnCompiler<'_>),
1439{
1440    emit_subject(fc);
1441    let inner_fail_patches = emit_pattern_check(fc, pattern)?;
1442    fc.emit_op(POP);
1443
1444    if inner_fail_patches.is_empty() {
1445        return Ok(Vec::new());
1446    }
1447
1448    let success_skip = fc.emit_jump(JUMP);
1449    let cleanup_target = fc.offset();
1450    for patch in inner_fail_patches {
1451        fc.patch_jump_to(patch, cleanup_target);
1452    }
1453    fc.emit_op(POP);
1454    let outer_fail = fc.emit_jump(JUMP);
1455    fc.patch_jump(success_skip);
1456    Ok(vec![outer_fail])
1457}
1458
1459/// Extract `(slot, last_use)` if `expr` is a bare local read.
1460fn mir_local_slot_last_use(expr: &MirExpr) -> Option<(u32, bool)> {
1461    match expr {
1462        MirExpr::Local(l) => Some((l.node.slot.0, l.node.last_use)),
1463        _ => None,
1464    }
1465}
1466
1467/// Recognize the two fused vector compound shapes the HIR walker emits
1468/// as single opcodes, and emit the same opcode from MIR so the
1469/// VM-default path keeps the in-place / no-`Option`-alloc fast path:
1470///
1471/// - `Option.withDefault(Vector.set(v, i, x), v)` (same `v`) →
1472///   `VECTOR_SET_OR_KEEP`. `owned` mirrors the HIR rule: last use of the
1473///   inner vector slot AND the slot is not alias-prone.
1474/// - `Option.withDefault(Vector.get(v, i), <literal>)` → `VECTOR_GET_OR`
1475///   with the literal default inlined as a constant.
1476///
1477/// Returns `Ok(true)` when it emitted a fused op; `Ok(false)` leaves the
1478/// generic `UNWRAP_OR` path to the caller.
1479fn try_emit_vector_compound(
1480    fc: &mut FnCompiler<'_>,
1481    args: &[Spanned<MirExpr>],
1482) -> Result<bool, MirVmUnsupported> {
1483    if args.len() != 2 {
1484        return Ok(false);
1485    }
1486    let MirExpr::Call(inner_call) = &args[0].node else {
1487        return Ok(false);
1488    };
1489    let MirCall {
1490        callee: MirCallee::Builtin(inner_id),
1491        args: inner_args,
1492    } = &inner_call.node
1493    else {
1494        return Ok(false);
1495    };
1496    let inner_name = fc
1497        .mir_program
1498        .map(|p| p.builtin_name(*inner_id))
1499        .unwrap_or("");
1500    match lookup_vm_builtin(inner_name) {
1501        Some(VmBuiltin::VectorSet) if inner_args.len() == 3 => {
1502            // The default must be the same vector the set targets —
1503            // compared by slot, since the two reads carry different
1504            // last-use bits and a structural compare would miss it.
1505            let vec = mir_local_slot_last_use(&inner_args[0].node);
1506            let def = mir_local_slot_last_use(&args[1].node);
1507            let (Some((vec_slot, vec_last_use)), Some((def_slot, def_last_use))) = (vec, def)
1508            else {
1509                return Ok(false);
1510            };
1511            if vec_slot != def_slot {
1512                return Ok(false);
1513            }
1514            // Self-keep fusion ownership collapse. The inner `Vector.set`
1515            // and the `withDefault` default read the SAME slot
1516            // (vec_slot == def_slot), and the fused `VECTOR_SET_OR_KEEP`
1517            // returns exactly one of those two handles — so the slot is
1518            // dead after the op iff EITHER occurrence is its last use.
1519            // `last_use` annotates only the textually-last read (the
1520            // default, arg[1]), leaving the inner read last_use=false; OR
1521            // the two bits so a linearly-threaded vector takes the
1522            // in-place path. Still gated on `!is_aliased_slot`: the
1523            // owned-param refinement (`own_param.rs`) is what proves a
1524            // threaded `Vector`/`Map` param non-aliased, and only then
1525            // does the in-place set fire — keeping the guard load-bearing.
1526            let owned = (vec_last_use || def_last_use) && !fc.is_aliased_slot(vec_slot as u16);
1527            compile_mir_expr(fc, &inner_args[0])?;
1528            compile_mir_expr(fc, &inner_args[1])?;
1529            compile_mir_expr(fc, &inner_args[2])?;
1530            fc.emit_op(VECTOR_SET_OR_KEEP);
1531            fc.emit_u8(u8::from(owned));
1532            Ok(true)
1533        }
1534        Some(VmBuiltin::VectorGet) if inner_args.len() == 2 => {
1535            let MirExpr::Literal(lit) = &args[1].node else {
1536                return Ok(false);
1537            };
1538            compile_mir_expr(fc, &inner_args[0])?;
1539            compile_mir_expr(fc, &inner_args[1])?;
1540            let default_value = fc.nan_literal(&lit.node);
1541            let const_idx = fc.add_constant(default_value);
1542            fc.emit_op(VECTOR_GET_OR);
1543            fc.emit_u16(const_idx);
1544            Ok(true)
1545        }
1546        _ => Ok(false),
1547    }
1548}
1549
1550/// Tail-call owned mask: bit `i` is set when arg `i` carries a last-use
1551/// read of slot `i` — i.e. param slot `i` is threaded straight back into
1552/// the callee's slot `i` and is dead afterwards, so it can be moved
1553/// rather than copied. Keyed on the arg-index == slot-index convention
1554/// that only holds for the slot-aligned tail-call args. Caps at 8 args
1555/// (mask is u8).
1556///
1557/// The alias-prone slot guard is load-bearing: the owned path empties
1558/// the arena slot in place (`take_map_value` / vector take), so marking
1559/// an aliased `Vector`/`Map` slot owned would mutate a binding the
1560/// caller still holds.
1561fn compute_owned_mask(args: &[Spanned<MirExpr>], fc: &FnCompiler<'_>) -> u8 {
1562    let mut mask = 0u8;
1563    for (i, arg) in args.iter().enumerate().take(8) {
1564        if contains_last_use_slot_mir(&arg.node, i as u32) && !fc.is_aliased_slot(i as u16) {
1565            mask |= 1 << i;
1566        }
1567    }
1568    mask
1569}
1570
1571/// Builtin owned mask: bit `i` is set when arg `i` is itself a last-use
1572/// read of a non-aliased slot (uniquely owned, safe to take in place) —
1573/// keyed on the arg's OWN slot, NOT the arg index. A builtin's arguments
1574/// do not line up with slot indices (`Map.set(m, k, v)` reads `m` from
1575/// whatever slot it was bound to), so the tail-call mask above silently
1576/// misses them. The runtime honors only bit 0 and only for
1577/// `Map.set` / `Vector.set` (`invoke_builtin_with_owned`), so a broader
1578/// mask is harmless for every other builtin. The `!is_aliased_slot`
1579/// guard stays load-bearing — `own_param` is what makes a linearly
1580/// threaded `Vector`/`Map` param non-aliased, and only then does the
1581/// in-place take fire.
1582fn compute_builtin_owned_mask(args: &[Spanned<MirExpr>], fc: &FnCompiler<'_>) -> u8 {
1583    let mut mask = 0u8;
1584    for (i, arg) in args.iter().enumerate().take(8) {
1585        if let Some((slot, last_use)) = mir_local_slot_last_use(&arg.node)
1586            && last_use
1587            && !fc.is_aliased_slot(slot as u16)
1588        {
1589            mask |= 1 << i;
1590        }
1591    }
1592    mask
1593}
1594
1595/// Recursive search: does `expr` contain a `MirExpr::Local`
1596/// reading slot `target` with `last_use = true`? Mirror of
1597/// HIR's `contains_last_use_slot`.
1598fn contains_last_use_slot_mir(expr: &MirExpr, target: u32) -> bool {
1599    match expr {
1600        MirExpr::Local(spanned_local) => {
1601            spanned_local.node.slot.0 == target && spanned_local.node.last_use
1602        }
1603        MirExpr::Call(c) => c
1604            .node
1605            .args
1606            .iter()
1607            .any(|a| contains_last_use_slot_mir(&a.node, target)),
1608        MirExpr::TailCall(t) => t
1609            .node
1610            .args
1611            .iter()
1612            .any(|a| contains_last_use_slot_mir(&a.node, target)),
1613        MirExpr::BinOp(b) => {
1614            contains_last_use_slot_mir(&b.node.lhs.node, target)
1615                || contains_last_use_slot_mir(&b.node.rhs.node, target)
1616        }
1617        MirExpr::Neg(inner) => contains_last_use_slot_mir(&inner.node, target),
1618        MirExpr::Project(p) => contains_last_use_slot_mir(&p.node.base.node, target),
1619        MirExpr::Try(inner) => contains_last_use_slot_mir(&inner.node, target),
1620        // ETAP-2 boundaries never appear on the VM path (the rewrite is
1621        // Rust-codegen-only); recurse transparently for completeness.
1622        MirExpr::Box(inner) | MirExpr::Unbox(inner) => {
1623            contains_last_use_slot_mir(&inner.node, target)
1624        }
1625        MirExpr::Construct(c) => c
1626            .node
1627            .args
1628            .iter()
1629            .any(|a| contains_last_use_slot_mir(&a.node, target)),
1630        MirExpr::RecordCreate(rc) => rc
1631            .node
1632            .fields
1633            .iter()
1634            .any(|f| contains_last_use_slot_mir(&f.value.node, target)),
1635        MirExpr::RecordUpdate(ru) => {
1636            contains_last_use_slot_mir(&ru.node.base.node, target)
1637                || ru
1638                    .node
1639                    .updates
1640                    .iter()
1641                    .any(|f| contains_last_use_slot_mir(&f.value.node, target))
1642        }
1643        MirExpr::List(items) | MirExpr::Tuple(items) => items
1644            .iter()
1645            .any(|i| contains_last_use_slot_mir(&i.node, target)),
1646        MirExpr::MapLiteral(entries) => entries.iter().any(|(k, v)| {
1647            contains_last_use_slot_mir(&k.node, target)
1648                || contains_last_use_slot_mir(&v.node, target)
1649        }),
1650        MirExpr::IndependentProduct(ip) => ip
1651            .node
1652            .items
1653            .iter()
1654            .any(|i| contains_last_use_slot_mir(&i.node, target)),
1655        MirExpr::InterpolatedStr(parts) => parts.iter().any(|p| match p {
1656            MirStrPart::Expr(e) => contains_last_use_slot_mir(&e.node, target),
1657            MirStrPart::Literal(_) => false,
1658        }),
1659        // Match / IfThenElse / Let / Return: structural
1660        // recursion stays conservative — we only care about
1661        // the immediate arg-evaluation context for the
1662        // owned_mask, so deep recursion into match arms /
1663        // if branches / let bodies isn't useful.
1664        MirExpr::Match(_)
1665        | MirExpr::IfThenElse(_)
1666        | MirExpr::Let(_)
1667        | MirExpr::Return(_)
1668        // FnValue is a bare symbol reference — no slot read.
1669        | MirExpr::FnValue(_)
1670        | MirExpr::Literal(_) => false,
1671    }
1672}
1673
1674/// Linear-search lookup `name → VmBuiltin`. Returns `None` for
1675/// names not in the builtin table — the caller drops back to HIR
1676/// via `MirVmUnsupported::UnsupportedCallee`. The table is small
1677/// (~60 entries) so linear scan is fine; a future Phase 6 can
1678/// cache it.
1679fn lookup_vm_builtin(name: &str) -> Option<VmBuiltin> {
1680    VmBuiltin::ALL.iter().copied().find(|b| b.name() == name)
1681}
1682
1683/// Helper: emit a single ctor arg, or `LOAD_UNIT` when the ctor
1684/// arg is absent (defensive — built-in Wrap-shaped ctors always
1685/// take exactly one arg in well-typed Aver, but the lowerer
1686/// only enforces that at the type level).
1687fn emit_constructor_arg(
1688    fc: &mut FnCompiler<'_>,
1689    arg: Option<&Spanned<MirExpr>>,
1690) -> Result<(), MirVmUnsupported> {
1691    match arg {
1692        Some(a) => compile_mir_expr(fc, a),
1693        None => {
1694            fc.emit_op(LOAD_UNIT);
1695            Ok(())
1696        }
1697    }
1698}
1699
1700fn emit_binop_typed(
1701    fc: &mut FnCompiler<'_>,
1702    op: crate::ast::BinOp,
1703    lhs_ty: Option<&crate::ast::Type>,
1704    rhs_ty: Option<&crate::ast::Type>,
1705) {
1706    use crate::ast::BinOp::*;
1707    use crate::ast::Type;
1708    let both_int = matches!((lhs_ty, rhs_ty), (Some(Type::Int), Some(Type::Int)));
1709    let both_float = matches!((lhs_ty, rhs_ty), (Some(Type::Float), Some(Type::Float)));
1710    let lt_op = if both_int {
1711        LT_INT
1712    } else if both_float {
1713        LT_FLOAT
1714    } else {
1715        LT
1716    };
1717    let gt_op = if both_int {
1718        GT_INT
1719    } else if both_float {
1720        GT_FLOAT
1721    } else {
1722        GT
1723    };
1724    let add_op = if both_int {
1725        ADD_INT
1726    } else if both_float {
1727        ADD_FLOAT
1728    } else {
1729        ADD
1730    };
1731    let sub_op = if both_int {
1732        SUB_INT
1733    } else if both_float {
1734        SUB_FLOAT
1735    } else {
1736        SUB
1737    };
1738    let mul_op = if both_int {
1739        MUL_INT
1740    } else if both_float {
1741        MUL_FLOAT
1742    } else {
1743        MUL
1744    };
1745    // No DIV_INT — integer division traps on `b == 0` and the
1746    // generic `arith_div` already does that branch + propagates
1747    // a typed runtime error.
1748    let div_op = if both_float { DIV_FLOAT } else { DIV };
1749    match op {
1750        Add => fc.emit_op(add_op),
1751        Sub => fc.emit_op(sub_op),
1752        Mul => fc.emit_op(mul_op),
1753        Div => fc.emit_op(div_op),
1754        Eq => fc.emit_op(if both_int { EQ_INT } else { EQ }),
1755        Lt => fc.emit_op(lt_op),
1756        Gt => fc.emit_op(gt_op),
1757        // `Neq` / `Lte` / `Gte` invert the corresponding
1758        // comparison (same composition HIR uses).
1759        Neq => {
1760            fc.emit_op(if both_int { EQ_INT } else { EQ });
1761            fc.emit_op(NOT);
1762        }
1763        Lte => {
1764            fc.emit_op(gt_op);
1765            fc.emit_op(NOT);
1766        }
1767        Gte => {
1768            fc.emit_op(lt_op);
1769            fc.emit_op(NOT);
1770        }
1771    }
1772}