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}