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