Skip to main content

harn_vm/
chunk.rs

1use std::cell::RefCell;
2use std::collections::BTreeMap;
3use std::fmt;
4use std::rc::Rc;
5
6use harn_parser::TypeExpr;
7
8/// Bytecode opcodes for the Harn VM.
9#[derive(Debug, Clone, Copy, PartialEq, Eq)]
10#[repr(u8)]
11pub enum Op {
12    /// Push a constant from the constant pool onto the stack.
13    Constant, // arg: u16 constant index
14    /// Push nil onto the stack.
15    Nil,
16    /// Push true onto the stack.
17    True,
18    /// Push false onto the stack.
19    False,
20
21    // --- Variable operations ---
22    /// Get a variable by name (from constant pool).
23    GetVar, // arg: u16 constant index (name)
24    /// Define a new immutable variable. Pops value from stack.
25    DefLet, // arg: u16 constant index (name)
26    /// Define a new mutable variable. Pops value from stack.
27    DefVar, // arg: u16 constant index (name)
28    /// Assign to an existing mutable variable. Pops value from stack.
29    SetVar, // arg: u16 constant index (name)
30    /// Push a new lexical scope onto the environment stack.
31    PushScope,
32    /// Pop the current lexical scope from the environment stack.
33    PopScope,
34
35    // --- Arithmetic ---
36    Add,
37    Sub,
38    Mul,
39    Div,
40    Mod,
41    Pow,
42    Negate,
43
44    // --- Comparison ---
45    Equal,
46    NotEqual,
47    Less,
48    Greater,
49    LessEqual,
50    GreaterEqual,
51
52    // --- Logical ---
53    Not,
54
55    // --- Control flow ---
56    /// Jump unconditionally. arg: u16 offset.
57    Jump,
58    /// Jump if top of stack is falsy. Does not pop. arg: u16 offset.
59    JumpIfFalse,
60    /// Jump if top of stack is truthy. Does not pop. arg: u16 offset.
61    JumpIfTrue,
62    /// Pop top of stack (discard).
63    Pop,
64
65    // --- Functions ---
66    /// Call a function/builtin. arg: u8 = arg count. Name is on stack below args.
67    Call,
68    /// Tail call: like Call, but replaces the current frame instead of pushing
69    /// a new one. Used for `return f(x)` to enable tail call optimization.
70    /// For builtins, behaves like a regular Call (no frame to replace).
71    TailCall,
72    /// Return from current function. Pops return value.
73    Return,
74    /// Create a closure. arg: u16 = chunk index in function table.
75    Closure,
76
77    // --- Collections ---
78    /// Build a list. arg: u16 = element count. Elements are on stack.
79    BuildList,
80    /// Build a dict. arg: u16 = entry count. Key-value pairs on stack.
81    BuildDict,
82    /// Subscript access: stack has [object, index]. Pushes result.
83    Subscript,
84    /// Optional subscript (`obj?[index]`). Like `Subscript` but pushes nil
85    /// instead of indexing when the object is nil.
86    SubscriptOpt,
87    /// Slice access: stack has [object, start_or_nil, end_or_nil]. Pushes sublist/substring.
88    Slice,
89
90    // --- Object operations ---
91    /// Property access. arg: u16 = constant index (property name).
92    GetProperty,
93    /// Optional property access (?.). Like GetProperty but returns nil
94    /// instead of erroring when the object is nil. arg: u16 = constant index.
95    GetPropertyOpt,
96    /// Property assignment. arg: u16 = constant index (property name).
97    /// Stack: [value] → assigns to the named variable's property.
98    SetProperty,
99    /// Subscript assignment. arg: u16 = constant index (variable name).
100    /// Stack: [index, value] → assigns to variable[index] = value.
101    SetSubscript,
102    /// Method call. arg1: u16 = constant index (method name), arg2: u8 = arg count.
103    MethodCall,
104    /// Optional method call (?.). Like MethodCall but returns nil if the
105    /// receiver is nil instead of dispatching. arg1: u16, arg2: u8.
106    MethodCallOpt,
107
108    // --- String ---
109    /// String concatenation of N parts. arg: u16 = part count.
110    Concat,
111
112    // --- Iteration ---
113    /// Set up a for-in loop. Expects iterable on stack. Pushes iterator state.
114    IterInit,
115    /// Advance iterator. If exhausted, jumps. arg: u16 = jump offset.
116    /// Pushes next value and the variable name is set via DefVar before the loop.
117    IterNext,
118
119    // --- Pipe ---
120    /// Pipe: pops [value, callable], invokes callable(value).
121    Pipe,
122
123    // --- Error handling ---
124    /// Pop value, raise as error.
125    Throw,
126    /// Push exception handler. arg: u16 = offset to catch handler.
127    TryCatchSetup,
128    /// Remove top exception handler (end of try body).
129    PopHandler,
130
131    // --- Concurrency ---
132    /// Execute closure N times sequentially, push results as list.
133    /// Stack: count, closure → result_list
134    Parallel,
135    /// Execute closure for each item in list, push results as list.
136    /// Stack: list, closure → result_list
137    ParallelMap,
138    /// Execute closure for each item in list, push a stream that emits in completion order.
139    /// Stack: list, closure → stream
140    ParallelMapStream,
141    /// Like ParallelMap but wraps each result in Result.Ok/Err, never fails.
142    /// Stack: list, closure → {results: [Result], succeeded: int, failed: int}
143    ParallelSettle,
144    /// Store closure for deferred execution, push TaskHandle.
145    /// Stack: closure → TaskHandle
146    Spawn,
147    /// Acquire a process-local mutex for the current lexical scope.
148    /// arg: u16 constant index (key string).
149    SyncMutexEnter,
150
151    // --- Imports ---
152    /// Import a file. arg: u16 = constant index (path string).
153    Import,
154    /// Selective import. arg1: u16 = path string, arg2: u16 = names list constant.
155    SelectiveImport,
156
157    // --- Deadline ---
158    /// Pop duration value, push deadline onto internal deadline stack.
159    DeadlineSetup,
160    /// Pop deadline from internal deadline stack.
161    DeadlineEnd,
162
163    // --- Enum ---
164    /// Build an enum variant value.
165    /// arg1: u16 = constant index (enum name), arg2: u16 = constant index (variant name),
166    /// arg3: u16 = field count. Fields are on stack.
167    BuildEnum,
168
169    // --- Match ---
170    /// Match an enum pattern. Checks enum_name + variant on the top of stack (dup'd match value).
171    /// arg1: u16 = constant index (enum name), arg2: u16 = constant index (variant name).
172    /// If match succeeds, pushes true; else pushes false.
173    MatchEnum,
174
175    // --- Loop control ---
176    /// Pop the top iterator from the iterator stack (cleanup on break from for-in).
177    PopIterator,
178
179    // --- Defaults ---
180    /// Push the number of arguments passed to the current function call.
181    GetArgc,
182
183    // --- Type checking ---
184    /// Runtime type check on a variable.
185    /// arg1: u16 = constant index (variable name),
186    /// arg2: u16 = constant index (expected type name).
187    /// Throws a TypeError if the variable's type doesn't match.
188    CheckType,
189
190    // --- Result try operator ---
191    /// Try-unwrap: if top is Result.Ok(v), replace with v. If Result.Err(e), return it.
192    TryUnwrap,
193    /// Wrap top of stack in Result.Ok unless it is already a Result.
194    TryWrapOk,
195
196    // --- Spread call ---
197    /// Call with spread arguments. Stack: [callee, args_list] -> result.
198    CallSpread,
199    /// Direct builtin call. Followed by u64 builtin ID, u16 name constant, u8 arg count.
200    /// Runtime still checks closure shadowing before using the ID.
201    CallBuiltin,
202    /// Direct builtin spread call. Followed by u64 builtin ID and u16 name constant.
203    /// Stack: [args_list] -> result.
204    CallBuiltinSpread,
205    /// Method call with spread arguments. Stack: [object, args_list] -> result.
206    /// Followed by 2 bytes for method name constant index.
207    MethodCallSpread,
208
209    // --- Misc ---
210    /// Duplicate top of stack.
211    Dup,
212    /// Swap top two stack values.
213    Swap,
214    /// Membership test: stack has [item, collection]. Pushes bool.
215    /// Works for lists (item in list), dicts (key in dict), strings (substr in string), and sets.
216    Contains,
217
218    // --- Typed arithmetic/comparison fast paths ---
219    AddInt,
220    SubInt,
221    MulInt,
222    DivInt,
223    ModInt,
224    AddFloat,
225    SubFloat,
226    MulFloat,
227    DivFloat,
228    ModFloat,
229    EqualInt,
230    NotEqualInt,
231    LessInt,
232    GreaterInt,
233    LessEqualInt,
234    GreaterEqualInt,
235    EqualFloat,
236    NotEqualFloat,
237    LessFloat,
238    GreaterFloat,
239    LessEqualFloat,
240    GreaterEqualFloat,
241    EqualBool,
242    NotEqualBool,
243    EqualString,
244    NotEqualString,
245
246    /// Yield a value from a generator. Pops value, sends through channel, suspends.
247    Yield,
248
249    // --- Slot-indexed locals ---
250    /// Get a frame-local slot. arg: u16 slot index.
251    GetLocalSlot,
252    /// Define or initialize a frame-local slot. Pops value from stack.
253    DefLocalSlot,
254    /// Assign an existing frame-local slot. Pops value from stack.
255    SetLocalSlot,
256}
257
258impl Op {
259    pub(crate) const ALL: &'static [Self] = &[
260        Op::Constant,
261        Op::Nil,
262        Op::True,
263        Op::False,
264        Op::GetVar,
265        Op::DefLet,
266        Op::DefVar,
267        Op::SetVar,
268        Op::PushScope,
269        Op::PopScope,
270        Op::Add,
271        Op::Sub,
272        Op::Mul,
273        Op::Div,
274        Op::Mod,
275        Op::Pow,
276        Op::Negate,
277        Op::Equal,
278        Op::NotEqual,
279        Op::Less,
280        Op::Greater,
281        Op::LessEqual,
282        Op::GreaterEqual,
283        Op::Not,
284        Op::Jump,
285        Op::JumpIfFalse,
286        Op::JumpIfTrue,
287        Op::Pop,
288        Op::Call,
289        Op::TailCall,
290        Op::Return,
291        Op::Closure,
292        Op::BuildList,
293        Op::BuildDict,
294        Op::Subscript,
295        Op::SubscriptOpt,
296        Op::Slice,
297        Op::GetProperty,
298        Op::GetPropertyOpt,
299        Op::SetProperty,
300        Op::SetSubscript,
301        Op::MethodCall,
302        Op::MethodCallOpt,
303        Op::Concat,
304        Op::IterInit,
305        Op::IterNext,
306        Op::Pipe,
307        Op::Throw,
308        Op::TryCatchSetup,
309        Op::PopHandler,
310        Op::Parallel,
311        Op::ParallelMap,
312        Op::ParallelMapStream,
313        Op::ParallelSettle,
314        Op::Spawn,
315        Op::SyncMutexEnter,
316        Op::Import,
317        Op::SelectiveImport,
318        Op::DeadlineSetup,
319        Op::DeadlineEnd,
320        Op::BuildEnum,
321        Op::MatchEnum,
322        Op::PopIterator,
323        Op::GetArgc,
324        Op::CheckType,
325        Op::TryUnwrap,
326        Op::TryWrapOk,
327        Op::CallSpread,
328        Op::CallBuiltin,
329        Op::CallBuiltinSpread,
330        Op::MethodCallSpread,
331        Op::Dup,
332        Op::Swap,
333        Op::Contains,
334        Op::AddInt,
335        Op::SubInt,
336        Op::MulInt,
337        Op::DivInt,
338        Op::ModInt,
339        Op::AddFloat,
340        Op::SubFloat,
341        Op::MulFloat,
342        Op::DivFloat,
343        Op::ModFloat,
344        Op::EqualInt,
345        Op::NotEqualInt,
346        Op::LessInt,
347        Op::GreaterInt,
348        Op::LessEqualInt,
349        Op::GreaterEqualInt,
350        Op::EqualFloat,
351        Op::NotEqualFloat,
352        Op::LessFloat,
353        Op::GreaterFloat,
354        Op::LessEqualFloat,
355        Op::GreaterEqualFloat,
356        Op::EqualBool,
357        Op::NotEqualBool,
358        Op::EqualString,
359        Op::NotEqualString,
360        Op::Yield,
361        Op::GetLocalSlot,
362        Op::DefLocalSlot,
363        Op::SetLocalSlot,
364    ];
365
366    pub(crate) fn from_byte(byte: u8) -> Option<Self> {
367        Self::ALL.get(byte as usize).copied()
368    }
369}
370
371/// A constant value in the constant pool.
372#[derive(Debug, Clone, PartialEq)]
373pub enum Constant {
374    Int(i64),
375    Float(f64),
376    String(String),
377    Bool(bool),
378    Nil,
379    Duration(i64),
380}
381
382/// Monomorphic inline-cache state for bytecode instructions that repeatedly
383/// resolve the same property or builtin method. Cache guards are intentionally
384/// conservative: each entry is tied to the instruction's name constant index
385/// and a single receiver variant. Harn collection values are immutable or
386/// copy-on-write at the VM level, so list/string/pair/range/set/dict receiver
387/// kind caches do not need invalidation; dynamic dict fields and struct fields
388/// are left on the generic path until they have stable layout metadata.
389#[derive(Debug, Clone, PartialEq, Eq)]
390pub(crate) enum InlineCacheEntry {
391    Empty,
392    Property {
393        name_idx: u16,
394        target: PropertyCacheTarget,
395    },
396    Method {
397        name_idx: u16,
398        argc: usize,
399        target: MethodCacheTarget,
400    },
401}
402
403#[derive(Debug, Clone, Copy, PartialEq, Eq)]
404pub(crate) enum PropertyCacheTarget {
405    ListCount,
406    ListEmpty,
407    ListFirst,
408    ListLast,
409    StringCount,
410    StringEmpty,
411    PairFirst,
412    PairSecond,
413    EnumVariant,
414    EnumFields,
415}
416
417#[derive(Debug, Clone, Copy, PartialEq, Eq)]
418pub(crate) enum MethodCacheTarget {
419    ListCount,
420    ListEmpty,
421    StringCount,
422    StringEmpty,
423    DictCount,
424    RangeCount,
425    RangeLen,
426    RangeEmpty,
427    RangeFirst,
428    RangeLast,
429    SetCount,
430    SetLen,
431    SetEmpty,
432}
433
434/// Debug metadata for a slot-indexed local in a compiled chunk.
435#[derive(Debug, Clone, PartialEq, Eq)]
436pub struct LocalSlotInfo {
437    pub name: String,
438    pub mutable: bool,
439    pub scope_depth: usize,
440}
441
442impl fmt::Display for Constant {
443    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
444        match self {
445            Constant::Int(n) => write!(f, "{n}"),
446            Constant::Float(n) => write!(f, "{n}"),
447            Constant::String(s) => write!(f, "\"{s}\""),
448            Constant::Bool(b) => write!(f, "{b}"),
449            Constant::Nil => write!(f, "nil"),
450            Constant::Duration(ms) => write!(f, "{ms}ms"),
451        }
452    }
453}
454
455/// A compiled chunk of bytecode.
456#[derive(Debug, Clone)]
457pub struct Chunk {
458    /// The bytecode instructions.
459    pub code: Vec<u8>,
460    /// Constant pool.
461    pub constants: Vec<Constant>,
462    /// Source line numbers for each instruction (for error reporting).
463    pub lines: Vec<u32>,
464    /// Source column numbers for each instruction (for error reporting).
465    /// Parallel to `lines`; 0 means no column info available.
466    pub columns: Vec<u32>,
467    /// Source file that this chunk was compiled from, when known. Set for
468    /// chunks compiled from imported modules so runtime errors can report
469    /// the correct file path for each frame instead of always pointing at
470    /// the entry-point pipeline.
471    pub source_file: Option<String>,
472    /// Current column to use when emitting instructions (set by compiler).
473    current_col: u32,
474    /// Compiled function bodies (for closures).
475    pub functions: Vec<CompiledFunctionRef>,
476    /// Instruction offset to inline-cache slot. Slots are assigned at emit time
477    /// for cacheable instructions while bytecode bytes remain immutable.
478    inline_cache_slots: BTreeMap<usize, usize>,
479    /// Shared cache entries so cloned chunks in call frames warm the same side
480    /// table as the compiled chunk used by tests/debugging.
481    inline_caches: Rc<RefCell<Vec<InlineCacheEntry>>>,
482    /// Source-name metadata for slot-indexed locals in this chunk.
483    pub(crate) local_slots: Vec<LocalSlotInfo>,
484}
485
486pub type ChunkRef = Rc<Chunk>;
487pub type CompiledFunctionRef = Rc<CompiledFunction>;
488
489#[derive(Debug, Clone)]
490pub(crate) struct CachedChunk {
491    code: Vec<u8>,
492    constants: Vec<Constant>,
493    lines: Vec<u32>,
494    columns: Vec<u32>,
495    source_file: Option<String>,
496    current_col: u32,
497    functions: Vec<CachedCompiledFunction>,
498    inline_cache_slots: BTreeMap<usize, usize>,
499    local_slots: Vec<LocalSlotInfo>,
500}
501
502#[derive(Debug, Clone)]
503pub(crate) struct CachedCompiledFunction {
504    name: String,
505    type_params: Vec<String>,
506    nominal_type_names: Vec<String>,
507    params: Vec<ParamSlot>,
508    default_start: Option<usize>,
509    chunk: CachedChunk,
510    is_generator: bool,
511    is_stream: bool,
512    has_rest_param: bool,
513}
514
515/// One parameter slot of a compiled user-defined function. Carries the
516/// declared name, the (optional) declared type expression, and a flag
517/// for whether a default value was provided. The runtime consults the
518/// type expression in `bind_param_slots` to enforce declared types
519/// against the values supplied at the call site.
520#[derive(Debug, Clone)]
521pub struct ParamSlot {
522    pub name: String,
523    /// Declared parameter type. `None` for untyped parameters (gradual
524    /// typing); the runtime skips type assertion when absent.
525    pub type_expr: Option<TypeExpr>,
526    /// True when the parameter has a default-value clause. Diagnostic
527    /// only — the canonical authority for arity ranges is
528    /// [`CompiledFunction::default_start`].
529    pub has_default: bool,
530}
531
532impl ParamSlot {
533    /// Build a [`ParamSlot`] from a parser-side [`harn_parser::TypedParam`].
534    /// Centralizes the conversion so every compile path stays in lockstep.
535    pub fn from_typed_param(param: &harn_parser::TypedParam) -> Self {
536        Self {
537            name: param.name.clone(),
538            type_expr: param.type_expr.clone(),
539            has_default: param.default_value.is_some(),
540        }
541    }
542
543    /// Build a `Vec<ParamSlot>` from a slice of parser-side typed
544    /// parameters. Used pervasively at compile sites instead of
545    /// `TypedParam::names` (which discarded the type info we now need
546    /// at runtime).
547    pub fn vec_from_typed(params: &[harn_parser::TypedParam]) -> Vec<Self> {
548        params.iter().map(Self::from_typed_param).collect()
549    }
550}
551
552/// A compiled function (closure body).
553#[derive(Debug, Clone)]
554pub struct CompiledFunction {
555    pub name: String,
556    /// Generic type parameters declared by this function. Runtime
557    /// validation treats these as static-only constraints because the VM
558    /// does not monomorphize function bodies.
559    pub type_params: Vec<String>,
560    /// User-defined struct and enum names visible when this function was
561    /// compiled. These are the only non-primitive named types with runtime
562    /// nominal identity; aliases and interfaces remain static-only.
563    pub nominal_type_names: Vec<String>,
564    pub params: Vec<ParamSlot>,
565    /// Index of the first parameter with a default value, or None if all required.
566    pub default_start: Option<usize>,
567    pub chunk: ChunkRef,
568    /// True if the function body contains `yield` expressions (generator function).
569    pub is_generator: bool,
570    /// True if the function was declared as `gen fn` and should return Stream.
571    pub is_stream: bool,
572    /// True if the last parameter is a rest parameter (`...name`).
573    pub has_rest_param: bool,
574}
575
576impl CompiledFunction {
577    /// Returns just the parameter names — convenience for code paths that
578    /// don't care about types or defaults.
579    pub fn param_names(&self) -> impl Iterator<Item = &str> {
580        self.params.iter().map(|p| p.name.as_str())
581    }
582
583    /// Number of required parameters (those before `default_start`).
584    pub fn required_param_count(&self) -> usize {
585        self.default_start.unwrap_or(self.params.len())
586    }
587
588    pub fn declares_type_param(&self, name: &str) -> bool {
589        self.type_params.iter().any(|param| param == name)
590    }
591
592    pub fn has_nominal_type(&self, name: &str) -> bool {
593        self.nominal_type_names.iter().any(|ty| ty == name)
594    }
595
596    pub(crate) fn freeze_for_cache(&self) -> CachedCompiledFunction {
597        CachedCompiledFunction {
598            name: self.name.clone(),
599            type_params: self.type_params.clone(),
600            nominal_type_names: self.nominal_type_names.clone(),
601            params: self.params.clone(),
602            default_start: self.default_start,
603            chunk: self.chunk.freeze_for_cache(),
604            is_generator: self.is_generator,
605            is_stream: self.is_stream,
606            has_rest_param: self.has_rest_param,
607        }
608    }
609
610    pub(crate) fn from_cached(cached: &CachedCompiledFunction) -> Self {
611        Self {
612            name: cached.name.clone(),
613            type_params: cached.type_params.clone(),
614            nominal_type_names: cached.nominal_type_names.clone(),
615            params: cached.params.clone(),
616            default_start: cached.default_start,
617            chunk: Rc::new(Chunk::from_cached(&cached.chunk)),
618            is_generator: cached.is_generator,
619            is_stream: cached.is_stream,
620            has_rest_param: cached.has_rest_param,
621        }
622    }
623}
624
625impl Chunk {
626    pub fn new() -> Self {
627        Self {
628            code: Vec::new(),
629            constants: Vec::new(),
630            lines: Vec::new(),
631            columns: Vec::new(),
632            source_file: None,
633            current_col: 0,
634            functions: Vec::new(),
635            inline_cache_slots: BTreeMap::new(),
636            inline_caches: Rc::new(RefCell::new(Vec::new())),
637            local_slots: Vec::new(),
638        }
639    }
640
641    /// Set the current column for subsequent emit calls.
642    pub fn set_column(&mut self, col: u32) {
643        self.current_col = col;
644    }
645
646    /// Add a constant and return its index.
647    pub fn add_constant(&mut self, constant: Constant) -> u16 {
648        for (i, c) in self.constants.iter().enumerate() {
649            if c == &constant {
650                return i as u16;
651            }
652        }
653        let idx = self.constants.len();
654        self.constants.push(constant);
655        idx as u16
656    }
657
658    /// Emit a single-byte instruction.
659    pub fn emit(&mut self, op: Op, line: u32) {
660        let col = self.current_col;
661        self.code.push(op as u8);
662        self.lines.push(line);
663        self.columns.push(col);
664    }
665
666    /// Emit an instruction with a u16 argument.
667    pub fn emit_u16(&mut self, op: Op, arg: u16, line: u32) {
668        let col = self.current_col;
669        let op_offset = self.code.len();
670        self.code.push(op as u8);
671        self.code.push((arg >> 8) as u8);
672        self.code.push((arg & 0xFF) as u8);
673        self.lines.push(line);
674        self.lines.push(line);
675        self.lines.push(line);
676        self.columns.push(col);
677        self.columns.push(col);
678        self.columns.push(col);
679        if matches!(
680            op,
681            Op::GetProperty | Op::GetPropertyOpt | Op::MethodCallSpread
682        ) {
683            self.register_inline_cache(op_offset);
684        }
685    }
686
687    /// Emit an instruction with a u8 argument.
688    pub fn emit_u8(&mut self, op: Op, arg: u8, line: u32) {
689        let col = self.current_col;
690        self.code.push(op as u8);
691        self.code.push(arg);
692        self.lines.push(line);
693        self.lines.push(line);
694        self.columns.push(col);
695        self.columns.push(col);
696    }
697
698    /// Emit a direct builtin call.
699    pub fn emit_call_builtin(
700        &mut self,
701        id: crate::BuiltinId,
702        name_idx: u16,
703        arg_count: u8,
704        line: u32,
705    ) {
706        let col = self.current_col;
707        self.code.push(Op::CallBuiltin as u8);
708        self.code.extend_from_slice(&id.raw().to_be_bytes());
709        self.code.push((name_idx >> 8) as u8);
710        self.code.push((name_idx & 0xFF) as u8);
711        self.code.push(arg_count);
712        for _ in 0..12 {
713            self.lines.push(line);
714            self.columns.push(col);
715        }
716    }
717
718    /// Emit a direct builtin spread call.
719    pub fn emit_call_builtin_spread(&mut self, id: crate::BuiltinId, name_idx: u16, line: u32) {
720        let col = self.current_col;
721        self.code.push(Op::CallBuiltinSpread as u8);
722        self.code.extend_from_slice(&id.raw().to_be_bytes());
723        self.code.push((name_idx >> 8) as u8);
724        self.code.push((name_idx & 0xFF) as u8);
725        for _ in 0..11 {
726            self.lines.push(line);
727            self.columns.push(col);
728        }
729    }
730
731    /// Emit a method call: op + u16 (method name) + u8 (arg count).
732    pub fn emit_method_call(&mut self, name_idx: u16, arg_count: u8, line: u32) {
733        self.emit_method_call_inner(Op::MethodCall, name_idx, arg_count, line);
734    }
735
736    /// Emit an optional method call (?.) — returns nil if receiver is nil.
737    pub fn emit_method_call_opt(&mut self, name_idx: u16, arg_count: u8, line: u32) {
738        self.emit_method_call_inner(Op::MethodCallOpt, name_idx, arg_count, line);
739    }
740
741    fn emit_method_call_inner(&mut self, op: Op, name_idx: u16, arg_count: u8, line: u32) {
742        let col = self.current_col;
743        let op_offset = self.code.len();
744        self.code.push(op as u8);
745        self.code.push((name_idx >> 8) as u8);
746        self.code.push((name_idx & 0xFF) as u8);
747        self.code.push(arg_count);
748        self.lines.push(line);
749        self.lines.push(line);
750        self.lines.push(line);
751        self.lines.push(line);
752        self.columns.push(col);
753        self.columns.push(col);
754        self.columns.push(col);
755        self.columns.push(col);
756        self.register_inline_cache(op_offset);
757    }
758
759    /// Current code offset (for jump patching).
760    pub fn current_offset(&self) -> usize {
761        self.code.len()
762    }
763
764    /// Emit a jump instruction with a placeholder offset. Returns the position to patch.
765    pub fn emit_jump(&mut self, op: Op, line: u32) -> usize {
766        let col = self.current_col;
767        self.code.push(op as u8);
768        let patch_pos = self.code.len();
769        self.code.push(0xFF);
770        self.code.push(0xFF);
771        self.lines.push(line);
772        self.lines.push(line);
773        self.lines.push(line);
774        self.columns.push(col);
775        self.columns.push(col);
776        self.columns.push(col);
777        patch_pos
778    }
779
780    /// Patch a jump instruction at the given position to jump to the current offset.
781    pub fn patch_jump(&mut self, patch_pos: usize) {
782        let target = self.code.len() as u16;
783        self.code[patch_pos] = (target >> 8) as u8;
784        self.code[patch_pos + 1] = (target & 0xFF) as u8;
785    }
786
787    /// Patch a jump to a specific target position.
788    pub fn patch_jump_to(&mut self, patch_pos: usize, target: usize) {
789        let target = target as u16;
790        self.code[patch_pos] = (target >> 8) as u8;
791        self.code[patch_pos + 1] = (target & 0xFF) as u8;
792    }
793
794    /// Read a u16 argument at the given position.
795    pub fn read_u16(&self, pos: usize) -> u16 {
796        ((self.code[pos] as u16) << 8) | (self.code[pos + 1] as u16)
797    }
798
799    fn register_inline_cache(&mut self, op_offset: usize) {
800        if self.inline_cache_slots.contains_key(&op_offset) {
801            return;
802        }
803        let mut entries = self.inline_caches.borrow_mut();
804        let slot = entries.len();
805        entries.push(InlineCacheEntry::Empty);
806        self.inline_cache_slots.insert(op_offset, slot);
807    }
808
809    pub(crate) fn inline_cache_slot(&self, op_offset: usize) -> Option<usize> {
810        self.inline_cache_slots.get(&op_offset).copied()
811    }
812
813    pub(crate) fn inline_cache_entry(&self, slot: usize) -> InlineCacheEntry {
814        self.inline_caches
815            .borrow()
816            .get(slot)
817            .cloned()
818            .unwrap_or(InlineCacheEntry::Empty)
819    }
820
821    pub(crate) fn set_inline_cache_entry(&self, slot: usize, entry: InlineCacheEntry) {
822        if let Some(existing) = self.inline_caches.borrow_mut().get_mut(slot) {
823            *existing = entry;
824        }
825    }
826
827    pub(crate) fn freeze_for_cache(&self) -> CachedChunk {
828        CachedChunk {
829            code: self.code.clone(),
830            constants: self.constants.clone(),
831            lines: self.lines.clone(),
832            columns: self.columns.clone(),
833            source_file: self.source_file.clone(),
834            current_col: self.current_col,
835            functions: self
836                .functions
837                .iter()
838                .map(|function| function.freeze_for_cache())
839                .collect(),
840            inline_cache_slots: self.inline_cache_slots.clone(),
841            local_slots: self.local_slots.clone(),
842        }
843    }
844
845    pub(crate) fn from_cached(cached: &CachedChunk) -> Self {
846        let inline_cache_count = cached.inline_cache_slots.len();
847        Self {
848            code: cached.code.clone(),
849            constants: cached.constants.clone(),
850            lines: cached.lines.clone(),
851            columns: cached.columns.clone(),
852            source_file: cached.source_file.clone(),
853            current_col: cached.current_col,
854            functions: cached
855                .functions
856                .iter()
857                .map(|function| Rc::new(CompiledFunction::from_cached(function)))
858                .collect(),
859            inline_cache_slots: cached.inline_cache_slots.clone(),
860            inline_caches: Rc::new(RefCell::new(vec![
861                InlineCacheEntry::Empty;
862                inline_cache_count
863            ])),
864            local_slots: cached.local_slots.clone(),
865        }
866    }
867
868    pub(crate) fn add_local_slot(
869        &mut self,
870        name: String,
871        mutable: bool,
872        scope_depth: usize,
873    ) -> u16 {
874        let idx = self.local_slots.len();
875        self.local_slots.push(LocalSlotInfo {
876            name,
877            mutable,
878            scope_depth,
879        });
880        idx as u16
881    }
882
883    #[cfg(test)]
884    pub(crate) fn inline_cache_entries(&self) -> Vec<InlineCacheEntry> {
885        self.inline_caches.borrow().clone()
886    }
887
888    /// Read a u64 argument at the given position.
889    pub fn read_u64(&self, pos: usize) -> u64 {
890        u64::from_be_bytes([
891            self.code[pos],
892            self.code[pos + 1],
893            self.code[pos + 2],
894            self.code[pos + 3],
895            self.code[pos + 4],
896            self.code[pos + 5],
897            self.code[pos + 6],
898            self.code[pos + 7],
899        ])
900    }
901
902    /// Disassemble for debugging.
903    pub fn disassemble(&self, name: &str) -> String {
904        let mut out = format!("== {name} ==\n");
905        let mut ip = 0;
906        while ip < self.code.len() {
907            let op = self.code[ip];
908            let line = self.lines.get(ip).copied().unwrap_or(0);
909            out.push_str(&format!("{:04} [{:>4}] ", ip, line));
910            ip += 1;
911
912            match op {
913                x if x == Op::Constant as u8 => {
914                    let idx = self.read_u16(ip);
915                    ip += 2;
916                    let val = &self.constants[idx as usize];
917                    out.push_str(&format!("CONSTANT {:>4} ({})\n", idx, val));
918                }
919                x if x == Op::Nil as u8 => out.push_str("NIL\n"),
920                x if x == Op::True as u8 => out.push_str("TRUE\n"),
921                x if x == Op::False as u8 => out.push_str("FALSE\n"),
922                x if x == Op::GetVar as u8 => {
923                    let idx = self.read_u16(ip);
924                    ip += 2;
925                    out.push_str(&format!(
926                        "GET_VAR {:>4} ({})\n",
927                        idx, self.constants[idx as usize]
928                    ));
929                }
930                x if x == Op::DefLet as u8 => {
931                    let idx = self.read_u16(ip);
932                    ip += 2;
933                    out.push_str(&format!(
934                        "DEF_LET {:>4} ({})\n",
935                        idx, self.constants[idx as usize]
936                    ));
937                }
938                x if x == Op::DefVar as u8 => {
939                    let idx = self.read_u16(ip);
940                    ip += 2;
941                    out.push_str(&format!(
942                        "DEF_VAR {:>4} ({})\n",
943                        idx, self.constants[idx as usize]
944                    ));
945                }
946                x if x == Op::SetVar as u8 => {
947                    let idx = self.read_u16(ip);
948                    ip += 2;
949                    out.push_str(&format!(
950                        "SET_VAR {:>4} ({})\n",
951                        idx, self.constants[idx as usize]
952                    ));
953                }
954                x if x == Op::GetLocalSlot as u8 => {
955                    let slot = self.read_u16(ip);
956                    ip += 2;
957                    out.push_str(&format!("GET_LOCAL_SLOT {:>4}", slot));
958                    if let Some(info) = self.local_slots.get(slot as usize) {
959                        out.push_str(&format!(" ({})", info.name));
960                    }
961                    out.push('\n');
962                }
963                x if x == Op::DefLocalSlot as u8 => {
964                    let slot = self.read_u16(ip);
965                    ip += 2;
966                    out.push_str(&format!("DEF_LOCAL_SLOT {:>4}", slot));
967                    if let Some(info) = self.local_slots.get(slot as usize) {
968                        out.push_str(&format!(" ({})", info.name));
969                    }
970                    out.push('\n');
971                }
972                x if x == Op::SetLocalSlot as u8 => {
973                    let slot = self.read_u16(ip);
974                    ip += 2;
975                    out.push_str(&format!("SET_LOCAL_SLOT {:>4}", slot));
976                    if let Some(info) = self.local_slots.get(slot as usize) {
977                        out.push_str(&format!(" ({})", info.name));
978                    }
979                    out.push('\n');
980                }
981                x if x == Op::PushScope as u8 => out.push_str("PUSH_SCOPE\n"),
982                x if x == Op::PopScope as u8 => out.push_str("POP_SCOPE\n"),
983                x if x == Op::Add as u8 => out.push_str("ADD\n"),
984                x if x == Op::Sub as u8 => out.push_str("SUB\n"),
985                x if x == Op::Mul as u8 => out.push_str("MUL\n"),
986                x if x == Op::Div as u8 => out.push_str("DIV\n"),
987                x if x == Op::Mod as u8 => out.push_str("MOD\n"),
988                x if x == Op::Pow as u8 => out.push_str("POW\n"),
989                x if x == Op::Negate as u8 => out.push_str("NEGATE\n"),
990                x if x == Op::Equal as u8 => out.push_str("EQUAL\n"),
991                x if x == Op::NotEqual as u8 => out.push_str("NOT_EQUAL\n"),
992                x if x == Op::Less as u8 => out.push_str("LESS\n"),
993                x if x == Op::Greater as u8 => out.push_str("GREATER\n"),
994                x if x == Op::LessEqual as u8 => out.push_str("LESS_EQUAL\n"),
995                x if x == Op::GreaterEqual as u8 => out.push_str("GREATER_EQUAL\n"),
996                x if x == Op::Contains as u8 => out.push_str("CONTAINS\n"),
997                x if x == Op::Not as u8 => out.push_str("NOT\n"),
998                x if x == Op::Jump as u8 => {
999                    let target = self.read_u16(ip);
1000                    ip += 2;
1001                    out.push_str(&format!("JUMP {:>4}\n", target));
1002                }
1003                x if x == Op::JumpIfFalse as u8 => {
1004                    let target = self.read_u16(ip);
1005                    ip += 2;
1006                    out.push_str(&format!("JUMP_IF_FALSE {:>4}\n", target));
1007                }
1008                x if x == Op::JumpIfTrue as u8 => {
1009                    let target = self.read_u16(ip);
1010                    ip += 2;
1011                    out.push_str(&format!("JUMP_IF_TRUE {:>4}\n", target));
1012                }
1013                x if x == Op::Pop as u8 => out.push_str("POP\n"),
1014                x if x == Op::Call as u8 => {
1015                    let argc = self.code[ip];
1016                    ip += 1;
1017                    out.push_str(&format!("CALL {:>4}\n", argc));
1018                }
1019                x if x == Op::TailCall as u8 => {
1020                    let argc = self.code[ip];
1021                    ip += 1;
1022                    out.push_str(&format!("TAIL_CALL {:>4}\n", argc));
1023                }
1024                x if x == Op::Return as u8 => out.push_str("RETURN\n"),
1025                x if x == Op::Closure as u8 => {
1026                    let idx = self.read_u16(ip);
1027                    ip += 2;
1028                    out.push_str(&format!("CLOSURE {:>4}\n", idx));
1029                }
1030                x if x == Op::BuildList as u8 => {
1031                    let count = self.read_u16(ip);
1032                    ip += 2;
1033                    out.push_str(&format!("BUILD_LIST {:>4}\n", count));
1034                }
1035                x if x == Op::BuildDict as u8 => {
1036                    let count = self.read_u16(ip);
1037                    ip += 2;
1038                    out.push_str(&format!("BUILD_DICT {:>4}\n", count));
1039                }
1040                x if x == Op::Subscript as u8 => out.push_str("SUBSCRIPT\n"),
1041                x if x == Op::SubscriptOpt as u8 => out.push_str("SUBSCRIPT_OPT\n"),
1042                x if x == Op::Slice as u8 => out.push_str("SLICE\n"),
1043                x if x == Op::GetProperty as u8 => {
1044                    let idx = self.read_u16(ip);
1045                    ip += 2;
1046                    out.push_str(&format!(
1047                        "GET_PROPERTY {:>4} ({})\n",
1048                        idx, self.constants[idx as usize]
1049                    ));
1050                }
1051                x if x == Op::GetPropertyOpt as u8 => {
1052                    let idx = self.read_u16(ip);
1053                    ip += 2;
1054                    out.push_str(&format!(
1055                        "GET_PROPERTY_OPT {:>4} ({})\n",
1056                        idx, self.constants[idx as usize]
1057                    ));
1058                }
1059                x if x == Op::SetProperty as u8 => {
1060                    let idx = self.read_u16(ip);
1061                    ip += 2;
1062                    out.push_str(&format!(
1063                        "SET_PROPERTY {:>4} ({})\n",
1064                        idx, self.constants[idx as usize]
1065                    ));
1066                }
1067                x if x == Op::SetSubscript as u8 => {
1068                    let idx = self.read_u16(ip);
1069                    ip += 2;
1070                    out.push_str(&format!(
1071                        "SET_SUBSCRIPT {:>4} ({})\n",
1072                        idx, self.constants[idx as usize]
1073                    ));
1074                }
1075                x if x == Op::MethodCall as u8 => {
1076                    let idx = self.read_u16(ip);
1077                    ip += 2;
1078                    let argc = self.code[ip];
1079                    ip += 1;
1080                    out.push_str(&format!(
1081                        "METHOD_CALL {:>4} ({}) argc={}\n",
1082                        idx, self.constants[idx as usize], argc
1083                    ));
1084                }
1085                x if x == Op::MethodCallOpt as u8 => {
1086                    let idx = self.read_u16(ip);
1087                    ip += 2;
1088                    let argc = self.code[ip];
1089                    ip += 1;
1090                    out.push_str(&format!(
1091                        "METHOD_CALL_OPT {:>4} ({}) argc={}\n",
1092                        idx, self.constants[idx as usize], argc
1093                    ));
1094                }
1095                x if x == Op::Concat as u8 => {
1096                    let count = self.read_u16(ip);
1097                    ip += 2;
1098                    out.push_str(&format!("CONCAT {:>4}\n", count));
1099                }
1100                x if x == Op::IterInit as u8 => out.push_str("ITER_INIT\n"),
1101                x if x == Op::IterNext as u8 => {
1102                    let target = self.read_u16(ip);
1103                    ip += 2;
1104                    out.push_str(&format!("ITER_NEXT {:>4}\n", target));
1105                }
1106                x if x == Op::Throw as u8 => out.push_str("THROW\n"),
1107                x if x == Op::TryCatchSetup as u8 => {
1108                    let target = self.read_u16(ip);
1109                    ip += 2;
1110                    out.push_str(&format!("TRY_CATCH_SETUP {:>4}\n", target));
1111                }
1112                x if x == Op::PopHandler as u8 => out.push_str("POP_HANDLER\n"),
1113                x if x == Op::Pipe as u8 => out.push_str("PIPE\n"),
1114                x if x == Op::Parallel as u8 => out.push_str("PARALLEL\n"),
1115                x if x == Op::ParallelMap as u8 => out.push_str("PARALLEL_MAP\n"),
1116                x if x == Op::ParallelMapStream as u8 => out.push_str("PARALLEL_MAP_STREAM\n"),
1117                x if x == Op::ParallelSettle as u8 => out.push_str("PARALLEL_SETTLE\n"),
1118                x if x == Op::Spawn as u8 => out.push_str("SPAWN\n"),
1119                x if x == Op::Import as u8 => {
1120                    let idx = self.read_u16(ip);
1121                    ip += 2;
1122                    out.push_str(&format!(
1123                        "IMPORT {:>4} ({})\n",
1124                        idx, self.constants[idx as usize]
1125                    ));
1126                }
1127                x if x == Op::SelectiveImport as u8 => {
1128                    let path_idx = self.read_u16(ip);
1129                    ip += 2;
1130                    let names_idx = self.read_u16(ip);
1131                    ip += 2;
1132                    out.push_str(&format!(
1133                        "SELECTIVE_IMPORT {:>4} ({}) names: {:>4} ({})\n",
1134                        path_idx,
1135                        self.constants[path_idx as usize],
1136                        names_idx,
1137                        self.constants[names_idx as usize]
1138                    ));
1139                }
1140                x if x == Op::SyncMutexEnter as u8 => {
1141                    let idx = self.read_u16(ip);
1142                    ip += 2;
1143                    out.push_str(&format!(
1144                        "SYNC_MUTEX_ENTER {:>4} ({})\n",
1145                        idx, self.constants[idx as usize]
1146                    ));
1147                }
1148                x if x == Op::DeadlineSetup as u8 => out.push_str("DEADLINE_SETUP\n"),
1149                x if x == Op::DeadlineEnd as u8 => out.push_str("DEADLINE_END\n"),
1150                x if x == Op::BuildEnum as u8 => {
1151                    let enum_idx = self.read_u16(ip);
1152                    ip += 2;
1153                    let variant_idx = self.read_u16(ip);
1154                    ip += 2;
1155                    let field_count = self.read_u16(ip);
1156                    ip += 2;
1157                    out.push_str(&format!(
1158                        "BUILD_ENUM {:>4} ({}) {:>4} ({}) fields={}\n",
1159                        enum_idx,
1160                        self.constants[enum_idx as usize],
1161                        variant_idx,
1162                        self.constants[variant_idx as usize],
1163                        field_count
1164                    ));
1165                }
1166                x if x == Op::MatchEnum as u8 => {
1167                    let enum_idx = self.read_u16(ip);
1168                    ip += 2;
1169                    let variant_idx = self.read_u16(ip);
1170                    ip += 2;
1171                    out.push_str(&format!(
1172                        "MATCH_ENUM {:>4} ({}) {:>4} ({})\n",
1173                        enum_idx,
1174                        self.constants[enum_idx as usize],
1175                        variant_idx,
1176                        self.constants[variant_idx as usize]
1177                    ));
1178                }
1179                x if x == Op::PopIterator as u8 => out.push_str("POP_ITERATOR\n"),
1180                x if x == Op::TryUnwrap as u8 => out.push_str("TRY_UNWRAP\n"),
1181                x if x == Op::TryWrapOk as u8 => out.push_str("TRY_WRAP_OK\n"),
1182                x if x == Op::CallSpread as u8 => out.push_str("CALL_SPREAD\n"),
1183                x if x == Op::CallBuiltin as u8 => {
1184                    let id = self.read_u64(ip);
1185                    ip += 8;
1186                    let idx = self.read_u16(ip);
1187                    ip += 2;
1188                    let argc = self.code[ip];
1189                    ip += 1;
1190                    out.push_str(&format!(
1191                        "CALL_BUILTIN {id:#018x} {:>4} ({}) argc={}\n",
1192                        idx, self.constants[idx as usize], argc
1193                    ));
1194                }
1195                x if x == Op::CallBuiltinSpread as u8 => {
1196                    let id = self.read_u64(ip);
1197                    ip += 8;
1198                    let idx = self.read_u16(ip);
1199                    ip += 2;
1200                    out.push_str(&format!(
1201                        "CALL_BUILTIN_SPREAD {id:#018x} {:>4} ({})\n",
1202                        idx, self.constants[idx as usize]
1203                    ));
1204                }
1205                x if x == Op::MethodCallSpread as u8 => {
1206                    let idx = self.read_u16(ip + 1);
1207                    ip += 2;
1208                    out.push_str(&format!("METHOD_CALL_SPREAD {idx}\n"));
1209                }
1210                x if x == Op::Dup as u8 => out.push_str("DUP\n"),
1211                x if x == Op::Swap as u8 => out.push_str("SWAP\n"),
1212                x if x == Op::AddInt as u8 => out.push_str("ADD_INT\n"),
1213                x if x == Op::SubInt as u8 => out.push_str("SUB_INT\n"),
1214                x if x == Op::MulInt as u8 => out.push_str("MUL_INT\n"),
1215                x if x == Op::DivInt as u8 => out.push_str("DIV_INT\n"),
1216                x if x == Op::ModInt as u8 => out.push_str("MOD_INT\n"),
1217                x if x == Op::AddFloat as u8 => out.push_str("ADD_FLOAT\n"),
1218                x if x == Op::SubFloat as u8 => out.push_str("SUB_FLOAT\n"),
1219                x if x == Op::MulFloat as u8 => out.push_str("MUL_FLOAT\n"),
1220                x if x == Op::DivFloat as u8 => out.push_str("DIV_FLOAT\n"),
1221                x if x == Op::ModFloat as u8 => out.push_str("MOD_FLOAT\n"),
1222                x if x == Op::EqualInt as u8 => out.push_str("EQUAL_INT\n"),
1223                x if x == Op::NotEqualInt as u8 => out.push_str("NOT_EQUAL_INT\n"),
1224                x if x == Op::LessInt as u8 => out.push_str("LESS_INT\n"),
1225                x if x == Op::GreaterInt as u8 => out.push_str("GREATER_INT\n"),
1226                x if x == Op::LessEqualInt as u8 => out.push_str("LESS_EQUAL_INT\n"),
1227                x if x == Op::GreaterEqualInt as u8 => out.push_str("GREATER_EQUAL_INT\n"),
1228                x if x == Op::EqualFloat as u8 => out.push_str("EQUAL_FLOAT\n"),
1229                x if x == Op::NotEqualFloat as u8 => out.push_str("NOT_EQUAL_FLOAT\n"),
1230                x if x == Op::LessFloat as u8 => out.push_str("LESS_FLOAT\n"),
1231                x if x == Op::GreaterFloat as u8 => out.push_str("GREATER_FLOAT\n"),
1232                x if x == Op::LessEqualFloat as u8 => out.push_str("LESS_EQUAL_FLOAT\n"),
1233                x if x == Op::GreaterEqualFloat as u8 => out.push_str("GREATER_EQUAL_FLOAT\n"),
1234                x if x == Op::EqualBool as u8 => out.push_str("EQUAL_BOOL\n"),
1235                x if x == Op::NotEqualBool as u8 => out.push_str("NOT_EQUAL_BOOL\n"),
1236                x if x == Op::EqualString as u8 => out.push_str("EQUAL_STRING\n"),
1237                x if x == Op::NotEqualString as u8 => out.push_str("NOT_EQUAL_STRING\n"),
1238                x if x == Op::Yield as u8 => out.push_str("YIELD\n"),
1239                _ => {
1240                    out.push_str(&format!("UNKNOWN(0x{:02x})\n", op));
1241                }
1242            }
1243        }
1244        out
1245    }
1246}
1247
1248impl Default for Chunk {
1249    fn default() -> Self {
1250        Self::new()
1251    }
1252}
1253
1254#[cfg(test)]
1255mod tests {
1256    use super::Op;
1257
1258    #[test]
1259    fn op_from_byte_matches_repr_order() {
1260        for (byte, op) in Op::ALL.iter().copied().enumerate() {
1261            assert_eq!(byte as u8, op as u8);
1262            assert_eq!(Op::from_byte(byte as u8), Some(op));
1263        }
1264        assert_eq!(Op::from_byte(Op::ALL.len() as u8), None);
1265    }
1266}