Skip to main content

aver/vm/compiler/
mod.rs

1mod classify;
2mod expr;
3pub mod mir;
4mod resolve_helpers;
5
6use std::collections::HashMap;
7
8use crate::ast::{Stmt, TopLevel, TypeDef};
9use crate::ir::SymbolTable;
10use crate::ir::hir::{
11    ResolveCtx, ResolvedFnDef, ResolvedStmt, ResolvedTopLevel, resolve_top_level,
12};
13use crate::nan_value::{Arena, NanValue};
14use crate::types::{option, result};
15use crate::visibility;
16
17use super::builtin::VmBuiltin;
18use super::opcode::*;
19use super::symbol::{VmSymbolTable, VmVariantCtor};
20use super::types::{CodeStore, FnChunk};
21
22/// Compile a resolved program into bytecode.
23///
24/// `items` is the entry's resolved HIR (the output of the
25/// `NameResolve` pipeline stage). `symbols` is the entry's symbol
26/// table — every `ResolvedCallee::Fn(FnId)` / `ResolvedCtor::User`
27/// in the resolved tree resolves through it to a canonical name
28/// that the VM dispatches against.
29///
30/// `analysis` carries per-fn `FnAnalysis.allocates` from the
31/// pipeline's analyze stage; the VM compiler reads `chunk.no_alloc`
32/// from it directly.
33///
34/// Compile with explicit module root for `depends` resolution.
35pub fn compile_program_with_modules(
36    items: &[ResolvedTopLevel],
37    symbols: &SymbolTable,
38    arena: &mut Arena,
39    module_root: Option<&str>,
40    source_file: &str,
41    analysis: Option<&crate::ir::AnalysisResult>,
42) -> Result<(CodeStore, Vec<NanValue>), CompileError> {
43    compile_program_inner(
44        items,
45        symbols,
46        arena,
47        source_file,
48        ModuleSource::Disk(module_root),
49        analysis,
50    )
51}
52
53/// Compile using dependency modules that were already parsed off-disk
54/// (or out of a virtual filesystem). The browser playground uses this
55/// to run multi-file programs without any real fs access.
56pub fn compile_program_with_loaded_modules(
57    items: &[ResolvedTopLevel],
58    symbols: &SymbolTable,
59    arena: &mut Arena,
60    loaded: Vec<crate::source::LoadedModule>,
61    source_file: &str,
62    analysis: Option<&crate::ir::AnalysisResult>,
63) -> Result<(CodeStore, Vec<NanValue>), CompileError> {
64    compile_program_inner(
65        items,
66        symbols,
67        arena,
68        source_file,
69        ModuleSource::Loaded(loaded),
70        analysis,
71    )
72}
73
74/// Phase 4b of #252: compile with MIR-first dispatch + HIR
75/// fallback. Per fn: if the fn's body lowers cleanly to MIR
76/// *and* MIR-emit produces bytecode, use that chunk; otherwise
77/// fall back to the existing HIR walker. The fallback is
78/// deliberate — every fn that lands in `MirVmUnsupported`
79/// territory (Match / Try / TailCall / Construct / Record* /
80/// Project / List / Tuple / Map / InterpolatedStr /
81/// IndependentProduct / builtin callees / first-class fn
82/// values) keeps the well-tested HIR shape.
83///
84/// Same I/O contract as [`compile_program`]; the only
85/// difference is the per-fn dispatch.
86pub fn compile_program_with_mir_fallback(
87    items: &[ResolvedTopLevel],
88    symbols: &SymbolTable,
89    arena: &mut Arena,
90    analysis: Option<&crate::ir::AnalysisResult>,
91) -> Result<(CodeStore, Vec<NanValue>), CompileError> {
92    // Single-module MIR-default entry, kept for the parity tests
93    // (`tests/mir_vm_parity.rs`) that exercise the MIR path without a
94    // module root. The optimize pipeline + per-fn MIR dispatch now
95    // live in `compile_program_inner` under `use_mir`, shared with the
96    // module-aware production entry points.
97    compile_program_inner(
98        items,
99        symbols,
100        arena,
101        "",
102        ModuleSource::Disk(None),
103        analysis,
104    )
105}
106
107enum ModuleSource<'a> {
108    Disk(Option<&'a str>),
109    Loaded(Vec<crate::source::LoadedModule>),
110}
111
112/// Lower a resolved item list to MIR and run the Phase 6 optimize
113/// pipeline. Order is deliberate: (1) nullary-literal inlining unlocks
114/// call-site literals, (2) const-fold collapses literal arithmetic,
115/// (3) algebraic-simplify rewrites Int identities, (4) bool-match-to-if
116/// rewrites two-arm `Bool` matches into `IfThenElse`, (5) branch-collapse
117/// drops the dead branch of a folded `IfThenElse`, (6) DCE drops unread
118/// `let _ = <pure>` chains. Shared by the entry compile and per-dep-module
119/// MIR builds so both backends see identical lowered+optimized shapes.
120fn build_optimized_mir(
121    items: &[ResolvedTopLevel],
122    external_callers_possible: bool,
123) -> crate::ir::mir::MirProgram {
124    let mut lowered = crate::ir::mir::lower_program(items);
125    // Mark dependency-module fragments so `own_param_refine` bails: the
126    // VM compiles each `depends [...]` module separately, so a dep
127    // fragment cannot see the entry/sibling call sites that may alias a
128    // param. The entry compile (and the flattened wasm-gc / Rust builds)
129    // see all callers, so graduation stays enabled there.
130    lowered.external_callers_possible = external_callers_possible;
131    crate::ir::mir::optimize(lowered)
132}
133
134fn compile_program_inner(
135    items: &[ResolvedTopLevel],
136    symbols: &SymbolTable,
137    arena: &mut Arena,
138    source_file: &str,
139    module_source: ModuleSource<'_>,
140    analysis: Option<&crate::ir::AnalysisResult>,
141) -> Result<(CodeStore, Vec<NanValue>), CompileError> {
142    // MIR-default path. Lower the entry items to MIR and run the
143    // optimize pipeline; the per-fn loop below dispatches each fn
144    // through the MIR walker and falls back to the HIR walker for
145    // shapes outside the MIR subset. Built here — not in the caller —
146    // so every module-aware entry point shares one pipeline. Order is
147    // deliberate: (1) nullary-literal inlining unlocks call-site
148    // literals, (2) const-fold collapses literal arithmetic,
149    // (3) algebraic-simplify rewrites Int identities, (4) bool-match-
150    // to-if rewrites two-arm `Bool` matches into `IfThenElse`,
151    // (5) branch-collapse drops the dead branch of a folded
152    // `IfThenElse`, (6) DCE drops unread `let _ = <pure>` chains.
153    // Dep-module fns build their own MIR in `integrate_module` (same
154    // `build_optimized_mir` pipeline); the per-fn loop there dispatches
155    // through the MIR walker with the dep's module scope.
156    let mir_built = build_optimized_mir(items, false);
157    let mir_program = &mir_built;
158
159    let mut compiler = ProgramCompiler::new();
160    compiler.source_file = source_file.to_string();
161    compiler.sync_record_field_symbols(arena)?;
162    // Oracle v1: `BranchPath.Root` is a nullary value constructor
163    // (like `Option.None`). The VM symbol table needs it as a
164    // constant pointing at a pre-allocated arena record; this
165    // happens here because bootstrap_core_symbols runs before the
166    // arena is available.
167    compiler.install_branch_path_root_constant(arena)?;
168
169    match module_source {
170        ModuleSource::Disk(Some(module_root)) => {
171            compiler.load_modules(items, module_root, symbols, arena)?;
172        }
173        ModuleSource::Disk(None) => {}
174        ModuleSource::Loaded(loaded) => {
175            for m in loaded {
176                compiler.integrate_module(&m.dep_name, m.items, symbols, arena)?;
177            }
178        }
179    }
180
181    for item in items {
182        if let ResolvedTopLevel::Passthrough(TopLevel::Stmt(Stmt::Binding(name, _, _))) = item {
183            compiler.ensure_global(name);
184        }
185    }
186
187    for item in items {
188        match item {
189            ResolvedTopLevel::FnDef(rfd) => {
190                compiler.ensure_global(&rfd.name);
191                let effect_ids: Vec<u32> = rfd
192                    .effects
193                    .iter()
194                    .map(|effect| compiler.symbols.intern_name(&effect.node))
195                    .collect();
196                let fn_id = compiler.code.add_function(FnChunk {
197                    name: rfd.name.clone(),
198                    arity: rfd.params.len() as u8,
199                    local_count: 0,
200                    code: Vec::new(),
201                    constants: Vec::new(),
202                    effects: effect_ids,
203                    thin: false,
204                    parent_thin: false,
205                    leaf: false,
206                    no_alloc: false,
207                    source_file: String::new(),
208                    line_table: Vec::new(),
209                });
210                let symbol_id = compiler.symbols.intern_function(
211                    &rfd.name,
212                    fn_id,
213                    &rfd.effects
214                        .iter()
215                        .map(|e| e.node.clone())
216                        .collect::<Vec<_>>(),
217                )?;
218                let global_idx = compiler.global_names[&rfd.name];
219                compiler.globals[global_idx as usize] = VmSymbolTable::symbol_ref(symbol_id);
220            }
221            ResolvedTopLevel::Passthrough(TopLevel::TypeDef(td)) => {
222                // Current module: register in Arena (no qualified alias needed)
223                match td {
224                    TypeDef::Product { name, fields, .. } => {
225                        let field_names: Vec<String> =
226                            fields.iter().map(|(n, _)| n.clone()).collect();
227                        arena.register_record_type(name, field_names);
228                    }
229                    TypeDef::Sum { name, variants, .. } => {
230                        let variant_names: Vec<String> =
231                            variants.iter().map(|v| v.name.clone()).collect();
232                        arena.register_sum_type(name, variant_names);
233                    }
234                }
235                // VM-specific: register type symbols
236                compiler.register_type_in_symbols(td, arena)?;
237            }
238            _ => {}
239        }
240    }
241
242    compiler.register_current_module_namespace(items)?;
243
244    for item in items {
245        if let ResolvedTopLevel::FnDef(rfd) = item {
246            let fn_id = compiler.code.find(&rfd.name).unwrap();
247            // Walk this fn's MIR body into bytecode. MIR is the only VM
248            // codegen path — there is no HIR fallback. Every well-formed
249            // fn lowers (the full corpus + test suite hit zero
250            // rejections); a rejection here means an unsupported shape
251            // reached codegen on malformed / typecheck-rejected input, so
252            // it surfaces as a hard CompileError.
253            // Entry fns resolve through `global_names`; no module scope.
254            let entry_scope = HashMap::new();
255            let mir_fn = mir_program.fn_by_id(rfd.fn_id).ok_or_else(|| CompileError {
256                msg: format!(
257                    "internal error: fn `{}` did not lower to MIR (an unsupported shape reached the VM backend)",
258                    rfd.name
259                ),
260            })?;
261            let chunk = compiler
262                .compile_fn_via_mir(rfd, mir_fn, symbols, arena, &entry_scope, mir_program)
263                .map_err(|e| e.into_compile_error(&format!("fn `{}`", rfd.name)))?;
264            compiler.code.functions[fn_id as usize] = chunk;
265        }
266    }
267
268    compiler.compile_top_level(items, symbols, arena, mir_program)?;
269    compiler.code.symbols = compiler.symbols.clone();
270    classify::classify_thin_functions(&mut compiler.code, arena)?;
271
272    // Lowering-level no-alloc analysis driven by the supplied
273    // analysis. The pre-Phase-E in-place `compute_alloc_info`
274    // fallback assumed access to the original `FnDef` shape;
275    // after migration the VM compiler no longer holds those
276    // (resolved fn defs carry typed params, not source strings),
277    // so the fallback path becomes a conservative "assume yes".
278    // Every production caller passes `Some(analysis)` so the
279    // optimisation is reached on every real path.
280    let allocates = |name: &str| -> bool {
281        if let Some(a) = analysis
282            && let Some(fa) = a.fn_analyses.get(name)
283            && let Some(b) = fa.allocates
284        {
285            return b;
286        }
287        true
288    };
289    for item in items {
290        if let ResolvedTopLevel::FnDef(rfd) = item
291            && !allocates(&rfd.name)
292            && let Some(fn_id) = compiler.code.find(&rfd.name)
293        {
294            let chunk = &mut compiler.code.functions[fn_id as usize];
295            chunk.no_alloc = true;
296            // No-alloc bodies always satisfy `can_fast_return`'s
297            // runtime length-equality guards, so promote them into
298            // the thin fast-return class. The bytecode classifier
299            // rejected them for unrelated reasons (mutual TCO call,
300            // body size > MAX_PARENT_THIN, etc.) but for return
301            // purposes there's nothing left to do.
302            chunk.thin = true;
303        }
304    }
305
306    Ok((compiler.code, compiler.globals))
307}
308
309#[derive(Debug)]
310pub struct CompileError {
311    pub msg: String,
312}
313
314impl std::fmt::Display for CompileError {
315    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
316        write!(f, "Compile error: {}", self.msg)
317    }
318}
319
320/// Iron — B2: lift `SymbolError` into the compiler's diagnostic
321/// channel. `VmSymbolTable` used to `panic!` on every kind clash;
322/// the conversion lets the compile path surface the same condition
323/// as a regular `CompileError` instead of aborting the process.
324impl From<crate::vm::symbol::SymbolError> for CompileError {
325    fn from(err: crate::vm::symbol::SymbolError) -> Self {
326        CompileError {
327            msg: err.to_string(),
328        }
329    }
330}
331
332struct ProgramCompiler {
333    code: CodeStore,
334    symbols: VmSymbolTable,
335    globals: Vec<NanValue>,
336    global_names: HashMap<String, u16>,
337    /// Source file path for the main program (propagated to FnChunks).
338    source_file: String,
339}
340
341impl ProgramCompiler {
342    fn new() -> Self {
343        let mut compiler = ProgramCompiler {
344            code: CodeStore::new(),
345            symbols: VmSymbolTable::default(),
346            globals: Vec::new(),
347            global_names: HashMap::new(),
348            source_file: String::new(),
349        };
350        // bootstrap into a fresh `VmSymbolTable` populates well-known
351        // builtins / wrappers / namespaces; nothing it inserts can
352        // clash with prior state, so a `SymbolError` here would be a
353        // bug in the bootstrap data, not a user-input failure.
354        compiler
355            .bootstrap_core_symbols()
356            .expect("bootstrap_core_symbols on empty VmSymbolTable cannot fail");
357        compiler
358    }
359
360    fn sync_record_field_symbols(&mut self, arena: &Arena) -> Result<(), CompileError> {
361        for type_id in 0..arena.type_count() {
362            let type_name = arena.get_type_name(type_id);
363            self.symbols.intern_namespace_path(type_name)?;
364            let field_names = arena.get_field_names(type_id);
365            if field_names.is_empty() {
366                continue;
367            }
368            let field_symbol_ids: Vec<u32> = field_names
369                .iter()
370                .map(|field_name| self.symbols.intern_name(field_name))
371                .collect();
372            self.code.register_record_fields(type_id, &field_symbol_ids);
373        }
374        Ok(())
375    }
376
377    /// Load all modules from `depends [...]` declarations using the shared loader,
378    /// then compile each module's functions and register symbols.
379    fn load_modules(
380        &mut self,
381        items: &[ResolvedTopLevel],
382        module_root: &str,
383        entry_symbols: &SymbolTable,
384        arena: &mut Arena,
385    ) -> Result<(), CompileError> {
386        let module = items.iter().find_map(|i| match i {
387            ResolvedTopLevel::Module(m) => Some(m),
388            _ => None,
389        });
390        let module = match module {
391            Some(m) => m,
392            None => return Ok(()),
393        };
394
395        let modules = crate::source::load_module_tree(&module.depends, module_root)
396            .map_err(|e| CompileError { msg: e })?;
397
398        for loaded in modules {
399            self.integrate_module(&loaded.dep_name, loaded.items, entry_symbols, arena)?;
400        }
401        Ok(())
402    }
403
404    /// Integrate a loaded module into the VM: register types, compile functions,
405    /// expose symbols.
406    ///
407    /// Resolves dep items against the entry's `SymbolTable` so every
408    /// scope shares the same `FnId` / `TypeId` namespace — the VM no
409    /// longer owns a parallel resolver. Callers ensure the entry
410    /// pipeline ran with `dep_modules` populated so `entry_symbols`
411    /// knows about every transitive dep before this is invoked
412    /// (`cmd_run_vm`, `cmd_compile_aver`, and tests via
413    /// `load_compile_deps`).
414    fn integrate_module(
415        &mut self,
416        dep_name: &str,
417        mut mod_items: Vec<TopLevel>,
418        entry_symbols: &SymbolTable,
419        arena: &mut Arena,
420    ) -> Result<(), CompileError> {
421        // Caller already ran the full canonical pipeline on the entry,
422        // including BuildSymbols + NameResolve over `dep_modules`. We
423        // still need TCO + slot-resolve on the freshly-parsed dep
424        // items so the body shape matches what the entry's resolver
425        // saw (TCO rewrites tail calls; the slot resolver allocates
426        // local slots both passes rely on).
427        crate::ir::pipeline::tco(&mut mod_items);
428        crate::ir::pipeline::resolve(&mut mod_items);
429
430        // Register types in Arena with qualified aliases.
431        for mt in visibility::collect_module_types(&mod_items) {
432            let type_id = match &mt.kind {
433                visibility::ModuleTypeKind::Record { field_names } => {
434                    arena.register_record_type(&mt.bare_name, field_names.clone())
435                }
436                visibility::ModuleTypeKind::Sum { variant_names } => {
437                    arena.register_sum_type(&mt.bare_name, variant_names.clone())
438                }
439            };
440            arena.register_type_alias(
441                &visibility::qualified_name(dep_name, &mt.bare_name),
442                type_id,
443            );
444        }
445        for item in &mod_items {
446            if let TopLevel::TypeDef(td) = item {
447                self.register_type_in_symbols(td, arena)?;
448            }
449        }
450
451        // Lift dep items into resolved HIR against the *entry's*
452        // symbol table — keeps a single, unified `FnId` / `TypeId`
453        // namespace across the whole compile unit. Pin the
454        // resolver's `current_module` to the canonical dep_name so
455        // intra-dep call shapes (`Foo.bar`, bare `bar`) match the
456        // `FnKey::in_module(dep_name, _)` rows the entry pipeline
457        // already inserted, regardless of the dep's source-declared
458        // leaf name (`module Ast` inside `Domain.Ast.av`).
459        let mut ctx = ResolveCtx::new(entry_symbols);
460        ctx.current_module = Some(dep_name.to_string());
461        let dep_resolved: Vec<ResolvedTopLevel> = mod_items
462            .iter()
463            .map(|i| resolve_top_level(&ctx, i))
464            .collect();
465
466        // Compile ALL functions (not just exposed).
467        let mut module_fn_ids: Vec<(String, u32)> = Vec::new();
468        for item in &dep_resolved {
469            if let ResolvedTopLevel::FnDef(rfd) = item {
470                let qualified_name = visibility::qualified_name(dep_name, &rfd.name);
471                let effect_ids: Vec<u32> = rfd
472                    .effects
473                    .iter()
474                    .map(|effect| self.symbols.intern_name(&effect.node))
475                    .collect();
476                let fn_id = self.code.add_function(FnChunk {
477                    name: qualified_name.clone(),
478                    arity: rfd.params.len() as u8,
479                    local_count: 0,
480                    code: Vec::new(),
481                    constants: Vec::new(),
482                    effects: effect_ids,
483                    thin: false,
484                    parent_thin: false,
485                    leaf: false,
486                    no_alloc: false,
487                    source_file: String::new(),
488                    line_table: Vec::new(),
489                });
490                self.symbols.intern_function(
491                    &qualified_name,
492                    fn_id,
493                    &rfd.effects
494                        .iter()
495                        .map(|e| e.node.clone())
496                        .collect::<Vec<_>>(),
497                )?;
498                module_fn_ids.push((rfd.name.clone(), fn_id));
499            }
500        }
501
502        let module_scope: HashMap<String, u32> = module_fn_ids.iter().cloned().collect();
503        // Lower the dep module to MIR and walk each fn's MIR body into
504        // bytecode with the dep's module scope — the same path the entry
505        // module takes. MIR is the only VM codegen path; a rejection
506        // surfaces as a hard CompileError (no HIR fallback).
507        let dep_mir = build_optimized_mir(&dep_resolved, true);
508        let mut fn_idx = 0;
509        for item in &dep_resolved {
510            if let ResolvedTopLevel::FnDef(rfd) = item {
511                let (fn_name, fn_id) = &module_fn_ids[fn_idx];
512                let mir_fn = dep_mir.fn_by_id(rfd.fn_id).ok_or_else(|| CompileError {
513                    msg: format!(
514                        "internal error: dep fn `{}` did not lower to MIR (an unsupported shape reached the VM backend)",
515                        rfd.name
516                    ),
517                })?;
518                let mut chunk = self
519                    .compile_fn_via_mir(rfd, mir_fn, entry_symbols, arena, &module_scope, &dep_mir)
520                    .map_err(|e| e.into_compile_error(&format!("dep fn `{}`", rfd.name)))?;
521                chunk.name = visibility::qualified_name(dep_name, fn_name);
522                self.code.functions[*fn_id as usize] = chunk;
523                fn_idx += 1;
524            }
525        }
526
527        // Expose exported functions and types via globals and namespace members.
528        let exports = visibility::collect_module_exports(&mod_items);
529
530        for fd in &exports.functions {
531            let qualified = visibility::qualified_name(dep_name, &fd.name);
532            let global_idx = self.ensure_global(&qualified);
533            let symbol_id = self.symbols.find(&qualified).ok_or_else(|| CompileError {
534                msg: format!("missing VM symbol for exposed function {}", qualified),
535            })?;
536            self.globals[global_idx as usize] = VmSymbolTable::symbol_ref(symbol_id);
537        }
538
539        let module_symbol_id = self.symbols.intern_namespace_path(dep_name)?;
540        for et in &exports.types {
541            let type_name = match et.def {
542                TypeDef::Sum { name, .. } | TypeDef::Product { name, .. } => name,
543            };
544            if let Some(type_symbol_id) = self.symbols.find(type_name) {
545                let member_symbol_id = self.symbols.intern_name(type_name);
546                self.symbols.add_namespace_member_by_id(
547                    module_symbol_id,
548                    member_symbol_id,
549                    VmSymbolTable::symbol_ref(type_symbol_id),
550                )?;
551            }
552        }
553        for fd in &exports.functions {
554            let qualified = visibility::qualified_name(dep_name, &fd.name);
555            if let Some(fn_symbol_id) = self.symbols.find(&qualified) {
556                let member_symbol_id = self.symbols.intern_name(&fd.name);
557                self.symbols.add_namespace_member_by_id(
558                    module_symbol_id,
559                    member_symbol_id,
560                    VmSymbolTable::symbol_ref(fn_symbol_id),
561                )?;
562            }
563        }
564
565        Ok(())
566    }
567
568    /// Oracle v1: install `BranchPath.Root` as a nullary constant
569    /// member of the `BranchPath` namespace. The record is allocated
570    /// once in the arena; the symbol table holds a NanValue
571    /// referencing it. Follows the same pattern as `Option.None`
572    /// which is installed as an immediate constant in
573    /// `bootstrap_core_symbols`.
574    fn install_branch_path_root_constant(&mut self, arena: &mut Arena) -> Result<(), CompileError> {
575        // Guard: micro-benchmarks and unit tests often build a VM
576        // without calling `register_service_types` first. When the
577        // BranchPath arena type is absent, there's nothing Oracle-
578        // related in the program and skipping the install is safe.
579        let Some(type_id) = arena.find_type_id(crate::types::branch_path::TYPE_NAME) else {
580            return Ok(());
581        };
582        let dewey = crate::nan_value::NanValue::new_string_value("", arena);
583        let record_idx = arena.push_record(type_id, vec![dewey]);
584        let root_value = crate::nan_value::NanValue::new_record(record_idx);
585        self.symbols
586            .intern_constant("BranchPath.Root", root_value)?;
587        let namespace_symbol_id = self.symbols.intern_namespace_path("BranchPath")?;
588        let member_symbol_id = self.symbols.intern_name("Root");
589        self.symbols.add_namespace_member_by_id(
590            namespace_symbol_id,
591            member_symbol_id,
592            root_value,
593        )?;
594        Ok(())
595    }
596
597    fn ensure_global(&mut self, name: &str) -> u16 {
598        if let Some(&idx) = self.global_names.get(name) {
599            return idx;
600        }
601        let idx = self.globals.len() as u16;
602        self.global_names.insert(name.to_string(), idx);
603        self.globals.push(NanValue::UNIT);
604        idx
605    }
606
607    /// Register type symbols in VmSymbolTable for namespace resolution.
608    /// Arena registration is handled separately via shared `collect_module_types`.
609    fn register_type_in_symbols(
610        &mut self,
611        td: &TypeDef,
612        arena: &Arena,
613    ) -> Result<(), CompileError> {
614        match td {
615            TypeDef::Product { name, fields, .. } => {
616                self.symbols.intern_namespace_path(name)?;
617                let type_id = arena
618                    .find_type_id(name)
619                    .expect("type already registered in Arena");
620                let field_symbol_ids: Vec<u32> = fields
621                    .iter()
622                    .map(|(field_name, _)| self.symbols.intern_name(field_name))
623                    .collect();
624                self.code.register_record_fields(type_id, &field_symbol_ids);
625            }
626            TypeDef::Sum { name, variants, .. } => {
627                let type_symbol_id = self.symbols.intern_namespace_path(name)?;
628                let type_id = arena
629                    .find_type_id(name)
630                    .expect("type already registered in Arena");
631                for (variant_id, variant) in variants.iter().enumerate() {
632                    let ctor_id = arena
633                        .find_ctor_id(type_id, variant_id as u16)
634                        .expect("ctor id");
635                    let qualified_name = visibility::member_key(name, &variant.name);
636                    let ctor_symbol_id = self.symbols.intern_variant_ctor(
637                        &qualified_name,
638                        VmVariantCtor {
639                            type_id,
640                            variant_id: variant_id as u16,
641                            ctor_id,
642                            field_count: variant.fields.len() as u8,
643                        },
644                    )?;
645                    let member_symbol_id = self.symbols.intern_name(&variant.name);
646                    self.symbols.add_namespace_member_by_id(
647                        type_symbol_id,
648                        member_symbol_id,
649                        VmSymbolTable::symbol_ref(ctor_symbol_id),
650                    )?;
651                }
652            }
653        }
654        Ok(())
655    }
656
657    fn bootstrap_core_symbols(&mut self) -> Result<(), CompileError> {
658        for builtin in VmBuiltin::ALL.iter().copied() {
659            let builtin_symbol_id = self.symbols.intern_builtin(builtin)?;
660            if let Some((namespace, member)) = builtin.name().split_once('.') {
661                let namespace_symbol_id = self.symbols.intern_namespace_path(namespace)?;
662                let member_symbol_id = self.symbols.intern_name(member);
663                self.symbols.add_namespace_member_by_id(
664                    namespace_symbol_id,
665                    member_symbol_id,
666                    VmSymbolTable::symbol_ref(builtin_symbol_id),
667                )?;
668            }
669        }
670
671        let result_symbol_id = self.symbols.intern_namespace_path("Result")?;
672        let ok_symbol_id = self.symbols.intern_wrapper("Result.Ok", 0)?;
673        let err_symbol_id = self.symbols.intern_wrapper("Result.Err", 1)?;
674        let ok_member_symbol_id = self.symbols.intern_name("Ok");
675        self.symbols.add_namespace_member_by_id(
676            result_symbol_id,
677            ok_member_symbol_id,
678            VmSymbolTable::symbol_ref(ok_symbol_id),
679        )?;
680        let err_member_symbol_id = self.symbols.intern_name("Err");
681        self.symbols.add_namespace_member_by_id(
682            result_symbol_id,
683            err_member_symbol_id,
684            VmSymbolTable::symbol_ref(err_symbol_id),
685        )?;
686        for (member, builtin_name) in result::extra_members() {
687            if let Some(symbol_id) = self.symbols.find(&builtin_name) {
688                let member_symbol_id = self.symbols.intern_name(member);
689                self.symbols.add_namespace_member_by_id(
690                    result_symbol_id,
691                    member_symbol_id,
692                    VmSymbolTable::symbol_ref(symbol_id),
693                )?;
694            }
695        }
696
697        let option_symbol_id = self.symbols.intern_namespace_path("Option")?;
698        let some_symbol_id = self.symbols.intern_wrapper("Option.Some", 2)?;
699        self.symbols
700            .intern_constant("Option.None", NanValue::NONE)?;
701        let some_member_symbol_id = self.symbols.intern_name("Some");
702        self.symbols.add_namespace_member_by_id(
703            option_symbol_id,
704            some_member_symbol_id,
705            VmSymbolTable::symbol_ref(some_symbol_id),
706        )?;
707        let none_member_symbol_id = self.symbols.intern_name("None");
708        self.symbols.add_namespace_member_by_id(
709            option_symbol_id,
710            none_member_symbol_id,
711            NanValue::NONE,
712        )?;
713        for (member, builtin_name) in option::extra_members() {
714            if let Some(symbol_id) = self.symbols.find(&builtin_name) {
715                let member_symbol_id = self.symbols.intern_name(member);
716                self.symbols.add_namespace_member_by_id(
717                    option_symbol_id,
718                    member_symbol_id,
719                    VmSymbolTable::symbol_ref(symbol_id),
720                )?;
721            }
722        }
723        Ok(())
724    }
725
726    /// Phase 4b: emit a fn's bytecode by walking the MIR body
727    /// instead of the HIR body. Mirrors `compile_fn_with_scope`'s
728    /// `FnCompiler` setup exactly — same arity / local_count /
729    /// effects / aliased slots — so the resulting `FnChunk` is
730    /// drop-in for the HIR-emitted version when the MIR walker
731    /// covers the body shape.
732    fn compile_fn_via_mir(
733        &mut self,
734        rfd: &ResolvedFnDef,
735        mir_fn: &crate::ir::mir::MirFn,
736        symbols: &SymbolTable,
737        arena: &mut Arena,
738        module_scope: &HashMap<String, u32>,
739        mir_program: &crate::ir::mir::MirProgram,
740    ) -> Result<FnChunk, mir::MirVmUnsupported> {
741        let resolution = rfd.resolution.as_ref();
742        // The MIR body may mint synthetic slots past the resolver's
743        // `local_count` (opaque-let temps for effectful intermediates),
744        // so trust the MIR fn's own count — the resolver's is a lower
745        // bound and underruns the frame on `STORE_LOCAL` to a temp.
746        let local_count = mir_fn
747            .local_count
748            .max(resolution.map_or(rfd.params.len() as u32, |r| u32::from(r.local_count)))
749            as u16;
750        let local_slots: HashMap<String, u16> = resolution
751            .map(|r| r.local_slots.as_ref().clone())
752            .unwrap_or_else(|| {
753                rfd.params
754                    .iter()
755                    .enumerate()
756                    .map(|(i, (name, _))| (name.clone(), i as u16))
757                    .collect()
758            });
759
760        let mut fc = FnCompiler::new(
761            &rfd.name,
762            rfd.params.len() as u8,
763            local_count,
764            rfd.effects
765                .iter()
766                .map(|effect| self.symbols.intern_name(&effect.node))
767                .collect(),
768            local_slots,
769            &self.global_names,
770            module_scope,
771            &self.code,
772            &mut self.symbols,
773            arena,
774            symbols,
775            Some(mir_program),
776        );
777        fc.source_file = self.source_file.clone();
778        fc.note_line(rfd.line);
779        // Alias facts now ride the MIR fn (cloned from the resolver at
780        // lowering) rather than the AST `FnResolution` side-channel —
781        // the VM reads aliasedness off MIR like every other MIR-sourced
782        // fact. Identical bits today; the move lets the analysis
783        // re-home into a MIR pass later without touching this site.
784        fc.set_aliased_slots(mir_fn.aliased_slots.clone());
785
786        mir::compile_mir_fn_body(&mut fc, mir_fn)?;
787        Ok(fc.finish())
788    }
789
790    fn compile_top_level(
791        &mut self,
792        items: &[ResolvedTopLevel],
793        symbols: &SymbolTable,
794        arena: &mut Arena,
795        mir_program: &crate::ir::mir::MirProgram,
796    ) -> Result<(), CompileError> {
797        let has_stmts = items
798            .iter()
799            .any(|i| matches!(i, ResolvedTopLevel::Passthrough(TopLevel::Stmt(_))));
800        if !has_stmts {
801            return Ok(());
802        }
803
804        for item in items {
805            if let ResolvedTopLevel::Passthrough(TopLevel::Stmt(Stmt::Binding(name, _, _))) = item {
806                self.ensure_global(name);
807            }
808        }
809
810        // Top-level statements never went through the resolver pass
811        // (Phase E lifts `FnDef` bodies but leaves `TopLevel::Stmt`
812        // as passthrough). Resolve them here against the entry's
813        // symbol table.
814        let resolver_ctx = crate::ir::hir::ResolveCtx::new(symbols);
815        let resolved: Vec<ResolvedStmt> = items
816            .iter()
817            .filter_map(|i| match i {
818                ResolvedTopLevel::Passthrough(TopLevel::Stmt(stmt)) => {
819                    Some(resolve_stmt_for_top_level(&resolver_ctx, stmt))
820                }
821                _ => None,
822            })
823            .collect();
824
825        // Each statement stores its value to a global (`Binding`) or pops
826        // it (`Expr`); compute the store targets up front so the borrow
827        // of `self.global_names` doesn't collide with the `FnCompiler`.
828        let store_targets: Vec<Option<u16>> = resolved
829            .iter()
830            .map(|rs| match rs {
831                ResolvedStmt::Binding { name, .. } => Some(self.global_names[name.as_str()]),
832                ResolvedStmt::Expr(_) => None,
833            })
834            .collect();
835
836        // Lower every value expression (the builtin / instantiation
837        // tables grow on a clone of the entry program so the BuiltinId /
838        // FnId references match the walker's program). The walker emits
839        // the value; the `STORE_GLOBAL` / `POP` is emitted here, so no
840        // MIR-level global-binding node is needed. MIR is the only VM
841        // codegen path — a statement outside the lowerable subset (only
842        // reachable on malformed / typecheck-rejected input) is a hard
843        // CompileError, checked before any bytecode is emitted.
844        let mut prog = mir_program.clone();
845        let lowered: Vec<crate::ast::Spanned<crate::ir::mir::MirExpr>> = resolved
846            .iter()
847            .map(|rs| {
848                let value = match rs {
849                    ResolvedStmt::Binding { value, .. } | ResolvedStmt::Expr(value) => value,
850                };
851                crate::ir::mir::lower_top_level_value(value, &mut prog).map_err(|reason| {
852                    CompileError {
853                        msg: format!(
854                            "internal error: a top-level statement did not lower to MIR ({reason:?})"
855                        ),
856                    }
857                })
858            })
859            .collect::<Result<_, _>>()?;
860        if let Some(bad) = lowered.iter().find(|low| !mir::mir_expr_compilable(low)) {
861            return Err(CompileError {
862                msg: format!(
863                    "internal error: a top-level statement is outside the VM backend subset: {:?}",
864                    bad.node
865                ),
866            });
867        }
868
869        let empty_mod_scope = HashMap::new();
870        let mut fc = FnCompiler::new(
871            "__top_level__",
872            0,
873            0,
874            Vec::new(),
875            HashMap::new(),
876            &self.global_names,
877            &empty_mod_scope,
878            &self.code,
879            &mut self.symbols,
880            arena,
881            symbols,
882            Some(&prog),
883        );
884
885        for (idx, low) in lowered.iter().enumerate() {
886            mir::compile_mir_expr(&mut fc, low)
887                .map_err(|e| e.into_compile_error("a top-level statement"))?;
888            match store_targets[idx] {
889                Some(global_idx) => {
890                    fc.emit_op(STORE_GLOBAL);
891                    fc.emit_u16(global_idx);
892                }
893                None => fc.emit_op(POP),
894            }
895        }
896
897        fc.emit_op(LOAD_UNIT);
898        fc.emit_op(RETURN);
899
900        let chunk = fc.finish();
901        self.code.add_function(chunk);
902        Ok(())
903    }
904
905    fn register_current_module_namespace(
906        &mut self,
907        items: &[ResolvedTopLevel],
908    ) -> Result<(), CompileError> {
909        let Some(module) = items.iter().find_map(|item| match item {
910            ResolvedTopLevel::Module(module) => Some(module),
911            _ => None,
912        }) else {
913            return Ok(());
914        };
915
916        let module_symbol_id = self.symbols.intern_namespace_path(&module.name)?;
917        let exposes_ref = if module.exposes.is_empty() {
918            None
919        } else {
920            Some(module.exposes.as_slice())
921        };
922
923        for item in items {
924            match item {
925                ResolvedTopLevel::FnDef(rfd) => {
926                    if visibility::is_exposed(&rfd.name, exposes_ref)
927                        && let Some(symbol_id) = self.symbols.find(&rfd.name)
928                    {
929                        let member_symbol_id = self.symbols.intern_name(&rfd.name);
930                        self.symbols.add_namespace_member_by_id(
931                            module_symbol_id,
932                            member_symbol_id,
933                            VmSymbolTable::symbol_ref(symbol_id),
934                        )?;
935                    }
936                }
937                ResolvedTopLevel::Passthrough(TopLevel::TypeDef(
938                    TypeDef::Product { name, .. } | TypeDef::Sum { name, .. },
939                )) => {
940                    if visibility::is_exposed(name, exposes_ref)
941                        && let Some(symbol_id) = self.symbols.find(name)
942                    {
943                        let member_symbol_id = self.symbols.intern_name(name);
944                        self.symbols.add_namespace_member_by_id(
945                            module_symbol_id,
946                            member_symbol_id,
947                            VmSymbolTable::symbol_ref(symbol_id),
948                        )?;
949                    }
950                }
951                _ => {}
952            }
953        }
954        Ok(())
955    }
956}
957
958fn resolve_stmt_for_top_level(ctx: &crate::ir::hir::ResolveCtx<'_>, stmt: &Stmt) -> ResolvedStmt {
959    crate::ir::hir::resolve::resolve_stmt_external(ctx, stmt)
960}
961
962/// What a function expression resolves to at compile time.
963pub(super) struct FnCompiler<'a> {
964    name: String,
965    arity: u8,
966    local_count: u16,
967    effects: Vec<u32>,
968    pub(super) local_slots: HashMap<String, u16>,
969    global_names: &'a HashMap<String, u16>,
970    /// Module-local function scope: simple_name → fn_id.
971    /// Used for intra-module calls (e.g. `placeStairs` inside map.av).
972    module_scope: &'a HashMap<String, u32>,
973    pub(super) code_store: &'a CodeStore,
974    pub(super) symbols: &'a mut VmSymbolTable,
975    pub(super) arena: &'a mut Arena,
976    /// Resolved-identity table for the current compilation scope. Used
977    /// to map [`crate::ir::hir::ResolvedCallee::Fn`] and `ResolvedCtor::User`
978    /// references back to their source-level canonical names so the VM
979    /// can dispatch through `code_store.find` / arena lookups by name.
980    ///
981    /// Entry fns use the entry's `SymbolTable`. Dep fns (compiled via
982    /// `integrate_module`) use a per-dep `SymbolTable` built off the
983    /// dep's own items — keeps each compilation scope's `FnId` space
984    /// self-consistent without forcing the caller to pre-merge.
985    pub(super) symbol_table: &'a SymbolTable,
986    /// Phase 6 wave 11 — the lowered MIR for the current program,
987    /// when this `FnCompiler` is running on the MIR walker path
988    /// (`compile_program_with_mir_fallback`). Used by the walker
989    /// to resolve `MirCallee::Builtin(BuiltinId)` back to the
990    /// canonical name the VM builtin table keys on. `None` on the
991    /// HIR-only path.
992    pub(super) mir_program: Option<&'a crate::ir::mir::MirProgram>,
993    code: Vec<u8>,
994    constants: Vec<NanValue>,
995    /// Byte offset of the last emitted opcode (for superinstruction fusion).
996    last_op_pos: usize,
997    /// Source file path for this function.
998    source_file: String,
999    /// Run-length encoded line table being built: (bytecode_offset, source_line).
1000    line_table: Vec<(u16, u16)>,
1001    /// Last emitted line (for RLE dedup).
1002    last_noted_line: u16,
1003    /// Snapshot of `FnResolution.aliased_slots` for the current fn.
1004    /// Stamped per slot by the IR `alias` pass; backends consume it
1005    /// rather than re-deriving the same shape per fn. Empty when the
1006    /// fn was compiled outside the standard pipeline (REPL with no
1007    /// last-use phase, partial integrations) — the safe-but-slow
1008    /// reading is "every slot might be aliased" but the VM defaults
1009    /// to the legacy "everyone owned" behaviour for backwards
1010    /// compatibility; the alias pass always runs in real builds.
1011    aliased_slots: std::sync::Arc<Vec<bool>>,
1012}
1013
1014impl<'a> FnCompiler<'a> {
1015    #[allow(clippy::too_many_arguments)]
1016    fn new(
1017        name: &str,
1018        arity: u8,
1019        local_count: u16,
1020        effects: Vec<u32>,
1021        local_slots: HashMap<String, u16>,
1022        global_names: &'a HashMap<String, u16>,
1023        module_scope: &'a HashMap<String, u32>,
1024        code_store: &'a CodeStore,
1025        symbols: &'a mut VmSymbolTable,
1026        arena: &'a mut Arena,
1027        symbol_table: &'a SymbolTable,
1028        mir_program: Option<&'a crate::ir::mir::MirProgram>,
1029    ) -> Self {
1030        FnCompiler {
1031            name: name.to_string(),
1032            arity,
1033            local_count,
1034            effects,
1035            local_slots,
1036            global_names,
1037            module_scope,
1038            code_store,
1039            symbols,
1040            arena,
1041            symbol_table,
1042            mir_program,
1043            code: Vec::new(),
1044            constants: Vec::new(),
1045            last_op_pos: usize::MAX,
1046            source_file: String::new(),
1047            line_table: Vec::new(),
1048            last_noted_line: 0,
1049            aliased_slots: std::sync::Arc::new(Vec::new()),
1050        }
1051    }
1052
1053    fn set_aliased_slots(&mut self, aliased: std::sync::Arc<Vec<bool>>) {
1054        self.aliased_slots = aliased;
1055    }
1056
1057    pub(super) fn is_aliased_slot(&self, slot: u16) -> bool {
1058        self.aliased_slots
1059            .get(slot as usize)
1060            .copied()
1061            .unwrap_or(false)
1062    }
1063
1064    pub(super) fn name(&self) -> &str {
1065        &self.name
1066    }
1067
1068    pub(super) fn global_names(&self) -> &HashMap<String, u16> {
1069        self.global_names
1070    }
1071
1072    pub(super) fn module_scope(&self) -> &HashMap<String, u32> {
1073        self.module_scope
1074    }
1075
1076    fn finish(self) -> FnChunk {
1077        FnChunk {
1078            name: self.name,
1079            arity: self.arity,
1080            local_count: self.local_count,
1081            code: self.code,
1082            constants: self.constants,
1083            effects: self.effects,
1084            thin: false,
1085            parent_thin: false,
1086            leaf: false,
1087            no_alloc: false,
1088            source_file: self.source_file,
1089            line_table: self.line_table,
1090        }
1091    }
1092
1093    /// Record that bytecode emitted from this point forward corresponds to
1094    /// the given source line. RLE-deduplicated: consecutive calls with the
1095    /// same line produce only one entry.
1096    pub(super) fn note_line(&mut self, line: usize) {
1097        if line == 0 {
1098            return;
1099        }
1100        let line16 = line as u16;
1101        if line16 == self.last_noted_line {
1102            return; // RLE dedup
1103        }
1104        self.last_noted_line = line16;
1105        self.line_table.push((self.code.len() as u16, line16));
1106    }
1107
1108    pub(super) fn emit_op(&mut self, op: u8) {
1109        let prev_pos = self.last_op_pos;
1110        let prev_op = if prev_pos < self.code.len() {
1111            self.code[prev_pos]
1112        } else {
1113            0xFF
1114        };
1115
1116        // LOAD_LOCAL + LOAD_LOCAL → LOAD_LOCAL_2
1117        if op == LOAD_LOCAL && prev_op == LOAD_LOCAL && prev_pos + 2 == self.code.len() {
1118            self.code[prev_pos] = LOAD_LOCAL_2;
1119            // slot_a already at prev_pos+1, slot_b emitted next via emit_u8
1120            return;
1121        }
1122        // LOAD_LOCAL + LOAD_CONST → LOAD_LOCAL_CONST
1123        if op == LOAD_CONST && prev_op == LOAD_LOCAL && prev_pos + 2 == self.code.len() {
1124            self.code[prev_pos] = LOAD_LOCAL_CONST;
1125            // slot at prev_pos+1, const_idx (u16) emitted next via emit_u16
1126            return;
1127        }
1128        // VECTOR_GET + LOAD_CONST(hi,lo) + UNWRAP_OR → VECTOR_GET_OR(hi,lo)
1129        // Before: [..., VECTOR_GET, LOAD_CONST, hi, lo] + about to emit UNWRAP_OR
1130        // After:  [..., VECTOR_GET_OR, hi, lo]
1131        if op == UNWRAP_OR && self.code.len() >= 4 {
1132            let len = self.code.len();
1133            if self.code[len - 4] == VECTOR_GET && self.code[len - 3] == LOAD_CONST {
1134                let hi = self.code[len - 2];
1135                let lo = self.code[len - 1];
1136                self.code[len - 4] = VECTOR_GET_OR;
1137                self.code[len - 3] = hi;
1138                self.code[len - 2] = lo;
1139                self.code.pop(); // remove extra byte
1140                self.last_op_pos = len - 4;
1141                return;
1142            }
1143        }
1144        self.last_op_pos = self.code.len();
1145        self.code.push(op);
1146    }
1147
1148    pub(super) fn emit_u8(&mut self, val: u8) {
1149        self.code.push(val);
1150    }
1151
1152    pub(super) fn emit_u16(&mut self, val: u16) {
1153        self.code.push((val >> 8) as u8);
1154        self.code.push((val & 0xFF) as u8);
1155    }
1156
1157    pub(super) fn emit_i16(&mut self, val: i16) {
1158        self.emit_u16(val as u16);
1159    }
1160
1161    pub(super) fn emit_u32(&mut self, val: u32) {
1162        self.code.push((val >> 24) as u8);
1163        self.code.push(((val >> 16) & 0xFF) as u8);
1164        self.code.push(((val >> 8) & 0xFF) as u8);
1165        self.code.push((val & 0xFF) as u8);
1166    }
1167
1168    pub(super) fn emit_u64(&mut self, val: u64) {
1169        self.code.extend_from_slice(&val.to_be_bytes());
1170    }
1171
1172    pub(super) fn emit_i64(&mut self, val: i64) {
1173        self.code.extend_from_slice(&val.to_be_bytes());
1174    }
1175
1176    pub(super) fn add_constant(&mut self, val: NanValue) -> u16 {
1177        for (i, c) in self.constants.iter().enumerate() {
1178            if c.bits() == val.bits() {
1179                return i as u16;
1180            }
1181        }
1182        let idx = self.constants.len() as u16;
1183        self.constants.push(val);
1184        idx
1185    }
1186
1187    pub(super) fn offset(&self) -> usize {
1188        self.code.len()
1189    }
1190
1191    pub(super) fn code_mut(&mut self) -> &mut Vec<u8> {
1192        &mut self.code
1193    }
1194
1195    pub(super) fn emit_jump(&mut self, op: u8) -> usize {
1196        self.emit_op(op);
1197        let patch_pos = self.code.len();
1198        self.emit_i16(0);
1199        patch_pos
1200    }
1201
1202    pub(super) fn patch_jump(&mut self, patch_pos: usize) {
1203        let target = self.code.len();
1204        let offset = (target as isize - patch_pos as isize - 2) as i16;
1205        let bytes = (offset as u16).to_be_bytes();
1206        self.code[patch_pos] = bytes[0];
1207        self.code[patch_pos + 1] = bytes[1];
1208    }
1209
1210    pub(super) fn patch_jump_to(&mut self, patch_pos: usize, target: usize) {
1211        let offset = (target as isize - patch_pos as isize - 2) as i16;
1212        let bytes = (offset as u16).to_be_bytes();
1213        self.code[patch_pos] = bytes[0];
1214        self.code[patch_pos + 1] = bytes[1];
1215    }
1216}
1217
1218#[cfg(test)]
1219mod tests {
1220    use super::compile_program_with_mir_fallback;
1221    use crate::ir::SymbolTable;
1222    use crate::ir::hir::resolve_program;
1223    use crate::nan_value::Arena;
1224    use crate::source::parse_source;
1225    use crate::vm::opcode::{LT, VECTOR_GET_OR, VECTOR_SET_OR_KEEP};
1226
1227    /// Mirror of the pre-Phase-E test helper: tco + slot-resolve +
1228    /// resolved-HIR lift, no typecheck. Matches the original
1229    /// `compile_program` callsites that exercised the bytecode-emit
1230    /// path in isolation — keeping the "no `LT_INT` because spans
1231    /// aren't typed" assumption alive so the byte-shape assertions
1232    /// don't get nudged by typed-opcode promotion.
1233    fn compile_via_pipeline(source: &str) -> crate::vm::CodeStore {
1234        let mut items = parse_source(source).expect("source should parse");
1235        crate::ir::pipeline::tco(&mut items);
1236        crate::ir::pipeline::resolve(&mut items);
1237        let symbols = SymbolTable::build(&items, &[]);
1238        let resolved = resolve_program(&symbols, &items);
1239        let mut arena = Arena::new();
1240        let (code, _globals) =
1241            compile_program_with_mir_fallback(&resolved, &symbols, &mut arena, None)
1242                .expect("vm compile should pass");
1243        code
1244    }
1245
1246    #[test]
1247    fn vector_get_with_literal_default_lowers_to_vector_get_or() {
1248        let source = r#"
1249module Demo
1250
1251fn cellAt(grid: Vector<Int>, idx: Int) -> Int
1252    Option.withDefault(Vector.get(grid, idx), 0)
1253"#;
1254
1255        let code = compile_via_pipeline(source);
1256        let fn_id = code.find("cellAt").expect("cellAt should exist");
1257        let chunk = code.get(fn_id);
1258
1259        assert!(
1260            chunk.code.contains(&VECTOR_GET_OR),
1261            "expected VECTOR_GET_OR in bytecode, got {:?}",
1262            chunk.code
1263        );
1264    }
1265
1266    #[test]
1267    fn vector_set_with_same_default_lowers_to_vector_set_or_keep() {
1268        let source = r#"
1269module Demo
1270
1271fn updateOrKeep(vec: Vector<Int>, idx: Int, value: Int) -> Vector<Int>
1272    Option.withDefault(Vector.set(vec, idx, value), vec)
1273"#;
1274
1275        let code = compile_via_pipeline(source);
1276        let fn_id = code
1277            .find("updateOrKeep")
1278            .expect("updateOrKeep should exist");
1279        let chunk = code.get(fn_id);
1280
1281        assert!(
1282            chunk.code.contains(&VECTOR_SET_OR_KEEP),
1283            "expected VECTOR_SET_OR_KEEP in bytecode, got {:?}",
1284            chunk.code
1285        );
1286    }
1287
1288    #[test]
1289    fn bool_match_on_gte_uses_base_compare_without_not() {
1290        let source = r#"
1291module Demo
1292
1293fn bucket(n: Int) -> Int
1294    match n >= 10
1295        true -> 7
1296        false -> 3
1297"#;
1298
1299        let code = compile_via_pipeline(source);
1300        let fn_id = code.find("bucket").expect("bucket should exist");
1301        let chunk = code.get(fn_id);
1302
1303        assert!(
1304            chunk.code.contains(&LT),
1305            "expected the base integer compare LT in bytecode, got {:?}",
1306            chunk.code
1307        );
1308    }
1309
1310    #[test]
1311    fn self_host_runtime_http_server_aliases_compile_in_vm() {
1312        let source = r#"
1313module Demo
1314
1315fn listen(handler: Int) -> Unit
1316    SelfHostRuntime.httpServerListen(8080, handler)
1317
1318fn listenWith(context: Int, handler: Int) -> Unit
1319    SelfHostRuntime.httpServerListenWith(8081, context, handler)
1320"#;
1321
1322        let code = compile_via_pipeline(source);
1323        assert!(code.find("listen").is_some(), "listen should compile");
1324        assert!(
1325            code.find("listenWith").is_some(),
1326            "listenWith should compile"
1327        );
1328    }
1329}