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