Skip to main content

aver/vm/compiler/
mod.rs

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