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        // ── Phase 4d: tail-call dispatch ────────────────────────
444        MirExpr::TailCall(spanned_tail) => {
445            let tc = &spanned_tail.node;
446            // Phase 6 wave 4: derive owned_mask from args' last-
447            // use flags BEFORE compiling them (compute looks at
448            // the MirExpr tree, not stack state).
449            let owned_mask = compute_owned_mask(&tc.args, fc);
450            for arg in &tc.args {
451                compile_mir_expr(fc, arg)?;
452            }
453            let target_name = fc.canonical_fn_name(tc.target)?;
454            if target_name == fc.name() {
455                fc.emit_op(TAIL_CALL_SELF);
456                fc.emit_u8(tc.args.len() as u8);
457                fc.emit_u8(owned_mask);
458            } else {
459                let vm_fn_id = fc.resolve_fn_id_by_name(&target_name).ok_or_else(|| {
460                    MirVmUnsupported::InnerError(CompileError {
461                        msg: format!(
462                            "MIR-VM: unresolved tail-call target `{target_name}` \
463                             (FnId={:?})",
464                            tc.target
465                        ),
466                    })
467                })?;
468                fc.emit_op(TAIL_CALL_KNOWN);
469                fc.emit_u16(vm_fn_id as u16);
470                fc.emit_u8(tc.args.len() as u8);
471                fc.emit_u8(owned_mask);
472            }
473            Ok(())
474        }
475
476        // ── Phase 4f: record + list + tuple builders ────────────
477        MirExpr::List(items) => {
478            if items.is_empty() {
479                fc.emit_op(LIST_NIL);
480                return Ok(());
481            }
482            for item in items {
483                compile_mir_expr(fc, item)?;
484            }
485            fc.emit_op(LIST_NEW);
486            fc.emit_u8(items.len() as u8);
487            Ok(())
488        }
489        MirExpr::Tuple(items) => {
490            for item in items {
491                compile_mir_expr(fc, item)?;
492            }
493            fc.emit_op(TUPLE_NEW);
494            fc.emit_u8(items.len() as u8);
495            Ok(())
496        }
497        MirExpr::RecordCreate(spanned_rc) => {
498            let rc = &spanned_rc.node;
499            // Resolve to the arena type id + field order. User records
500            // carry a `TypeId` → canonical name; built-in records
501            // (`HttpResponse`, …) carry no `TypeId` and ride their
502            // canonical `type_name` directly (the arena registers
503            // built-in record types by that name). The arena's field
504            // order is declaration order, which is what RECORD_NEW
505            // expects on the stack.
506            let qualified_type_name = match rc.type_id {
507                Some(id) => fc.canonical_type_name(id)?,
508                None => rc.type_name.clone(),
509            };
510            let arena_type_id = fc.resolve_type_id(&qualified_type_name).ok_or_else(|| {
511                MirVmUnsupported::InnerError(CompileError {
512                    msg: format!(
513                        "MIR-VM: unknown arena type `{qualified_type_name}` for \
514                         RecordCreate (TypeId={:?})",
515                        rc.type_id
516                    ),
517                })
518            })?;
519            let field_names = fc.arena.get_field_names(arena_type_id).to_vec();
520            // Push fields in declared order.
521            for expected_name in &field_names {
522                let field = rc.fields.iter().find(|f| f.name == *expected_name).ok_or_else(
523                    || {
524                        MirVmUnsupported::InnerError(CompileError {
525                            msg: format!(
526                                "MIR-VM: missing field `{expected_name}` in record `{qualified_type_name}`"
527                            ),
528                        })
529                    },
530                )?;
531                compile_mir_expr(fc, &field.value)?;
532            }
533            fc.emit_op(RECORD_NEW);
534            fc.emit_u16(arena_type_id as u16);
535            fc.emit_u8(field_names.len() as u8);
536            Ok(())
537        }
538        MirExpr::RecordUpdate(spanned_ru) => {
539            let ru = &spanned_ru.node;
540            let qualified_type_name = match ru.type_id {
541                Some(id) => fc.canonical_type_name(id)?,
542                None => ru.type_name.clone(),
543            };
544            let arena_type_id = fc.resolve_type_id(&qualified_type_name).ok_or_else(|| {
545                MirVmUnsupported::InnerError(CompileError {
546                    msg: format!(
547                        "MIR-VM: unknown arena type `{qualified_type_name}` for \
548                         RecordUpdate (TypeId={:?})",
549                        ru.type_id
550                    ),
551                })
552            })?;
553            let field_names = fc.arena.get_field_names(arena_type_id).to_vec();
554            let mut updated_indices = Vec::with_capacity(ru.updates.len());
555
556            compile_mir_expr(fc, &ru.base)?;
557
558            for (field_idx, field_name) in field_names.iter().enumerate() {
559                if let Some(field) = ru.updates.iter().find(|f| f.name == *field_name) {
560                    compile_mir_expr(fc, &field.value)?;
561                    updated_indices.push(field_idx as u8);
562                }
563            }
564            fc.emit_op(RECORD_UPDATE);
565            fc.emit_u16(arena_type_id as u16);
566            fc.emit_u8(updated_indices.len() as u8);
567            for idx in updated_indices {
568                fc.emit_u8(idx);
569            }
570            Ok(())
571        }
572
573        // ── Phase 4g-1: match with Wildcard + Literal(Int) arms ──
574        // The HIR walker's `compile_match` is 756 lines and
575        // includes fast-paths (MATCH_DISPATCH_CONST, bool-branch
576        // optimization) we don't replicate here. This sub-PR
577        // handles the smallest reviewable subset: arm patterns
578        // restricted to `Wildcard` and `Literal(Int)`. Any other
579        // pattern variant in any arm falls back to HIR.
580        //
581        // Emit shape (linear fallback, mirrors HIR's tail of
582        // `compile_match`):
583        //   <subject>
584        //   per arm (except last):
585        //     [MATCH_INT_LITERAL imm fail]  // skipped for Wildcard
586        //     POP
587        //     <body>
588        //     JUMP end
589        //     fail: <next arm>
590        //   last arm: skip pattern check entirely (exhaustive),
591        //             POP, <body>
592        //   end:
593        MirExpr::IfThenElse(spanned_ite) => {
594            // Phase 6 wave 9: direct conditional shape.
595            // Emit:
596            //   compile_mir_expr(cond)
597            //   JUMP_IF_FALSE → else_start
598            //   compile_mir_expr(then_branch)
599            //   JUMP → end
600            //   else_start: compile_mir_expr(else_branch)
601            //   end:
602            let ite = &spanned_ite.node;
603            compile_mir_expr(fc, &ite.cond)?;
604            let else_patch = fc.emit_jump(JUMP_IF_FALSE);
605            compile_mir_expr(fc, &ite.then_branch)?;
606            let end_patch = fc.emit_jump(JUMP);
607            fc.patch_jump(else_patch);
608            compile_mir_expr(fc, &ite.else_branch)?;
609            fc.patch_jump(end_patch);
610            Ok(())
611        }
612        MirExpr::Match(spanned_match) => {
613            let m = &spanned_match.node;
614            // A match with no arms can't produce a value — type-check
615            // rejects it as non-exhaustive, so reaching codegen with zero
616            // arms is an invariant violation, not user-reachable on valid
617            // input. Bail with a compile error rather than underflowing
618            // `arms.len() - 1` below (the `last_idx` computation). Found by
619            // the verify-runner fuzzer on a mutated program that slips a
620            // zero-arm match past the front-end.
621            if m.arms.is_empty() {
622                return Err(CompileError {
623                    msg: "match expression has no arms (non-exhaustive match reached codegen)"
624                        .to_string(),
625                }
626                .into());
627            }
628            // Phase 6 wave 2 — try the MATCH_DISPATCH_CONST table
629            // fast-path before falling back to the linear arm-by-
630            // arm emit. When every non-last arm has a literal
631            // pattern + literal body and the last arm is a
632            // wildcard/bind default, HIR's `compile_match` emits
633            // a single jump-table dispatch (MATCH_DISPATCH_CONST)
634            // with inline (kind, expected_bits, result_bits)
635            // entries — one VM op for the whole match. Mirror it
636            // here so MIR matches HIR byte-for-byte on the
637            // common shape.
638            if try_emit_match_dispatch_const(fc, &m.subject, &m.arms)?.is_some() {
639                return Ok(());
640            }
641            compile_mir_expr(fc, &m.subject)?;
642
643            let mut end_jumps: Vec<usize> = Vec::new();
644            let last_idx = m.arms.len() - 1;
645            for (i, arm) in m.arms.iter().enumerate() {
646                let is_last = i == last_idx;
647                let fail_patches: Vec<usize> = if is_last {
648                    // Last arm — exhaustive, no pattern check.
649                    // Bindings still need extracting (value on
650                    // stack, shape known from preceding arm
651                    // failures).
652                    emit_last_arm_bindings(fc, &arm.pattern)?;
653                    Vec::new()
654                } else {
655                    emit_pattern_check(fc, &arm.pattern)?
656                };
657                fc.emit_op(POP);
658                compile_mir_expr(fc, &arm.body)?;
659                if !is_last {
660                    end_jumps.push(fc.emit_jump(JUMP));
661                    let next_arm_start = fc.offset();
662                    for patch in fail_patches {
663                        fc.patch_jump_to(patch, next_arm_start);
664                    }
665                }
666            }
667            for patch in end_jumps {
668                fc.patch_jump(patch);
669            }
670            Ok(())
671        }
672        // ── Phase 4h: remaining MirExpr variants ────────────────
673        MirExpr::MapLiteral(entries) => {
674            // Mirror HIR's `compile_map`: LOAD_CONST an empty
675            // `PersistentMap` from the arena, then per entry
676            // compile key + value and emit a `MapSet` call.
677            let empty_map = fc.arena.push_map(crate::nan_value::PersistentMap::new());
678            let nv = NanValue::new_map(empty_map);
679            let idx = fc.add_constant(nv);
680            fc.emit_op(LOAD_CONST);
681            fc.emit_u16(idx);
682            for (k, v) in entries {
683                compile_mir_expr(fc, k)?;
684                compile_mir_expr(fc, v)?;
685                let symbol_id = fc.symbols.intern_builtin(VmBuiltin::MapSet).map_err(|e| {
686                    MirVmUnsupported::InnerError(CompileError {
687                        msg: format!("MIR-VM: intern_builtin(MapSet) failed: {e:?}"),
688                    })
689                })?;
690                fc.emit_op(CALL_BUILTIN);
691                fc.emit_u32(symbol_id);
692                fc.emit_u8(3);
693            }
694            Ok(())
695        }
696        MirExpr::InterpolatedStr(parts) => {
697            // `interp_lower` upstream of MIR usually desugars
698            // these into buffer-build calls before MIR sees
699            // them, so this branch is rarely reached in
700            // practice — kept for completeness so the walker
701            // covers every `MirExpr` variant.
702            if parts.is_empty() {
703                let nv = NanValue::new_string_value("", fc.arena);
704                let cidx = fc.add_constant(nv);
705                fc.emit_op(LOAD_CONST);
706                fc.emit_u16(cidx);
707                return Ok(());
708            }
709            let mut first = true;
710            for part in parts {
711                match part {
712                    MirStrPart::Literal(s) => {
713                        let nv = NanValue::new_string_value(s, fc.arena);
714                        let cidx = fc.add_constant(nv);
715                        fc.emit_op(LOAD_CONST);
716                        fc.emit_u16(cidx);
717                    }
718                    MirStrPart::Expr(e) => {
719                        compile_mir_expr(fc, e)?;
720                        let empty_nv = NanValue::new_string_value("", fc.arena);
721                        let empty_const = fc.add_constant(empty_nv);
722                        fc.emit_op(LOAD_CONST);
723                        fc.emit_u16(empty_const);
724                        fc.emit_op(CONCAT);
725                    }
726                }
727                if !first {
728                    fc.emit_op(CONCAT);
729                }
730                first = false;
731            }
732            Ok(())
733        }
734        MirExpr::IndependentProduct(spanned_ip) => {
735            let ip = &spanned_ip.node;
736            // Mirror HIR's `compile_independent_product`. Each
737            // item is expected to be a Call (or Try-wrapped
738            // Call) so we push the callee as a value followed
739            // by its args; the dispatcher then emits CALL_PAR
740            // with per-branch arities. If any item shape isn't
741            // call-like, fall back to a sequential TUPLE_NEW
742            // (the same safety net HIR uses).
743            let call_ready = ip.items.iter().all(|item| {
744                let inner = match &item.node {
745                    MirExpr::Try(boxed) => &boxed.node,
746                    other => other,
747                };
748                matches!(inner, MirExpr::Call(_))
749            });
750            if !call_ready {
751                for item in &ip.items {
752                    compile_mir_expr(fc, item)?;
753                }
754                fc.emit_op(TUPLE_NEW);
755                fc.emit_u8(ip.items.len() as u8);
756                return Ok(());
757            }
758            let mut arg_counts: Vec<u8> = Vec::with_capacity(ip.items.len());
759            for item in &ip.items {
760                let inner_call = match &item.node {
761                    MirExpr::Try(boxed) => &boxed.node,
762                    other => other,
763                };
764                let MirExpr::Call(spanned_call) = inner_call else {
765                    unreachable!("call_ready preflight just confirmed");
766                };
767                let call = &spanned_call.node;
768                match &call.callee {
769                    MirCallee::Fn(fn_id) => {
770                        let name = fc.canonical_fn_name(*fn_id)?;
771                        let symbol_id = fc.symbols.find(&name).ok_or_else(|| {
772                            MirVmUnsupported::InnerError(CompileError {
773                                msg: format!("MIR-VM: missing VM symbol for fn `{name}`"),
774                            })
775                        })?;
776                        let idx = fc.add_constant(VmSymbolTable::symbol_ref(symbol_id));
777                        fc.emit_op(LOAD_CONST);
778                        fc.emit_u16(idx);
779                    }
780                    MirCallee::Builtin(id) => {
781                        let name = fc.mir_program.map(|p| p.builtin_name(*id)).unwrap_or("");
782                        let symbol_id = fc.symbols.find(name).ok_or_else(|| {
783                            MirVmUnsupported::InnerError(CompileError {
784                                msg: format!("MIR-VM: missing VM symbol for builtin `{name}`"),
785                            })
786                        })?;
787                        let idx = fc.add_constant(VmSymbolTable::symbol_ref(symbol_id));
788                        fc.emit_op(LOAD_CONST);
789                        fc.emit_u16(idx);
790                    }
791                    // A synthesis intrinsic can't appear as an
792                    // independent-product branch callee (intrinsics are
793                    // never first-class values); a first-class-fn-valued
794                    // IP branch is rare — both fall back conservatively.
795                    MirCallee::Intrinsic(_) | MirCallee::LocalSlot { .. } => {
796                        return Err(MirVmUnsupported::UnsupportedCallee);
797                    }
798                }
799                for arg in &call.args {
800                    compile_mir_expr(fc, arg)?;
801                }
802                arg_counts.push(call.args.len() as u8);
803            }
804            fc.emit_op(CALL_PAR);
805            fc.emit_u8(ip.items.len() as u8);
806            fc.emit_u8(if ip.unwrap_results { 1 } else { 0 });
807            for argc in arg_counts {
808                fc.emit_u8(argc);
809            }
810            Ok(())
811        }
812    }
813}
814
815/// Emit a MIR fn's body into the supplied `FnCompiler` and finish
816/// with `RETURN`. Caller has already constructed `fc` with the
817/// right arity / local_count / local_slots — same path the HIR
818/// compiler takes through `compile_fn_with_scope`.
819pub(super) fn compile_mir_fn_body(
820    fc: &mut FnCompiler<'_>,
821    mir_fn: &MirFn,
822) -> Result<(), MirVmUnsupported> {
823    compile_mir_expr(fc, &mir_fn.body)?;
824    fc.emit_op(RETURN);
825    Ok(())
826}
827
828/// Convenience: walk a `MirProgram` and report which fns the
829/// Phase 4 subset can handle vs which still need HIR fallback.
830/// Useful for parity tests + Phase 4 coverage tracking.
831pub fn classify_mir_program_coverage(mir: &MirProgram) -> MirVmCoverage {
832    let mut covered = 0u32;
833    let mut needs_hir_fallback = 0u32;
834    for mir_fn in mir.fns.values() {
835        if can_compile(&mir_fn.body) {
836            covered += 1;
837        } else {
838            needs_hir_fallback += 1;
839        }
840    }
841    MirVmCoverage {
842        covered,
843        needs_hir_fallback,
844    }
845}
846
847#[derive(Debug, Clone, Copy, Default)]
848pub struct MirVmCoverage {
849    pub covered: u32,
850    pub needs_hir_fallback: u32,
851}
852
853/// Whether the MIR walker can emit bytecode for `expr` without hitting
854/// an `MirVmUnsupported`. Used by `compile_top_level` to pre-check a
855/// batch of lowered top-level value expressions before committing to
856/// the MIR walk, so a mid-emit rejection can't leave half-written
857/// bytecode — it falls back to a clean alternative path instead.
858pub(super) fn mir_expr_compilable(expr: &Spanned<MirExpr>) -> bool {
859    can_compile(expr)
860}
861
862fn can_compile(expr: &Spanned<MirExpr>) -> bool {
863    match &expr.node {
864        MirExpr::Literal(_) => true,
865        MirExpr::Local(_) => true,
866        MirExpr::BinOp(b) => can_compile(&b.node.lhs) && can_compile(&b.node.rhs),
867        MirExpr::Neg(inner) => can_compile(inner),
868        MirExpr::Let(l) => can_compile(&l.node.value) && can_compile(&l.node.body),
869        MirExpr::Call(c) => {
870            let callee_ok = match &c.node.callee {
871                MirCallee::Fn(_) => true,
872                // Phase 6 wave 11: optimistic — the `BuiltinId →
873                // name` resolution lives behind `MirProgram` and
874                // `can_compile` is a standalone walker. The hot
875                // path in `compile_mir_expr` still surfaces
876                // `MirVmUnsupported::UnsupportedCallee` when the
877                // name isn't in `VmBuiltin::ALL`, and the caller
878                // drops back to HIR — so reporting `true` here at
879                // worst costs one rejected compilation attempt
880                // rather than silently mis-emitting.
881                MirCallee::Builtin(_) => true,
882                // Buffer-build / stringify intrinsics emit dedicated
883                // BUFFER_* / CONCAT opcodes — always compilable.
884                MirCallee::Intrinsic(_) => true,
885                // First-class fn value → CALL_VALUE dynamic dispatch.
886                MirCallee::LocalSlot { .. } => true,
887            };
888            callee_ok && c.node.args.iter().all(can_compile)
889        }
890        MirExpr::Return(inner) => can_compile(inner),
891        // Phase 6: fn-as-value. Optimistic, same rationale as the
892        // `Builtin` callee above — `compile_ident` resolves the
893        // symbol on the hot path and surfaces a real error (→ HIR
894        // fallback) if the name doesn't resolve.
895        MirExpr::FnValue(_) => true,
896        // Phase 4c additions:
897        MirExpr::Construct(c) => c.node.args.iter().all(can_compile),
898        MirExpr::Project(p) => can_compile(&p.node.base),
899        // Phase 4d additions:
900        MirExpr::Try(inner) => can_compile(inner),
901        MirExpr::TailCall(t) => t.node.args.iter().all(can_compile),
902        // Phase 4f additions:
903        MirExpr::List(items) => items.iter().all(can_compile),
904        MirExpr::Tuple(items) => items.iter().all(can_compile),
905        MirExpr::RecordCreate(rc) => rc.node.fields.iter().all(|f| can_compile(&f.value)),
906        MirExpr::RecordUpdate(ru) => {
907            can_compile(&ru.node.base) && ru.node.updates.iter().all(|f| can_compile(&f.value))
908        }
909        // Phase 4h additions:
910        MirExpr::MapLiteral(entries) => entries
911            .iter()
912            .all(|(k, v)| can_compile(k) && can_compile(v)),
913        MirExpr::InterpolatedStr(parts) => parts.iter().all(|p| match p {
914            MirStrPart::Literal(_) => true,
915            MirStrPart::Expr(e) => can_compile(e),
916        }),
917        MirExpr::IndependentProduct(ip) => ip.node.items.iter().all(can_compile),
918        // Phase 4i: all `MirPattern` variants supported (Tuple
919        // recurses into subpatterns).
920        // Phase 6 wave 9: direct conditional shape introduced
921        // by `bool_match_to_if`. Every backend (incl. VM)
922        // supports it natively as branch + branch.
923        MirExpr::IfThenElse(ite) => {
924            can_compile(&ite.node.cond)
925                && can_compile(&ite.node.then_branch)
926                && can_compile(&ite.node.else_branch)
927        }
928        MirExpr::Match(m) => {
929            can_compile(&m.node.subject)
930                && m.node
931                    .arms
932                    .iter()
933                    .all(|arm| pattern_supported(&arm.pattern) && can_compile(&arm.body))
934        }
935    }
936}
937
938/// Sentinel `LocalId` value used by the resolver for wildcard
939/// bindings (`_`) inside patterns. The HIR walker treats this
940/// as "no slot to write — POP the value off the stack instead
941/// of `STORE_LOCAL`". MIR carries the sentinel as the same
942/// `u32::from(u16::MAX)` so we can detect it here.
943const WILDCARD_SLOT_SENTINEL: u32 = u16::MAX as u32;
944
945/// Emit either `STORE_LOCAL slot` for a real binding or `POP`
946/// for the wildcard sentinel — mirroring HIR's
947/// `bind_top_to_local`.
948fn emit_store_or_pop(fc: &mut FnCompiler<'_>, local: LocalId) {
949    if local.0 == WILDCARD_SLOT_SENTINEL {
950        fc.emit_op(POP);
951    } else {
952        fc.emit_op(STORE_LOCAL);
953        fc.emit_u8(local.0 as u8);
954    }
955}
956
957/// DUP + (`STORE_LOCAL` or `POP`) — mirror of HIR's
958/// `dup_and_bind_top_to_local`. When the binding is the
959/// wildcard sentinel, DUP + POP is a no-op, so we emit nothing.
960fn emit_dup_and_bind(fc: &mut FnCompiler<'_>, local: LocalId) {
961    if local.0 == WILDCARD_SLOT_SENTINEL {
962        return;
963    }
964    fc.emit_op(DUP);
965    fc.emit_op(STORE_LOCAL);
966    fc.emit_u8(local.0 as u8);
967}
968
969/// Recursive predicate: every `MirPattern` variant is now
970/// walker-supported. Tuple subpatterns recurse so nested
971/// structural patterns work after Phase 4i landed
972/// `emit_nested_subpattern`.
973fn pattern_supported(p: &MirPattern) -> bool {
974    match p {
975        MirPattern::Wildcard
976        | MirPattern::Bind(_, _)
977        | MirPattern::Literal(_)
978        | MirPattern::EmptyList
979        | MirPattern::Cons { .. }
980        | MirPattern::Ctor { .. } => true,
981        MirPattern::Tuple(items) => items.iter().all(pattern_supported),
982    }
983}
984
985/// Emit the pattern check for a non-last arm. Returns the
986/// list of `fail_offset` patch positions the caller will fill
987/// in to point at the next arm's start. Empty `Vec` = pattern
988/// always matches (Wildcard / Bind).
989fn emit_pattern_check(
990    fc: &mut FnCompiler<'_>,
991    pattern: &MirPattern,
992) -> Result<Vec<usize>, MirVmUnsupported> {
993    match pattern {
994        MirPattern::Wildcard => Ok(Vec::new()),
995        MirPattern::Bind(local, _name) => {
996            // Always matches; DUP the value and bind. Mirror of
997            // HIR's `dup_and_bind_top_to_local` on a top-level
998            // ident pattern. Wildcard sentinel collapses the
999            // DUP+POP to no emit.
1000            emit_dup_and_bind(fc, *local);
1001            Ok(Vec::new())
1002        }
1003        MirPattern::Literal(Literal::Int(v)) => {
1004            fc.emit_op(MATCH_INT_LITERAL);
1005            fc.emit_i64(*v);
1006            let patch = fc.offset();
1007            fc.emit_i16(0);
1008            Ok(vec![patch])
1009        }
1010        MirPattern::Literal(lit) => {
1011            // Generic literal path — DUP + LOAD_CONST + EQ +
1012            // JUMP_IF_FALSE. Mirror of HIR's fallback for non-Int
1013            // literals (Bool / Float / Str / Unit).
1014            fc.emit_op(DUP);
1015            fc.compile_literal(lit)?;
1016            fc.emit_op(EQ);
1017            let patch = fc.emit_jump(JUMP_IF_FALSE);
1018            Ok(vec![patch])
1019        }
1020        MirPattern::EmptyList => {
1021            fc.emit_op(MATCH_NIL);
1022            let patch = fc.offset();
1023            fc.emit_i16(0);
1024            Ok(vec![patch])
1025        }
1026        MirPattern::Cons { head, tail, .. } => {
1027            fc.emit_op(MATCH_CONS);
1028            let patch = fc.offset();
1029            fc.emit_i16(0);
1030            // Successful match: extract head/tail and bind into
1031            // the resolver-assigned slots. The HIR walker does
1032            // the same shape; MIR's `LocalId` directly carries
1033            // the slot.
1034            fc.emit_op(DUP);
1035            fc.emit_op(LIST_HEAD_TAIL);
1036            emit_store_or_pop(fc, *head);
1037            emit_store_or_pop(fc, *tail);
1038            Ok(vec![patch])
1039        }
1040        MirPattern::Ctor {
1041            ctor: MirCtor::User(ctor_id),
1042            bindings,
1043            ..
1044        } => {
1045            // Resolve `CtorId → arena ctor_id` via the same path
1046            // the HIR walker uses (symbol table → canonical name
1047            // → arena type id → variant id → arena ctor id).
1048            let entry = fc.symbol_table.ctor_entry(*ctor_id);
1049            let owning_type = entry.owning_type;
1050            let variant_name = entry.name.clone();
1051            let qualified_type_name = fc.canonical_type_name(owning_type)?;
1052            let arena_type_id = fc.resolve_type_id(&qualified_type_name).ok_or_else(|| {
1053                MirVmUnsupported::InnerError(CompileError {
1054                    msg: format!(
1055                        "MIR-VM: unknown arena type `{qualified_type_name}` for ctor pattern"
1056                    ),
1057                })
1058            })?;
1059            let variant_id = fc
1060                .arena
1061                .find_variant_id(arena_type_id, &variant_name)
1062                .ok_or_else(|| {
1063                    MirVmUnsupported::InnerError(CompileError {
1064                        msg: format!(
1065                            "MIR-VM: unknown variant `{variant_name}` on `{qualified_type_name}`"
1066                        ),
1067                    })
1068                })?;
1069            let arena_ctor_id =
1070                fc.arena.find_ctor_id(arena_type_id, variant_id).ok_or_else(|| {
1071                    MirVmUnsupported::InnerError(CompileError {
1072                        msg: format!(
1073                            "MIR-VM: unknown arena ctor id for `{qualified_type_name}.{variant_name}`"
1074                        ),
1075                    })
1076                })?;
1077            if arena_ctor_id > u16::MAX as u32 {
1078                return Err(MirVmUnsupported::InnerError(CompileError {
1079                    msg: format!(
1080                        "MIR-VM: ctor id too large for MATCH_VARIANT: {qualified_type_name}.{variant_name}"
1081                    ),
1082                }));
1083            }
1084            fc.emit_op(MATCH_VARIANT);
1085            fc.emit_u16(arena_ctor_id as u16);
1086            let patch = fc.offset();
1087            fc.emit_i16(0);
1088            // EXTRACT_FIELD doesn't consume the subject — value
1089            // stays on the stack between field extractions.
1090            // Wildcard `_` bindings carry the sentinel slot;
1091            // `emit_store_or_pop` collapses to POP for those.
1092            for (i, b) in bindings.iter().enumerate() {
1093                fc.emit_op(EXTRACT_FIELD);
1094                fc.emit_u8(i as u8);
1095                emit_store_or_pop(fc, *b);
1096            }
1097            Ok(vec![patch])
1098        }
1099        MirPattern::Tuple(items) => {
1100            // Phase 4i — tuple pattern with arbitrarily-nested
1101            // subpatterns. Emit MATCH_TUPLE for the arity check,
1102            // then walk each subpattern through
1103            // `emit_nested_subpattern` (analog of HIR's
1104            // `compile_extracted_subpattern`) so nested
1105            // structural patterns (Cons / Ctor / Tuple / …) get
1106            // a cleanup-jump if they fail mid-extraction.
1107            fc.emit_op(MATCH_TUPLE);
1108            fc.emit_u8(items.len() as u8);
1109            let tuple_fail = fc.offset();
1110            fc.emit_i16(0);
1111            let mut all_patches = vec![tuple_fail];
1112            for (i, sub) in items.iter().enumerate() {
1113                let nested = emit_nested_subpattern(
1114                    fc,
1115                    |fc| {
1116                        fc.emit_op(EXTRACT_TUPLE_ITEM);
1117                        fc.emit_u8(i as u8);
1118                    },
1119                    sub,
1120                )?;
1121                all_patches.extend(nested);
1122            }
1123            Ok(all_patches)
1124        }
1125        MirPattern::Ctor {
1126            ctor: MirCtor::Builtin(bc),
1127            bindings,
1128            ..
1129        } => {
1130            // Built-in wrapper variants (Result.Ok / Result.Err /
1131            // Option.Some). Option.None is the nullary case —
1132            // dispatched via the NONE constant compare.
1133            match bc {
1134                BuiltinCtor::ResultOk | BuiltinCtor::ResultErr | BuiltinCtor::OptionSome => {
1135                    let kind: u8 = match bc {
1136                        BuiltinCtor::ResultOk => 0,
1137                        BuiltinCtor::ResultErr => 1,
1138                        BuiltinCtor::OptionSome => 2,
1139                        BuiltinCtor::OptionNone => unreachable!(),
1140                    };
1141                    fc.emit_op(MATCH_UNWRAP);
1142                    fc.emit_u8(kind);
1143                    let patch = fc.offset();
1144                    fc.emit_i16(0);
1145                    // MATCH_UNWRAP replaces TOS with the inner
1146                    // value; the binding (when present) takes a
1147                    // DUP + store-or-pop — same shape the HIR
1148                    // walker uses (wildcard `_` collapses to no
1149                    // emit at all, mirroring HIR's `dup_and_bind`
1150                    // on `_`).
1151                    if let Some(b) = bindings.first() {
1152                        emit_dup_and_bind(fc, *b);
1153                    }
1154                    Ok(vec![patch])
1155                }
1156                BuiltinCtor::OptionNone => {
1157                    // Nullary: DUP + LOAD_CONST NONE + EQ +
1158                    // JUMP_IF_FALSE. No bindings to extract.
1159                    fc.emit_op(DUP);
1160                    let none_const = fc.add_constant(NanValue::NONE);
1161                    fc.emit_op(LOAD_CONST);
1162                    fc.emit_u16(none_const);
1163                    fc.emit_op(EQ);
1164                    let patch = fc.emit_jump(JUMP_IF_FALSE);
1165                    Ok(vec![patch])
1166                }
1167            }
1168        }
1169    }
1170}
1171
1172/// Phase 6 wave 2 — try the MATCH_DISPATCH_CONST table
1173/// fast-path. Returns `Ok(Some(()))` when the table was emitted
1174/// (caller skips the linear emit), `Ok(None)` when the arm
1175/// shape doesn't fit the fast-path (caller proceeds with linear
1176/// emit). Mirrors HIR's `emit_match_dispatch_const`.
1177///
1178/// Fast-path requirements (mirrors HIR's classifier):
1179/// - At least one dispatchable arm (otherwise the table is
1180///   pointless).
1181/// - Every non-last arm: pattern is `Literal(Int | Bool | Unit)`
1182///   (the bit-comparable subset) with a body that itself
1183///   reduces to a `Literal` whose `NanValue::bits()` we can
1184///   inline as the result.
1185/// - The last arm is the default: `Wildcard` / `Bind`. We do
1186///   not require its body to be a literal — the opcode pushes
1187///   the subject back on miss and falls through to the default
1188///   arm body, which we compile normally.
1189fn try_emit_match_dispatch_const(
1190    fc: &mut FnCompiler<'_>,
1191    subject: &Spanned<MirExpr>,
1192    arms: &[crate::ir::mir::MirMatchArm],
1193) -> Result<Option<()>, MirVmUnsupported> {
1194    if arms.len() < 2 {
1195        return Ok(None);
1196    }
1197    let last_idx = arms.len() - 1;
1198    let default_arm = &arms[last_idx];
1199    let default_local = match &default_arm.pattern {
1200        MirPattern::Wildcard => None,
1201        MirPattern::Bind(local, _name) => Some(*local),
1202        _ => return Ok(None),
1203    };
1204
1205    // Classify all non-default arms — every one must be a
1206    // const-dispatchable literal pattern with a const literal
1207    // body. Anything else aborts the fast-path.
1208    let mut entries: Vec<(u8, u64, u64)> = Vec::with_capacity(last_idx);
1209    for arm in &arms[..last_idx] {
1210        let pattern_lit = match &arm.pattern {
1211            MirPattern::Literal(lit) => lit,
1212            _ => return Ok(None),
1213        };
1214        let body_lit = match &arm.body.node {
1215            MirExpr::Literal(spanned_lit) => &spanned_lit.node,
1216            _ => return Ok(None),
1217        };
1218        let expected = literal_dispatch_bits(fc, pattern_lit);
1219        let result = literal_dispatch_bits(fc, body_lit);
1220        // Strings would need DISPATCH_KIND_STRING + arena-side
1221        // interning to compare; keep this wave focused on
1222        // bit-equal dispatch (Int / Bool / Unit / Float-as-bits).
1223        // Str / non-bit-comparable literals abort the fast-path.
1224        if pattern_is_dispatchable_bits(pattern_lit) && body_is_dispatchable_bits(body_lit) {
1225            entries.push((0u8, expected, result)); // DISPATCH_KIND_EXACT = 0
1226        } else {
1227            return Ok(None);
1228        }
1229    }
1230
1231    if entries.is_empty() {
1232        return Ok(None);
1233    }
1234
1235    // ── Emit ────────────────────────────────────────────────
1236    compile_mir_expr(fc, subject)?;
1237
1238    fc.emit_op(MATCH_DISPATCH_CONST);
1239    fc.emit_u8(entries.len() as u8);
1240    let default_offset_patch = fc.offset();
1241    fc.emit_i16(0); // default_offset — patched after the table
1242
1243    for (kind, expected, result) in &entries {
1244        fc.emit_u8(*kind);
1245        fc.emit_u64(*expected);
1246        fc.emit_u64(*result);
1247    }
1248    let table_end = fc.offset();
1249
1250    // On hit: opcode pushes the result and ip lands right after
1251    // the table — emit a JUMP to skip past the default arm body.
1252    let hit_skip = fc.emit_jump(JUMP);
1253
1254    // Default arm starts here. Default offset is relative to
1255    // `table_end` (the opcode handler adds `default_offset` to
1256    // ip-after-table-end).
1257    let default_start = fc.offset();
1258    let default_rel = (default_start as isize - table_end as isize) as i16;
1259    let bytes = (default_rel as u16).to_be_bytes();
1260    fc.code_mut()[default_offset_patch] = bytes[0];
1261    fc.code_mut()[default_offset_patch + 1] = bytes[1];
1262
1263    // Default arm body — subject was popped+repushed by the
1264    // opcode on miss. Bind it if the pattern is `Bind(local)`,
1265    // then drop it via POP before the body.
1266    if let Some(local) = default_local {
1267        emit_dup_and_bind(fc, local);
1268    }
1269    fc.emit_op(POP);
1270    compile_mir_expr(fc, &default_arm.body)?;
1271
1272    // Patch the hit-skip JUMP to land after the default body.
1273    fc.patch_jump(hit_skip);
1274    Ok(Some(()))
1275}
1276
1277/// `true` when a literal's runtime bits are dispatch-comparable
1278/// (Int / Bool / Unit / Float). Strings need a different
1279/// dispatch kind (interning) so they abort the fast-path.
1280fn pattern_is_dispatchable_bits(lit: &Literal) -> bool {
1281    matches!(
1282        lit,
1283        Literal::Int(_) | Literal::Bool(_) | Literal::Unit | Literal::Float(_)
1284    )
1285}
1286
1287fn body_is_dispatchable_bits(lit: &Literal) -> bool {
1288    // String *bodies* could in principle be supported by
1289    // interning + inline result bits, but HIR's fast-path also
1290    // skips them. Mirror that.
1291    pattern_is_dispatchable_bits(lit)
1292}
1293
1294fn literal_dispatch_bits(fc: &mut FnCompiler<'_>, lit: &Literal) -> u64 {
1295    let nv = match lit {
1296        Literal::Int(i) => NanValue::new_int(*i, fc.arena),
1297        Literal::Float(f) => NanValue::new_float(*f),
1298        Literal::Bool(b) => NanValue::new_bool(*b),
1299        Literal::Unit => NanValue::UNIT,
1300        Literal::Str(s) => NanValue::new_string_value(s, fc.arena),
1301    };
1302    nv.bits()
1303}
1304
1305/// Last-arm exhaustive binding extraction. The pattern is
1306/// guaranteed to match (preceding arms exhausted everything
1307/// else) so we skip the match-check opcode and just bind
1308/// whatever the pattern names. Mirror of HIR's last-arm logic
1309/// in `compile_match`.
1310fn emit_last_arm_bindings(
1311    fc: &mut FnCompiler<'_>,
1312    pattern: &MirPattern,
1313) -> Result<(), MirVmUnsupported> {
1314    match pattern {
1315        MirPattern::Wildcard | MirPattern::Literal(_) | MirPattern::EmptyList => Ok(()),
1316        MirPattern::Bind(local, _name) => {
1317            emit_dup_and_bind(fc, *local);
1318            Ok(())
1319        }
1320        MirPattern::Cons { head, tail, .. } => {
1321            fc.emit_op(DUP);
1322            fc.emit_op(LIST_HEAD_TAIL);
1323            emit_store_or_pop(fc, *head);
1324            emit_store_or_pop(fc, *tail);
1325            Ok(())
1326        }
1327        MirPattern::Ctor {
1328            ctor: MirCtor::User(_),
1329            bindings,
1330            ..
1331        } => {
1332            for (i, b) in bindings.iter().enumerate() {
1333                fc.emit_op(EXTRACT_FIELD);
1334                fc.emit_u8(i as u8);
1335                emit_store_or_pop(fc, *b);
1336            }
1337            Ok(())
1338        }
1339        MirPattern::Ctor {
1340            ctor: MirCtor::Builtin(bc),
1341            bindings,
1342            ..
1343        } => {
1344            if let Some(b) = bindings.first() {
1345                let kind: u8 = match bc {
1346                    BuiltinCtor::ResultOk => 0,
1347                    BuiltinCtor::ResultErr => 1,
1348                    BuiltinCtor::OptionSome => 2,
1349                    BuiltinCtor::OptionNone => {
1350                        // Option.None has no bindings — nothing
1351                        // to extract.
1352                        return Ok(());
1353                    }
1354                };
1355                fc.emit_op(MATCH_UNWRAP);
1356                fc.emit_u8(kind);
1357                fc.emit_i16(0); // no-fail (shape known)
1358                emit_dup_and_bind(fc, *b);
1359            }
1360            Ok(())
1361        }
1362        MirPattern::Tuple(items) => {
1363            // Last-arm Tuple: shape known — for each item emit
1364            // EXTRACT_TUPLE_ITEM then recurse into the subpattern
1365            // as if it were itself a last-arm pattern (the outer
1366            // exhaustiveness propagates inward — every nested
1367            // subpattern is also guaranteed to match).
1368            for (i, sub) in items.iter().enumerate() {
1369                fc.emit_op(EXTRACT_TUPLE_ITEM);
1370                fc.emit_u8(i as u8);
1371                emit_last_arm_bindings(fc, sub)?;
1372                fc.emit_op(POP);
1373            }
1374            Ok(())
1375        }
1376    }
1377}
1378
1379/// Compile a subpattern that operates on an extracted value,
1380/// with proper cleanup if the inner pattern fails. Mirrors HIR's
1381/// `compile_extracted_subpattern`:
1382///
1383///   emit_subject(fc);             // e.g. EXTRACT_TUPLE_ITEM i
1384///   <subpattern emit>             // fail_patches
1385///   POP                           // drop subpattern subject on success
1386///   if any fail patches:
1387///     JUMP success_skip
1388///     cleanup: POP + JUMP outer_fail
1389///     patch fail_patches → cleanup
1390///     patch success_skip → here
1391///     return vec![outer_fail]
1392///   else:
1393///     return empty Vec
1394///
1395/// The outer-fail patch lets the surrounding match arm jump to
1396/// the next arm if any nested subpattern fails, without
1397/// leaking the cleanup state.
1398fn emit_nested_subpattern<F>(
1399    fc: &mut FnCompiler<'_>,
1400    emit_subject: F,
1401    pattern: &MirPattern,
1402) -> Result<Vec<usize>, MirVmUnsupported>
1403where
1404    F: FnOnce(&mut FnCompiler<'_>),
1405{
1406    emit_subject(fc);
1407    let inner_fail_patches = emit_pattern_check(fc, pattern)?;
1408    fc.emit_op(POP);
1409
1410    if inner_fail_patches.is_empty() {
1411        return Ok(Vec::new());
1412    }
1413
1414    let success_skip = fc.emit_jump(JUMP);
1415    let cleanup_target = fc.offset();
1416    for patch in inner_fail_patches {
1417        fc.patch_jump_to(patch, cleanup_target);
1418    }
1419    fc.emit_op(POP);
1420    let outer_fail = fc.emit_jump(JUMP);
1421    fc.patch_jump(success_skip);
1422    Ok(vec![outer_fail])
1423}
1424
1425/// Extract `(slot, last_use)` if `expr` is a bare local read.
1426fn mir_local_slot_last_use(expr: &MirExpr) -> Option<(u32, bool)> {
1427    match expr {
1428        MirExpr::Local(l) => Some((l.node.slot.0, l.node.last_use)),
1429        _ => None,
1430    }
1431}
1432
1433/// Recognize the two fused vector compound shapes the HIR walker emits
1434/// as single opcodes, and emit the same opcode from MIR so the
1435/// VM-default path keeps the in-place / no-`Option`-alloc fast path:
1436///
1437/// - `Option.withDefault(Vector.set(v, i, x), v)` (same `v`) →
1438///   `VECTOR_SET_OR_KEEP`. `owned` mirrors the HIR rule: last use of the
1439///   inner vector slot AND the slot is not alias-prone.
1440/// - `Option.withDefault(Vector.get(v, i), <literal>)` → `VECTOR_GET_OR`
1441///   with the literal default inlined as a constant.
1442///
1443/// Returns `Ok(true)` when it emitted a fused op; `Ok(false)` leaves the
1444/// generic `UNWRAP_OR` path to the caller.
1445fn try_emit_vector_compound(
1446    fc: &mut FnCompiler<'_>,
1447    args: &[Spanned<MirExpr>],
1448) -> Result<bool, MirVmUnsupported> {
1449    if args.len() != 2 {
1450        return Ok(false);
1451    }
1452    let MirExpr::Call(inner_call) = &args[0].node else {
1453        return Ok(false);
1454    };
1455    let MirCall {
1456        callee: MirCallee::Builtin(inner_id),
1457        args: inner_args,
1458    } = &inner_call.node
1459    else {
1460        return Ok(false);
1461    };
1462    let inner_name = fc
1463        .mir_program
1464        .map(|p| p.builtin_name(*inner_id))
1465        .unwrap_or("");
1466    match lookup_vm_builtin(inner_name) {
1467        Some(VmBuiltin::VectorSet) if inner_args.len() == 3 => {
1468            // The default must be the same vector the set targets —
1469            // compared by slot, since the two reads carry different
1470            // last-use bits and a structural compare would miss it.
1471            let vec = mir_local_slot_last_use(&inner_args[0].node);
1472            let def = mir_local_slot_last_use(&args[1].node);
1473            let (Some((vec_slot, vec_last_use)), Some((def_slot, def_last_use))) = (vec, def)
1474            else {
1475                return Ok(false);
1476            };
1477            if vec_slot != def_slot {
1478                return Ok(false);
1479            }
1480            // Self-keep fusion ownership collapse. The inner `Vector.set`
1481            // and the `withDefault` default read the SAME slot
1482            // (vec_slot == def_slot), and the fused `VECTOR_SET_OR_KEEP`
1483            // returns exactly one of those two handles — so the slot is
1484            // dead after the op iff EITHER occurrence is its last use.
1485            // `last_use` annotates only the textually-last read (the
1486            // default, arg[1]), leaving the inner read last_use=false; OR
1487            // the two bits so a linearly-threaded vector takes the
1488            // in-place path. Still gated on `!is_aliased_slot`: the
1489            // owned-param refinement (`own_param.rs`) is what proves a
1490            // threaded `Vector`/`Map` param non-aliased, and only then
1491            // does the in-place set fire — keeping the guard load-bearing.
1492            let owned = (vec_last_use || def_last_use) && !fc.is_aliased_slot(vec_slot as u16);
1493            compile_mir_expr(fc, &inner_args[0])?;
1494            compile_mir_expr(fc, &inner_args[1])?;
1495            compile_mir_expr(fc, &inner_args[2])?;
1496            fc.emit_op(VECTOR_SET_OR_KEEP);
1497            fc.emit_u8(u8::from(owned));
1498            Ok(true)
1499        }
1500        Some(VmBuiltin::VectorGet) if inner_args.len() == 2 => {
1501            let MirExpr::Literal(lit) = &args[1].node else {
1502                return Ok(false);
1503            };
1504            compile_mir_expr(fc, &inner_args[0])?;
1505            compile_mir_expr(fc, &inner_args[1])?;
1506            let default_value = fc.nan_literal(&lit.node);
1507            let const_idx = fc.add_constant(default_value);
1508            fc.emit_op(VECTOR_GET_OR);
1509            fc.emit_u16(const_idx);
1510            Ok(true)
1511        }
1512        _ => Ok(false),
1513    }
1514}
1515
1516/// Tail-call owned mask: bit `i` is set when arg `i` carries a last-use
1517/// read of slot `i` — i.e. param slot `i` is threaded straight back into
1518/// the callee's slot `i` and is dead afterwards, so it can be moved
1519/// rather than copied. Keyed on the arg-index == slot-index convention
1520/// that only holds for the slot-aligned tail-call args. Caps at 8 args
1521/// (mask is u8).
1522///
1523/// The alias-prone slot guard is load-bearing: the owned path empties
1524/// the arena slot in place (`take_map_value` / vector take), so marking
1525/// an aliased `Vector`/`Map` slot owned would mutate a binding the
1526/// caller still holds.
1527fn compute_owned_mask(args: &[Spanned<MirExpr>], fc: &FnCompiler<'_>) -> u8 {
1528    let mut mask = 0u8;
1529    for (i, arg) in args.iter().enumerate().take(8) {
1530        if contains_last_use_slot_mir(&arg.node, i as u32) && !fc.is_aliased_slot(i as u16) {
1531            mask |= 1 << i;
1532        }
1533    }
1534    mask
1535}
1536
1537/// Builtin owned mask: bit `i` is set when arg `i` is itself a last-use
1538/// read of a non-aliased slot (uniquely owned, safe to take in place) —
1539/// keyed on the arg's OWN slot, NOT the arg index. A builtin's arguments
1540/// do not line up with slot indices (`Map.set(m, k, v)` reads `m` from
1541/// whatever slot it was bound to), so the tail-call mask above silently
1542/// misses them. The runtime honors only bit 0 and only for
1543/// `Map.set` / `Vector.set` (`invoke_builtin_with_owned`), so a broader
1544/// mask is harmless for every other builtin. The `!is_aliased_slot`
1545/// guard stays load-bearing — `own_param` is what makes a linearly
1546/// threaded `Vector`/`Map` param non-aliased, and only then does the
1547/// in-place take fire.
1548fn compute_builtin_owned_mask(args: &[Spanned<MirExpr>], fc: &FnCompiler<'_>) -> u8 {
1549    let mut mask = 0u8;
1550    for (i, arg) in args.iter().enumerate().take(8) {
1551        if let Some((slot, last_use)) = mir_local_slot_last_use(&arg.node)
1552            && last_use
1553            && !fc.is_aliased_slot(slot as u16)
1554        {
1555            mask |= 1 << i;
1556        }
1557    }
1558    mask
1559}
1560
1561/// Recursive search: does `expr` contain a `MirExpr::Local`
1562/// reading slot `target` with `last_use = true`? Mirror of
1563/// HIR's `contains_last_use_slot`.
1564fn contains_last_use_slot_mir(expr: &MirExpr, target: u32) -> bool {
1565    match expr {
1566        MirExpr::Local(spanned_local) => {
1567            spanned_local.node.slot.0 == target && spanned_local.node.last_use
1568        }
1569        MirExpr::Call(c) => c
1570            .node
1571            .args
1572            .iter()
1573            .any(|a| contains_last_use_slot_mir(&a.node, target)),
1574        MirExpr::TailCall(t) => t
1575            .node
1576            .args
1577            .iter()
1578            .any(|a| contains_last_use_slot_mir(&a.node, target)),
1579        MirExpr::BinOp(b) => {
1580            contains_last_use_slot_mir(&b.node.lhs.node, target)
1581                || contains_last_use_slot_mir(&b.node.rhs.node, target)
1582        }
1583        MirExpr::Neg(inner) => contains_last_use_slot_mir(&inner.node, target),
1584        MirExpr::Project(p) => contains_last_use_slot_mir(&p.node.base.node, target),
1585        MirExpr::Try(inner) => contains_last_use_slot_mir(&inner.node, target),
1586        MirExpr::Construct(c) => c
1587            .node
1588            .args
1589            .iter()
1590            .any(|a| contains_last_use_slot_mir(&a.node, target)),
1591        MirExpr::RecordCreate(rc) => rc
1592            .node
1593            .fields
1594            .iter()
1595            .any(|f| contains_last_use_slot_mir(&f.value.node, target)),
1596        MirExpr::RecordUpdate(ru) => {
1597            contains_last_use_slot_mir(&ru.node.base.node, target)
1598                || ru
1599                    .node
1600                    .updates
1601                    .iter()
1602                    .any(|f| contains_last_use_slot_mir(&f.value.node, target))
1603        }
1604        MirExpr::List(items) | MirExpr::Tuple(items) => items
1605            .iter()
1606            .any(|i| contains_last_use_slot_mir(&i.node, target)),
1607        MirExpr::MapLiteral(entries) => entries.iter().any(|(k, v)| {
1608            contains_last_use_slot_mir(&k.node, target)
1609                || contains_last_use_slot_mir(&v.node, target)
1610        }),
1611        MirExpr::IndependentProduct(ip) => ip
1612            .node
1613            .items
1614            .iter()
1615            .any(|i| contains_last_use_slot_mir(&i.node, target)),
1616        MirExpr::InterpolatedStr(parts) => parts.iter().any(|p| match p {
1617            MirStrPart::Expr(e) => contains_last_use_slot_mir(&e.node, target),
1618            MirStrPart::Literal(_) => false,
1619        }),
1620        // Match / IfThenElse / Let / Return: structural
1621        // recursion stays conservative — we only care about
1622        // the immediate arg-evaluation context for the
1623        // owned_mask, so deep recursion into match arms /
1624        // if branches / let bodies isn't useful.
1625        MirExpr::Match(_)
1626        | MirExpr::IfThenElse(_)
1627        | MirExpr::Let(_)
1628        | MirExpr::Return(_)
1629        // FnValue is a bare symbol reference — no slot read.
1630        | MirExpr::FnValue(_)
1631        | MirExpr::Literal(_) => false,
1632    }
1633}
1634
1635/// Linear-search lookup `name → VmBuiltin`. Returns `None` for
1636/// names not in the builtin table — the caller drops back to HIR
1637/// via `MirVmUnsupported::UnsupportedCallee`. The table is small
1638/// (~60 entries) so linear scan is fine; a future Phase 6 can
1639/// cache it.
1640fn lookup_vm_builtin(name: &str) -> Option<VmBuiltin> {
1641    VmBuiltin::ALL.iter().copied().find(|b| b.name() == name)
1642}
1643
1644/// Helper: emit a single ctor arg, or `LOAD_UNIT` when the ctor
1645/// arg is absent (defensive — built-in Wrap-shaped ctors always
1646/// take exactly one arg in well-typed Aver, but the lowerer
1647/// only enforces that at the type level).
1648fn emit_constructor_arg(
1649    fc: &mut FnCompiler<'_>,
1650    arg: Option<&Spanned<MirExpr>>,
1651) -> Result<(), MirVmUnsupported> {
1652    match arg {
1653        Some(a) => compile_mir_expr(fc, a),
1654        None => {
1655            fc.emit_op(LOAD_UNIT);
1656            Ok(())
1657        }
1658    }
1659}
1660
1661fn emit_binop_typed(
1662    fc: &mut FnCompiler<'_>,
1663    op: crate::ast::BinOp,
1664    lhs_ty: Option<&crate::ast::Type>,
1665    rhs_ty: Option<&crate::ast::Type>,
1666) {
1667    use crate::ast::BinOp::*;
1668    use crate::ast::Type;
1669    let both_int = matches!((lhs_ty, rhs_ty), (Some(Type::Int), Some(Type::Int)));
1670    let both_float = matches!((lhs_ty, rhs_ty), (Some(Type::Float), Some(Type::Float)));
1671    let lt_op = if both_int {
1672        LT_INT
1673    } else if both_float {
1674        LT_FLOAT
1675    } else {
1676        LT
1677    };
1678    let gt_op = if both_int {
1679        GT_INT
1680    } else if both_float {
1681        GT_FLOAT
1682    } else {
1683        GT
1684    };
1685    let add_op = if both_int {
1686        ADD_INT
1687    } else if both_float {
1688        ADD_FLOAT
1689    } else {
1690        ADD
1691    };
1692    let sub_op = if both_int {
1693        SUB_INT
1694    } else if both_float {
1695        SUB_FLOAT
1696    } else {
1697        SUB
1698    };
1699    let mul_op = if both_int {
1700        MUL_INT
1701    } else if both_float {
1702        MUL_FLOAT
1703    } else {
1704        MUL
1705    };
1706    // No DIV_INT — integer division traps on `b == 0` and the
1707    // generic `arith_div` already does that branch + propagates
1708    // a typed runtime error.
1709    let div_op = if both_float { DIV_FLOAT } else { DIV };
1710    match op {
1711        Add => fc.emit_op(add_op),
1712        Sub => fc.emit_op(sub_op),
1713        Mul => fc.emit_op(mul_op),
1714        Div => fc.emit_op(div_op),
1715        Eq => fc.emit_op(if both_int { EQ_INT } else { EQ }),
1716        Lt => fc.emit_op(lt_op),
1717        Gt => fc.emit_op(gt_op),
1718        // `Neq` / `Lte` / `Gte` invert the corresponding
1719        // comparison (same composition HIR uses).
1720        Neq => {
1721            fc.emit_op(if both_int { EQ_INT } else { EQ });
1722            fc.emit_op(NOT);
1723        }
1724        Lte => {
1725            fc.emit_op(gt_op);
1726            fc.emit_op(NOT);
1727        }
1728        Gte => {
1729            fc.emit_op(lt_op);
1730            fc.emit_op(NOT);
1731        }
1732    }
1733}