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}