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
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        compiler.bootstrap_core_symbols();
271        compiler
272    }
273
274    fn sync_record_field_symbols(&mut self, arena: &Arena) {
275        for type_id in 0..arena.type_count() {
276            let type_name = arena.get_type_name(type_id);
277            self.symbols.intern_namespace_path(type_name);
278            let field_names = arena.get_field_names(type_id);
279            if field_names.is_empty() {
280                continue;
281            }
282            let field_symbol_ids: Vec<u32> = field_names
283                .iter()
284                .map(|field_name| self.symbols.intern_name(field_name))
285                .collect();
286            self.code.register_record_fields(type_id, &field_symbol_ids);
287        }
288    }
289
290    /// Load all modules from `depends [...]` declarations using the shared loader,
291    /// then compile each module's functions and register symbols.
292    fn load_modules(
293        &mut self,
294        items: &[TopLevel],
295        module_root: &str,
296        arena: &mut Arena,
297    ) -> Result<(), CompileError> {
298        let module = items.iter().find_map(|i| {
299            if let TopLevel::Module(m) = i {
300                Some(m)
301            } else {
302                None
303            }
304        });
305        let module = match module {
306            Some(m) => m,
307            None => return Ok(()),
308        };
309
310        let modules = crate::source::load_module_tree(&module.depends, module_root)
311            .map_err(|e| CompileError { msg: e })?;
312
313        for loaded in modules {
314            self.integrate_module(&loaded.dep_name, loaded.items, arena)?;
315        }
316        Ok(())
317    }
318
319    /// Integrate a loaded module into the VM: register types, compile functions,
320    /// expose symbols.
321    fn integrate_module(
322        &mut self,
323        dep_name: &str,
324        mut mod_items: Vec<TopLevel>,
325        arena: &mut Arena,
326    ) -> Result<(), CompileError> {
327        // Internal VM dep-loading: TCO + resolver only. Caller already ran
328        // the full canonical pipeline on the entry; this path runs on
329        // freshly parsed dep items that are otherwise unprepared. Idempotent
330        // with `load_module_recursive`'s pipeline call when both touch the
331        // same module.
332        crate::ir::pipeline::tco(&mut mod_items);
333        crate::ir::pipeline::resolve(&mut mod_items);
334
335        // Register types in Arena with qualified aliases.
336        for mt in visibility::collect_module_types(&mod_items) {
337            let type_id = match &mt.kind {
338                visibility::ModuleTypeKind::Record { field_names } => {
339                    arena.register_record_type(&mt.bare_name, field_names.clone())
340                }
341                visibility::ModuleTypeKind::Sum { variant_names } => {
342                    arena.register_sum_type(&mt.bare_name, variant_names.clone())
343                }
344            };
345            arena.register_type_alias(
346                &visibility::qualified_name(dep_name, &mt.bare_name),
347                type_id,
348            );
349        }
350        for item in &mod_items {
351            if let TopLevel::TypeDef(td) = item {
352                self.register_type_in_symbols(td, arena);
353            }
354        }
355
356        // Compile ALL functions (not just exposed).
357        let mut module_fn_ids: Vec<(String, u32)> = Vec::new();
358        for item in &mod_items {
359            if let TopLevel::FnDef(fndef) = item {
360                let qualified_name = visibility::qualified_name(dep_name, &fndef.name);
361                let effect_ids: Vec<u32> = fndef
362                    .effects
363                    .iter()
364                    .map(|effect| self.symbols.intern_name(&effect.node))
365                    .collect();
366                let fn_id = self.code.add_function(FnChunk {
367                    name: qualified_name.clone(),
368                    arity: fndef.params.len() as u8,
369                    local_count: 0,
370                    code: Vec::new(),
371                    constants: Vec::new(),
372                    effects: effect_ids,
373                    thin: false,
374                    parent_thin: false,
375                    leaf: false,
376                    no_alloc: false,
377                    source_file: String::new(),
378                    line_table: Vec::new(),
379                });
380                self.symbols.intern_function(
381                    &qualified_name,
382                    fn_id,
383                    &fndef
384                        .effects
385                        .iter()
386                        .map(|e| e.node.clone())
387                        .collect::<Vec<_>>(),
388                );
389                module_fn_ids.push((fndef.name.clone(), fn_id));
390            }
391        }
392
393        let module_scope: HashMap<String, u32> = module_fn_ids.iter().cloned().collect();
394        let mut fn_idx = 0;
395        for item in &mod_items {
396            if let TopLevel::FnDef(fndef) = item {
397                let (fn_name, fn_id) = &module_fn_ids[fn_idx];
398                let mut chunk = self.compile_fn_with_scope(fndef, arena, &module_scope)?;
399                chunk.name = visibility::qualified_name(dep_name, fn_name);
400                self.code.functions[*fn_id as usize] = chunk;
401                fn_idx += 1;
402            }
403        }
404
405        // Expose exported functions and types via globals and namespace members.
406        let exports = visibility::collect_module_exports(&mod_items);
407
408        for fd in &exports.functions {
409            let qualified = visibility::qualified_name(dep_name, &fd.name);
410            let global_idx = self.ensure_global(&qualified);
411            let symbol_id = self.symbols.find(&qualified).ok_or_else(|| CompileError {
412                msg: format!("missing VM symbol for exposed function {}", qualified),
413            })?;
414            self.globals[global_idx as usize] = VmSymbolTable::symbol_ref(symbol_id);
415        }
416
417        let module_symbol_id = self.symbols.intern_namespace_path(dep_name);
418        for et in &exports.types {
419            let type_name = match et.def {
420                TypeDef::Sum { name, .. } | TypeDef::Product { name, .. } => name,
421            };
422            if let Some(type_symbol_id) = self.symbols.find(type_name) {
423                let member_symbol_id = self.symbols.intern_name(type_name);
424                self.symbols.add_namespace_member_by_id(
425                    module_symbol_id,
426                    member_symbol_id,
427                    VmSymbolTable::symbol_ref(type_symbol_id),
428                );
429            }
430        }
431        for fd in &exports.functions {
432            let qualified = visibility::qualified_name(dep_name, &fd.name);
433            if let Some(fn_symbol_id) = self.symbols.find(&qualified) {
434                let member_symbol_id = self.symbols.intern_name(&fd.name);
435                self.symbols.add_namespace_member_by_id(
436                    module_symbol_id,
437                    member_symbol_id,
438                    VmSymbolTable::symbol_ref(fn_symbol_id),
439                );
440            }
441        }
442
443        Ok(())
444    }
445
446    /// Oracle v1: install `BranchPath.Root` as a nullary constant
447    /// member of the `BranchPath` namespace. The record is allocated
448    /// once in the arena; the symbol table holds a NanValue
449    /// referencing it. Follows the same pattern as `Option.None`
450    /// which is installed as an immediate constant in
451    /// `bootstrap_core_symbols`.
452    fn install_branch_path_root_constant(&mut self, arena: &mut Arena) {
453        // Guard: micro-benchmarks and unit tests often build a VM
454        // without calling `register_service_types` first. When the
455        // BranchPath arena type is absent, there's nothing Oracle-
456        // related in the program and skipping the install is safe.
457        let Some(type_id) = arena.find_type_id(crate::types::branch_path::TYPE_NAME) else {
458            return;
459        };
460        let dewey = crate::nan_value::NanValue::new_string_value("", arena);
461        let record_idx = arena.push_record(type_id, vec![dewey]);
462        let root_value = crate::nan_value::NanValue::new_record(record_idx);
463        self.symbols.intern_constant("BranchPath.Root", root_value);
464        let namespace_symbol_id = self.symbols.intern_namespace_path("BranchPath");
465        let member_symbol_id = self.symbols.intern_name("Root");
466        self.symbols
467            .add_namespace_member_by_id(namespace_symbol_id, member_symbol_id, root_value);
468    }
469
470    fn ensure_global(&mut self, name: &str) -> u16 {
471        if let Some(&idx) = self.global_names.get(name) {
472            return idx;
473        }
474        let idx = self.globals.len() as u16;
475        self.global_names.insert(name.to_string(), idx);
476        self.globals.push(NanValue::UNIT);
477        idx
478    }
479
480    /// Register type symbols in VmSymbolTable for namespace resolution.
481    /// Arena registration is handled separately via shared `collect_module_types`.
482    fn register_type_in_symbols(&mut self, td: &TypeDef, arena: &Arena) {
483        match td {
484            TypeDef::Product { name, fields, .. } => {
485                self.symbols.intern_namespace_path(name);
486                let type_id = arena
487                    .find_type_id(name)
488                    .expect("type already registered in Arena");
489                let field_symbol_ids: Vec<u32> = fields
490                    .iter()
491                    .map(|(field_name, _)| self.symbols.intern_name(field_name))
492                    .collect();
493                self.code.register_record_fields(type_id, &field_symbol_ids);
494            }
495            TypeDef::Sum { name, variants, .. } => {
496                let type_symbol_id = self.symbols.intern_namespace_path(name);
497                let type_id = arena
498                    .find_type_id(name)
499                    .expect("type already registered in Arena");
500                for (variant_id, variant) in variants.iter().enumerate() {
501                    let ctor_id = arena
502                        .find_ctor_id(type_id, variant_id as u16)
503                        .expect("ctor id");
504                    let qualified_name = visibility::member_key(name, &variant.name);
505                    let ctor_symbol_id = self.symbols.intern_variant_ctor(
506                        &qualified_name,
507                        VmVariantCtor {
508                            type_id,
509                            variant_id: variant_id as u16,
510                            ctor_id,
511                            field_count: variant.fields.len() as u8,
512                        },
513                    );
514                    let member_symbol_id = self.symbols.intern_name(&variant.name);
515                    self.symbols.add_namespace_member_by_id(
516                        type_symbol_id,
517                        member_symbol_id,
518                        VmSymbolTable::symbol_ref(ctor_symbol_id),
519                    );
520                }
521            }
522        }
523    }
524
525    fn bootstrap_core_symbols(&mut self) {
526        for builtin in VmBuiltin::ALL.iter().copied() {
527            let builtin_symbol_id = self.symbols.intern_builtin(builtin);
528            if let Some((namespace, member)) = builtin.name().split_once('.') {
529                let namespace_symbol_id = self.symbols.intern_namespace_path(namespace);
530                let member_symbol_id = self.symbols.intern_name(member);
531                self.symbols.add_namespace_member_by_id(
532                    namespace_symbol_id,
533                    member_symbol_id,
534                    VmSymbolTable::symbol_ref(builtin_symbol_id),
535                );
536            }
537        }
538
539        let result_symbol_id = self.symbols.intern_namespace_path("Result");
540        let ok_symbol_id = self.symbols.intern_wrapper("Result.Ok", 0);
541        let err_symbol_id = self.symbols.intern_wrapper("Result.Err", 1);
542        let ok_member_symbol_id = self.symbols.intern_name("Ok");
543        self.symbols.add_namespace_member_by_id(
544            result_symbol_id,
545            ok_member_symbol_id,
546            VmSymbolTable::symbol_ref(ok_symbol_id),
547        );
548        let err_member_symbol_id = self.symbols.intern_name("Err");
549        self.symbols.add_namespace_member_by_id(
550            result_symbol_id,
551            err_member_symbol_id,
552            VmSymbolTable::symbol_ref(err_symbol_id),
553        );
554        for (member, builtin_name) in result::extra_members() {
555            if let Some(symbol_id) = self.symbols.find(&builtin_name) {
556                let member_symbol_id = self.symbols.intern_name(member);
557                self.symbols.add_namespace_member_by_id(
558                    result_symbol_id,
559                    member_symbol_id,
560                    VmSymbolTable::symbol_ref(symbol_id),
561                );
562            }
563        }
564
565        let option_symbol_id = self.symbols.intern_namespace_path("Option");
566        let some_symbol_id = self.symbols.intern_wrapper("Option.Some", 2);
567        self.symbols.intern_constant("Option.None", NanValue::NONE);
568        let some_member_symbol_id = self.symbols.intern_name("Some");
569        self.symbols.add_namespace_member_by_id(
570            option_symbol_id,
571            some_member_symbol_id,
572            VmSymbolTable::symbol_ref(some_symbol_id),
573        );
574        let none_member_symbol_id = self.symbols.intern_name("None");
575        self.symbols.add_namespace_member_by_id(
576            option_symbol_id,
577            none_member_symbol_id,
578            NanValue::NONE,
579        );
580        for (member, builtin_name) in option::extra_members() {
581            if let Some(symbol_id) = self.symbols.find(&builtin_name) {
582                let member_symbol_id = self.symbols.intern_name(member);
583                self.symbols.add_namespace_member_by_id(
584                    option_symbol_id,
585                    member_symbol_id,
586                    VmSymbolTable::symbol_ref(symbol_id),
587                );
588            }
589        }
590    }
591
592    fn compile_fn(&mut self, fndef: &FnDef, arena: &mut Arena) -> Result<FnChunk, CompileError> {
593        let empty_scope = HashMap::new();
594        self.compile_fn_with_scope(fndef, arena, &empty_scope)
595    }
596
597    fn compile_fn_with_scope(
598        &mut self,
599        fndef: &FnDef,
600        arena: &mut Arena,
601        module_scope: &HashMap<String, u32>,
602    ) -> Result<FnChunk, CompileError> {
603        let resolution = fndef.resolution.as_ref();
604        let local_count = resolution.map_or(fndef.params.len() as u16, |r| r.local_count);
605        let local_slots: HashMap<String, u16> = resolution
606            .map(|r| r.local_slots.as_ref().clone())
607            .unwrap_or_else(|| {
608                fndef
609                    .params
610                    .iter()
611                    .enumerate()
612                    .map(|(i, (name, _))| (name.clone(), i as u16))
613                    .collect()
614            });
615
616        let mut fc = FnCompiler::new(
617            &fndef.name,
618            fndef.params.len() as u8,
619            local_count,
620            fndef
621                .effects
622                .iter()
623                .map(|effect| self.symbols.intern_name(&effect.node))
624                .collect(),
625            local_slots,
626            &self.global_names,
627            module_scope,
628            &self.code,
629            &mut self.symbols,
630            arena,
631        );
632        fc.source_file = self.source_file.clone();
633        fc.note_line(fndef.line);
634
635        match fndef.body.as_ref() {
636            FnBody::Block(stmts) => fc.compile_body(stmts)?,
637        }
638
639        Ok(fc.finish())
640    }
641
642    fn compile_top_level(
643        &mut self,
644        items: &[TopLevel],
645        arena: &mut Arena,
646    ) -> Result<(), CompileError> {
647        let has_stmts = items.iter().any(|i| matches!(i, TopLevel::Stmt(_)));
648        if !has_stmts {
649            return Ok(());
650        }
651
652        for item in items {
653            if let TopLevel::Stmt(Stmt::Binding(name, _, _)) = item {
654                self.ensure_global(name);
655            }
656        }
657
658        let empty_mod_scope = HashMap::new();
659        let mut fc = FnCompiler::new(
660            "__top_level__",
661            0,
662            0,
663            Vec::new(),
664            HashMap::new(),
665            &self.global_names,
666            &empty_mod_scope,
667            &self.code,
668            &mut self.symbols,
669            arena,
670        );
671
672        for item in items {
673            if let TopLevel::Stmt(stmt) = item {
674                match stmt {
675                    Stmt::Binding(name, _type_ann, expr) => {
676                        fc.compile_expr(expr)?;
677                        let idx = self.global_names[name.as_str()];
678                        fc.emit_op(STORE_GLOBAL);
679                        fc.emit_u16(idx);
680                    }
681                    Stmt::Expr(expr) => {
682                        fc.compile_expr(expr)?;
683                        fc.emit_op(POP);
684                    }
685                }
686            }
687        }
688
689        fc.emit_op(LOAD_UNIT);
690        fc.emit_op(RETURN);
691
692        let chunk = fc.finish();
693        self.code.add_function(chunk);
694        Ok(())
695    }
696
697    fn register_current_module_namespace(&mut self, items: &[TopLevel]) {
698        let Some(module) = items.iter().find_map(|item| {
699            if let TopLevel::Module(module) = item {
700                Some(module)
701            } else {
702                None
703            }
704        }) else {
705            return;
706        };
707
708        let module_symbol_id = self.symbols.intern_namespace_path(&module.name);
709        let exposes_ref = if module.exposes.is_empty() {
710            None
711        } else {
712            Some(module.exposes.as_slice())
713        };
714
715        for item in items {
716            match item {
717                TopLevel::FnDef(fndef) => {
718                    if visibility::is_exposed(&fndef.name, exposes_ref)
719                        && let Some(symbol_id) = self.symbols.find(&fndef.name)
720                    {
721                        let member_symbol_id = self.symbols.intern_name(&fndef.name);
722                        self.symbols.add_namespace_member_by_id(
723                            module_symbol_id,
724                            member_symbol_id,
725                            VmSymbolTable::symbol_ref(symbol_id),
726                        );
727                    }
728                }
729                TopLevel::TypeDef(TypeDef::Product { name, .. } | TypeDef::Sum { name, .. }) => {
730                    if visibility::is_exposed(name, exposes_ref)
731                        && let Some(symbol_id) = self.symbols.find(name)
732                    {
733                        let member_symbol_id = self.symbols.intern_name(name);
734                        self.symbols.add_namespace_member_by_id(
735                            module_symbol_id,
736                            member_symbol_id,
737                            VmSymbolTable::symbol_ref(symbol_id),
738                        );
739                    }
740                }
741                _ => {}
742            }
743        }
744    }
745}
746
747/// What a function expression resolves to at compile time.
748enum CallTarget {
749    /// Known function id (local or qualified module function).
750    KnownFn(u32),
751    /// Result.Ok / Result.Err / Option.Some → WRAP opcode. kind: 0=Ok, 1=Err, 2=Some.
752    Wrapper(u8),
753    /// Option.None → load constant.
754    None_,
755    /// User-defined variant constructor: Shape.Circle → VARIANT_NEW (or inline nullary at runtime).
756    Variant(u32, u16),
757    /// Known VM builtin/service resolved by name and interned into the VM symbol table.
758    Builtin(VmBuiltin),
759    /// Unknown capitalized dotted path that did not resolve to a function, variant, or builtin.
760    UnknownQualified(String),
761}
762
763struct FnCompiler<'a> {
764    name: String,
765    arity: u8,
766    local_count: u16,
767    effects: Vec<u32>,
768    local_slots: HashMap<String, u16>,
769    global_names: &'a HashMap<String, u16>,
770    /// Module-local function scope: simple_name → fn_id.
771    /// Used for intra-module calls (e.g. `placeStairs` inside map.av).
772    module_scope: &'a HashMap<String, u32>,
773    code_store: &'a CodeStore,
774    symbols: &'a mut VmSymbolTable,
775    arena: &'a mut Arena,
776    code: Vec<u8>,
777    constants: Vec<NanValue>,
778    /// Byte offset of the last emitted opcode (for superinstruction fusion).
779    last_op_pos: usize,
780    /// Source file path for this function.
781    source_file: String,
782    /// Run-length encoded line table being built: (bytecode_offset, source_line).
783    line_table: Vec<(u16, u16)>,
784    /// Last emitted line (for RLE dedup).
785    last_noted_line: u16,
786}
787
788impl<'a> FnCompiler<'a> {
789    #[allow(clippy::too_many_arguments)]
790    fn new(
791        name: &str,
792        arity: u8,
793        local_count: u16,
794        effects: Vec<u32>,
795        local_slots: HashMap<String, u16>,
796        global_names: &'a HashMap<String, u16>,
797        module_scope: &'a HashMap<String, u32>,
798        code_store: &'a CodeStore,
799        symbols: &'a mut VmSymbolTable,
800        arena: &'a mut Arena,
801    ) -> Self {
802        FnCompiler {
803            name: name.to_string(),
804            arity,
805            local_count,
806            effects,
807            local_slots,
808            global_names,
809            module_scope,
810            code_store,
811            symbols,
812            arena,
813            code: Vec::new(),
814            constants: Vec::new(),
815            last_op_pos: usize::MAX,
816            source_file: String::new(),
817            line_table: Vec::new(),
818            last_noted_line: 0,
819        }
820    }
821
822    fn finish(self) -> FnChunk {
823        FnChunk {
824            name: self.name,
825            arity: self.arity,
826            local_count: self.local_count,
827            code: self.code,
828            constants: self.constants,
829            effects: self.effects,
830            thin: false,
831            parent_thin: false,
832            leaf: false,
833            no_alloc: false,
834            source_file: self.source_file,
835            line_table: self.line_table,
836        }
837    }
838
839    /// Record that bytecode emitted from this point forward corresponds to
840    /// the given source line. RLE-deduplicated: consecutive calls with the
841    /// same line produce only one entry.
842    fn note_line(&mut self, line: usize) {
843        if line == 0 {
844            return;
845        }
846        let line16 = line as u16;
847        if line16 == self.last_noted_line {
848            return; // RLE dedup
849        }
850        self.last_noted_line = line16;
851        self.line_table.push((self.code.len() as u16, line16));
852    }
853
854    fn emit_op(&mut self, op: u8) {
855        let prev_pos = self.last_op_pos;
856        let prev_op = if prev_pos < self.code.len() {
857            self.code[prev_pos]
858        } else {
859            0xFF
860        };
861
862        // LOAD_LOCAL + LOAD_LOCAL → LOAD_LOCAL_2
863        if op == LOAD_LOCAL && prev_op == LOAD_LOCAL && prev_pos + 2 == self.code.len() {
864            self.code[prev_pos] = LOAD_LOCAL_2;
865            // slot_a already at prev_pos+1, slot_b emitted next via emit_u8
866            return;
867        }
868        // LOAD_LOCAL + LOAD_CONST → LOAD_LOCAL_CONST
869        if op == LOAD_CONST && prev_op == LOAD_LOCAL && prev_pos + 2 == self.code.len() {
870            self.code[prev_pos] = LOAD_LOCAL_CONST;
871            // slot at prev_pos+1, const_idx (u16) emitted next via emit_u16
872            return;
873        }
874        // VECTOR_GET + LOAD_CONST(hi,lo) + UNWRAP_OR → VECTOR_GET_OR(hi,lo)
875        // Before: [..., VECTOR_GET, LOAD_CONST, hi, lo] + about to emit UNWRAP_OR
876        // After:  [..., VECTOR_GET_OR, hi, lo]
877        if op == UNWRAP_OR && self.code.len() >= 4 {
878            let len = self.code.len();
879            if self.code[len - 4] == VECTOR_GET && self.code[len - 3] == LOAD_CONST {
880                let hi = self.code[len - 2];
881                let lo = self.code[len - 1];
882                self.code[len - 4] = VECTOR_GET_OR;
883                self.code[len - 3] = hi;
884                self.code[len - 2] = lo;
885                self.code.pop(); // remove extra byte
886                self.last_op_pos = len - 4;
887                return;
888            }
889        }
890        self.last_op_pos = self.code.len();
891        self.code.push(op);
892    }
893
894    fn emit_u8(&mut self, val: u8) {
895        self.code.push(val);
896    }
897
898    fn emit_u16(&mut self, val: u16) {
899        self.code.push((val >> 8) as u8);
900        self.code.push((val & 0xFF) as u8);
901    }
902
903    fn emit_i16(&mut self, val: i16) {
904        self.emit_u16(val as u16);
905    }
906
907    fn emit_u32(&mut self, val: u32) {
908        self.code.push((val >> 24) as u8);
909        self.code.push(((val >> 16) & 0xFF) as u8);
910        self.code.push(((val >> 8) & 0xFF) as u8);
911        self.code.push((val & 0xFF) as u8);
912    }
913
914    fn emit_u64(&mut self, val: u64) {
915        self.code.extend_from_slice(&val.to_be_bytes());
916    }
917
918    fn add_constant(&mut self, val: NanValue) -> u16 {
919        for (i, c) in self.constants.iter().enumerate() {
920            if c.bits() == val.bits() {
921                return i as u16;
922            }
923        }
924        let idx = self.constants.len() as u16;
925        self.constants.push(val);
926        idx
927    }
928
929    fn offset(&self) -> usize {
930        self.code.len()
931    }
932
933    fn emit_jump(&mut self, op: u8) -> usize {
934        self.emit_op(op);
935        let patch_pos = self.code.len();
936        self.emit_i16(0);
937        patch_pos
938    }
939
940    fn patch_jump(&mut self, patch_pos: usize) {
941        let target = self.code.len();
942        let offset = (target as isize - patch_pos as isize - 2) as i16;
943        let bytes = (offset as u16).to_be_bytes();
944        self.code[patch_pos] = bytes[0];
945        self.code[patch_pos + 1] = bytes[1];
946    }
947
948    fn patch_jump_to(&mut self, patch_pos: usize, target: usize) {
949        let offset = (target as isize - patch_pos as isize - 2) as i16;
950        let bytes = (offset as u16).to_be_bytes();
951        self.code[patch_pos] = bytes[0];
952        self.code[patch_pos + 1] = bytes[1];
953    }
954
955    fn bind_top_to_local(&mut self, name: &str) {
956        if let Some(&slot) = self.local_slots.get(name) {
957            self.emit_op(STORE_LOCAL);
958            self.emit_u8(slot as u8);
959        } else {
960            self.emit_op(POP);
961        }
962    }
963
964    fn dup_and_bind_top_to_local(&mut self, name: &str) {
965        self.emit_op(DUP);
966        self.bind_top_to_local(name);
967    }
968
969    /// Override `local_slots` with this arm's per-arm fresh slots so
970    /// every `bind_top_to_local(name)` inside the arm writes to the
971    /// slot the resolver allocated for *this* arm (not whatever was
972    /// last allocated for the same name elsewhere). Returns the saved
973    /// prior mapping so the caller can `restore_local_slots` afterward.
974    pub(super) fn install_arm_slots(
975        &mut self,
976        arm: &crate::ast::MatchArm,
977    ) -> Vec<(String, Option<u16>)> {
978        let names = collect_pattern_binding_names(&arm.pattern);
979        let slots = arm.binding_slots.get().cloned().unwrap_or_default();
980        let mut saved = Vec::new();
981        for (i, name) in names.iter().enumerate() {
982            if name == "_" {
983                continue;
984            }
985            let Some(&slot) = slots.get(i) else { continue };
986            if slot == u16::MAX {
987                continue;
988            }
989            saved.push((name.clone(), self.local_slots.get(name).copied()));
990            self.local_slots.insert(name.clone(), slot);
991        }
992        saved
993    }
994
995    pub(super) fn restore_local_slots(&mut self, saved: Vec<(String, Option<u16>)>) {
996        for (name, prior) in saved.into_iter().rev() {
997            match prior {
998                Some(slot) => {
999                    self.local_slots.insert(name, slot);
1000                }
1001                None => {
1002                    self.local_slots.remove(&name);
1003                }
1004            }
1005        }
1006    }
1007}
1008
1009/// Pattern-position-ordered binding names — must mirror
1010/// `resolver::ResolverState::allocate_pattern` exactly so position
1011/// `i` lines up with `arm.binding_slots[i]`.
1012fn collect_pattern_binding_names(pattern: &crate::ast::Pattern) -> Vec<String> {
1013    use crate::ast::Pattern;
1014    match pattern {
1015        Pattern::Ident(name) => vec![name.clone()],
1016        Pattern::Cons(head, tail) => vec![head.clone(), tail.clone()],
1017        Pattern::Constructor(_, bindings) => bindings.clone(),
1018        Pattern::Tuple(items) => items
1019            .iter()
1020            .flat_map(collect_pattern_binding_names)
1021            .collect(),
1022        Pattern::Wildcard | Pattern::Literal(_) | Pattern::EmptyList => Vec::new(),
1023    }
1024}
1025
1026#[cfg(test)]
1027mod tests {
1028    use super::compile_program;
1029    use crate::nan_value::Arena;
1030    use crate::source::parse_source;
1031    use crate::vm::opcode::{LT, NOT, VECTOR_GET_OR, VECTOR_SET_OR_KEEP};
1032
1033    #[test]
1034    fn vector_get_with_literal_default_lowers_to_vector_get_or() {
1035        let source = r#"
1036module Demo
1037
1038fn cellAt(grid: Vector<Int>, idx: Int) -> Int
1039    Option.withDefault(Vector.get(grid, idx), 0)
1040"#;
1041
1042        let mut items = parse_source(source).expect("source should parse");
1043        crate::ir::pipeline::tco(&mut items);
1044        crate::ir::pipeline::resolve(&mut items);
1045
1046        let mut arena = Arena::new();
1047        let (code, _globals) =
1048            compile_program(&items, &mut arena, None).expect("vm compile should pass");
1049        let fn_id = code.find("cellAt").expect("cellAt should exist");
1050        let chunk = code.get(fn_id);
1051
1052        assert!(
1053            chunk.code.contains(&VECTOR_GET_OR),
1054            "expected VECTOR_GET_OR in bytecode, got {:?}",
1055            chunk.code
1056        );
1057    }
1058
1059    #[test]
1060    fn vector_set_with_same_default_lowers_to_vector_set_or_keep() {
1061        let source = r#"
1062module Demo
1063
1064fn updateOrKeep(vec: Vector<Int>, idx: Int, value: Int) -> Vector<Int>
1065    Option.withDefault(Vector.set(vec, idx, value), vec)
1066"#;
1067
1068        let mut items = parse_source(source).expect("source should parse");
1069        crate::ir::pipeline::tco(&mut items);
1070        crate::ir::pipeline::resolve(&mut items);
1071
1072        let mut arena = Arena::new();
1073        let (code, _globals) =
1074            compile_program(&items, &mut arena, None).expect("vm compile should pass");
1075        let fn_id = code
1076            .find("updateOrKeep")
1077            .expect("updateOrKeep should exist");
1078        let chunk = code.get(fn_id);
1079
1080        assert!(
1081            chunk.code.contains(&VECTOR_SET_OR_KEEP),
1082            "expected VECTOR_SET_OR_KEEP in bytecode, got {:?}",
1083            chunk.code
1084        );
1085    }
1086
1087    #[test]
1088    fn bool_match_on_gte_uses_base_compare_without_not() {
1089        let source = r#"
1090module Demo
1091
1092fn bucket(n: Int) -> Int
1093    match n >= 10
1094        true -> 7
1095        false -> 3
1096"#;
1097
1098        let mut items = parse_source(source).expect("source should parse");
1099        crate::ir::pipeline::tco(&mut items);
1100        crate::ir::pipeline::resolve(&mut items);
1101
1102        let mut arena = Arena::new();
1103        let (code, _globals) =
1104            compile_program(&items, &mut arena, None).expect("vm compile should pass");
1105        let fn_id = code.find("bucket").expect("bucket should exist");
1106        let chunk = code.get(fn_id);
1107
1108        assert!(
1109            chunk.code.contains(&LT),
1110            "expected LT in bytecode, got {:?}",
1111            chunk.code
1112        );
1113        assert!(
1114            !chunk.code.contains(&NOT),
1115            "did not expect NOT in normalized bool-match bytecode, got {:?}",
1116            chunk.code
1117        );
1118    }
1119
1120    #[test]
1121    fn self_host_runtime_http_server_aliases_compile_in_vm() {
1122        let source = r#"
1123module Demo
1124
1125fn listen(handler: Int) -> Unit
1126    SelfHostRuntime.httpServerListen(8080, handler)
1127
1128fn listenWith(context: Int, handler: Int) -> Unit
1129    SelfHostRuntime.httpServerListenWith(8081, context, handler)
1130"#;
1131
1132        let mut items = parse_source(source).expect("source should parse");
1133        crate::ir::pipeline::tco(&mut items);
1134        crate::ir::pipeline::resolve(&mut items);
1135
1136        let mut arena = Arena::new();
1137        let (code, _globals) =
1138            compile_program(&items, &mut arena, None).expect("vm compile should pass");
1139        assert!(code.find("listen").is_some(), "listen should compile");
1140        assert!(
1141            code.find("listenWith").is_some(),
1142            "listenWith should compile"
1143        );
1144    }
1145}