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 shape. Harn collection values are immutable or
386/// copy-on-write at the VM level, so receiver-kind caches do not need
387/// invalidation. Struct field caches guard on the field name at the cached
388/// slot before reading the indexed field.
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, PartialEq, Eq)]
404pub(crate) enum PropertyCacheTarget {
405    DictField(Rc<str>),
406    StructField { field_name: Rc<str>, index: usize },
407    ListCount,
408    ListEmpty,
409    ListFirst,
410    ListLast,
411    StringCount,
412    StringEmpty,
413    PairFirst,
414    PairSecond,
415    EnumVariant,
416    EnumFields,
417}
418
419#[derive(Debug, Clone, Copy, PartialEq, Eq)]
420pub(crate) enum MethodCacheTarget {
421    ListCount,
422    ListEmpty,
423    StringCount,
424    StringEmpty,
425    DictCount,
426    RangeCount,
427    RangeLen,
428    RangeEmpty,
429    RangeFirst,
430    RangeLast,
431    SetCount,
432    SetLen,
433    SetEmpty,
434}
435
436/// Debug metadata for a slot-indexed local in a compiled chunk.
437#[derive(Debug, Clone, PartialEq, Eq)]
438pub struct LocalSlotInfo {
439    pub name: String,
440    pub mutable: bool,
441    pub scope_depth: usize,
442}
443
444impl fmt::Display for Constant {
445    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
446        match self {
447            Constant::Int(n) => write!(f, "{n}"),
448            Constant::Float(n) => write!(f, "{n}"),
449            Constant::String(s) => write!(f, "\"{s}\""),
450            Constant::Bool(b) => write!(f, "{b}"),
451            Constant::Nil => write!(f, "nil"),
452            Constant::Duration(ms) => write!(f, "{ms}ms"),
453        }
454    }
455}
456
457/// A compiled chunk of bytecode.
458#[derive(Debug, Clone)]
459pub struct Chunk {
460    /// The bytecode instructions.
461    pub code: Vec<u8>,
462    /// Constant pool.
463    pub constants: Vec<Constant>,
464    /// Source line numbers for each instruction (for error reporting).
465    pub lines: Vec<u32>,
466    /// Source column numbers for each instruction (for error reporting).
467    /// Parallel to `lines`; 0 means no column info available.
468    pub columns: Vec<u32>,
469    /// Source file that this chunk was compiled from, when known. Set for
470    /// chunks compiled from imported modules so runtime errors can report
471    /// the correct file path for each frame instead of always pointing at
472    /// the entry-point pipeline.
473    pub source_file: Option<String>,
474    /// Current column to use when emitting instructions (set by compiler).
475    current_col: u32,
476    /// Compiled function bodies (for closures).
477    pub functions: Vec<CompiledFunctionRef>,
478    /// Instruction offset to inline-cache slot. Slots are assigned at emit time
479    /// for cacheable instructions while bytecode bytes remain immutable.
480    inline_cache_slots: BTreeMap<usize, usize>,
481    /// Shared cache entries so cloned chunks in call frames warm the same side
482    /// table as the compiled chunk used by tests/debugging.
483    inline_caches: Rc<RefCell<Vec<InlineCacheEntry>>>,
484    /// Source-name metadata for slot-indexed locals in this chunk.
485    pub(crate) local_slots: Vec<LocalSlotInfo>,
486}
487
488pub type ChunkRef = Rc<Chunk>;
489pub type CompiledFunctionRef = Rc<CompiledFunction>;
490
491#[derive(Debug, Clone)]
492pub(crate) struct CachedChunk {
493    code: Vec<u8>,
494    constants: Vec<Constant>,
495    lines: Vec<u32>,
496    columns: Vec<u32>,
497    source_file: Option<String>,
498    current_col: u32,
499    functions: Vec<CachedCompiledFunction>,
500    inline_cache_slots: BTreeMap<usize, usize>,
501    local_slots: Vec<LocalSlotInfo>,
502}
503
504#[derive(Debug, Clone)]
505pub(crate) struct CachedCompiledFunction {
506    name: String,
507    type_params: Vec<String>,
508    nominal_type_names: Vec<String>,
509    params: Vec<ParamSlot>,
510    default_start: Option<usize>,
511    chunk: CachedChunk,
512    is_generator: bool,
513    is_stream: bool,
514    has_rest_param: bool,
515}
516
517/// One parameter slot of a compiled user-defined function. Carries the
518/// declared name, the (optional) declared type expression, and a flag
519/// for whether a default value was provided. The runtime consults the
520/// type expression in `bind_param_slots` to enforce declared types
521/// against the values supplied at the call site.
522#[derive(Debug, Clone)]
523pub struct ParamSlot {
524    pub name: String,
525    /// Declared parameter type. `None` for untyped parameters (gradual
526    /// typing); the runtime skips type assertion when absent.
527    pub type_expr: Option<TypeExpr>,
528    /// True when the parameter has a default-value clause. Diagnostic
529    /// only — the canonical authority for arity ranges is
530    /// [`CompiledFunction::default_start`].
531    pub has_default: bool,
532}
533
534impl ParamSlot {
535    /// Build a [`ParamSlot`] from a parser-side [`harn_parser::TypedParam`].
536    /// Centralizes the conversion so every compile path stays in lockstep.
537    pub fn from_typed_param(param: &harn_parser::TypedParam) -> Self {
538        Self {
539            name: param.name.clone(),
540            type_expr: param.type_expr.clone(),
541            has_default: param.default_value.is_some(),
542        }
543    }
544
545    /// Build a `Vec<ParamSlot>` from a slice of parser-side typed
546    /// parameters. Used pervasively at compile sites instead of
547    /// `TypedParam::names` (which discarded the type info we now need
548    /// at runtime).
549    pub fn vec_from_typed(params: &[harn_parser::TypedParam]) -> Vec<Self> {
550        params.iter().map(Self::from_typed_param).collect()
551    }
552}
553
554/// A compiled function (closure body).
555#[derive(Debug, Clone)]
556pub struct CompiledFunction {
557    pub name: String,
558    /// Generic type parameters declared by this function. Runtime
559    /// validation treats these as static-only constraints because the VM
560    /// does not monomorphize function bodies.
561    pub type_params: Vec<String>,
562    /// User-defined struct and enum names visible when this function was
563    /// compiled. These are the only non-primitive named types with runtime
564    /// nominal identity; aliases and interfaces remain static-only.
565    pub nominal_type_names: Vec<String>,
566    pub params: Vec<ParamSlot>,
567    /// Index of the first parameter with a default value, or None if all required.
568    pub default_start: Option<usize>,
569    pub chunk: ChunkRef,
570    /// True if the function body contains `yield` expressions (generator function).
571    pub is_generator: bool,
572    /// True if the function was declared as `gen fn` and should return Stream.
573    pub is_stream: bool,
574    /// True if the last parameter is a rest parameter (`...name`).
575    pub has_rest_param: bool,
576}
577
578impl CompiledFunction {
579    /// Returns just the parameter names — convenience for code paths that
580    /// don't care about types or defaults.
581    pub fn param_names(&self) -> impl Iterator<Item = &str> {
582        self.params.iter().map(|p| p.name.as_str())
583    }
584
585    /// Number of required parameters (those before `default_start`).
586    pub fn required_param_count(&self) -> usize {
587        self.default_start.unwrap_or(self.params.len())
588    }
589
590    pub fn declares_type_param(&self, name: &str) -> bool {
591        self.type_params.iter().any(|param| param == name)
592    }
593
594    pub fn has_nominal_type(&self, name: &str) -> bool {
595        self.nominal_type_names.iter().any(|ty| ty == name)
596    }
597
598    pub(crate) fn freeze_for_cache(&self) -> CachedCompiledFunction {
599        CachedCompiledFunction {
600            name: self.name.clone(),
601            type_params: self.type_params.clone(),
602            nominal_type_names: self.nominal_type_names.clone(),
603            params: self.params.clone(),
604            default_start: self.default_start,
605            chunk: self.chunk.freeze_for_cache(),
606            is_generator: self.is_generator,
607            is_stream: self.is_stream,
608            has_rest_param: self.has_rest_param,
609        }
610    }
611
612    pub(crate) fn from_cached(cached: &CachedCompiledFunction) -> Self {
613        Self {
614            name: cached.name.clone(),
615            type_params: cached.type_params.clone(),
616            nominal_type_names: cached.nominal_type_names.clone(),
617            params: cached.params.clone(),
618            default_start: cached.default_start,
619            chunk: Rc::new(Chunk::from_cached(&cached.chunk)),
620            is_generator: cached.is_generator,
621            is_stream: cached.is_stream,
622            has_rest_param: cached.has_rest_param,
623        }
624    }
625}
626
627impl Chunk {
628    pub fn new() -> Self {
629        Self {
630            code: Vec::new(),
631            constants: Vec::new(),
632            lines: Vec::new(),
633            columns: Vec::new(),
634            source_file: None,
635            current_col: 0,
636            functions: Vec::new(),
637            inline_cache_slots: BTreeMap::new(),
638            inline_caches: Rc::new(RefCell::new(Vec::new())),
639            local_slots: Vec::new(),
640        }
641    }
642
643    /// Set the current column for subsequent emit calls.
644    pub fn set_column(&mut self, col: u32) {
645        self.current_col = col;
646    }
647
648    /// Add a constant and return its index.
649    pub fn add_constant(&mut self, constant: Constant) -> u16 {
650        for (i, c) in self.constants.iter().enumerate() {
651            if c == &constant {
652                return i as u16;
653            }
654        }
655        let idx = self.constants.len();
656        self.constants.push(constant);
657        idx as u16
658    }
659
660    /// Emit a single-byte instruction.
661    pub fn emit(&mut self, op: Op, line: u32) {
662        let col = self.current_col;
663        self.code.push(op as u8);
664        self.lines.push(line);
665        self.columns.push(col);
666    }
667
668    /// Emit an instruction with a u16 argument.
669    pub fn emit_u16(&mut self, op: Op, arg: u16, line: u32) {
670        let col = self.current_col;
671        let op_offset = self.code.len();
672        self.code.push(op as u8);
673        self.code.push((arg >> 8) as u8);
674        self.code.push((arg & 0xFF) as u8);
675        self.lines.push(line);
676        self.lines.push(line);
677        self.lines.push(line);
678        self.columns.push(col);
679        self.columns.push(col);
680        self.columns.push(col);
681        if matches!(
682            op,
683            Op::GetProperty | Op::GetPropertyOpt | Op::MethodCallSpread
684        ) {
685            self.register_inline_cache(op_offset);
686        }
687    }
688
689    /// Emit an instruction with a u8 argument.
690    pub fn emit_u8(&mut self, op: Op, arg: u8, line: u32) {
691        let col = self.current_col;
692        self.code.push(op as u8);
693        self.code.push(arg);
694        self.lines.push(line);
695        self.lines.push(line);
696        self.columns.push(col);
697        self.columns.push(col);
698    }
699
700    /// Emit a direct builtin call.
701    pub fn emit_call_builtin(
702        &mut self,
703        id: crate::BuiltinId,
704        name_idx: u16,
705        arg_count: u8,
706        line: u32,
707    ) {
708        let col = self.current_col;
709        self.code.push(Op::CallBuiltin as u8);
710        self.code.extend_from_slice(&id.raw().to_be_bytes());
711        self.code.push((name_idx >> 8) as u8);
712        self.code.push((name_idx & 0xFF) as u8);
713        self.code.push(arg_count);
714        for _ in 0..12 {
715            self.lines.push(line);
716            self.columns.push(col);
717        }
718    }
719
720    /// Emit a direct builtin spread call.
721    pub fn emit_call_builtin_spread(&mut self, id: crate::BuiltinId, name_idx: u16, line: u32) {
722        let col = self.current_col;
723        self.code.push(Op::CallBuiltinSpread as u8);
724        self.code.extend_from_slice(&id.raw().to_be_bytes());
725        self.code.push((name_idx >> 8) as u8);
726        self.code.push((name_idx & 0xFF) as u8);
727        for _ in 0..11 {
728            self.lines.push(line);
729            self.columns.push(col);
730        }
731    }
732
733    /// Emit a method call: op + u16 (method name) + u8 (arg count).
734    pub fn emit_method_call(&mut self, name_idx: u16, arg_count: u8, line: u32) {
735        self.emit_method_call_inner(Op::MethodCall, name_idx, arg_count, line);
736    }
737
738    /// Emit an optional method call (?.) — returns nil if receiver is nil.
739    pub fn emit_method_call_opt(&mut self, name_idx: u16, arg_count: u8, line: u32) {
740        self.emit_method_call_inner(Op::MethodCallOpt, name_idx, arg_count, line);
741    }
742
743    fn emit_method_call_inner(&mut self, op: Op, name_idx: u16, arg_count: u8, line: u32) {
744        let col = self.current_col;
745        let op_offset = self.code.len();
746        self.code.push(op as u8);
747        self.code.push((name_idx >> 8) as u8);
748        self.code.push((name_idx & 0xFF) as u8);
749        self.code.push(arg_count);
750        self.lines.push(line);
751        self.lines.push(line);
752        self.lines.push(line);
753        self.lines.push(line);
754        self.columns.push(col);
755        self.columns.push(col);
756        self.columns.push(col);
757        self.columns.push(col);
758        self.register_inline_cache(op_offset);
759    }
760
761    /// Current code offset (for jump patching).
762    pub fn current_offset(&self) -> usize {
763        self.code.len()
764    }
765
766    /// Emit a jump instruction with a placeholder offset. Returns the position to patch.
767    pub fn emit_jump(&mut self, op: Op, line: u32) -> usize {
768        let col = self.current_col;
769        self.code.push(op as u8);
770        let patch_pos = self.code.len();
771        self.code.push(0xFF);
772        self.code.push(0xFF);
773        self.lines.push(line);
774        self.lines.push(line);
775        self.lines.push(line);
776        self.columns.push(col);
777        self.columns.push(col);
778        self.columns.push(col);
779        patch_pos
780    }
781
782    /// Patch a jump instruction at the given position to jump to the current offset.
783    pub fn patch_jump(&mut self, patch_pos: usize) {
784        let target = self.code.len() as u16;
785        self.code[patch_pos] = (target >> 8) as u8;
786        self.code[patch_pos + 1] = (target & 0xFF) as u8;
787    }
788
789    /// Patch a jump to a specific target position.
790    pub fn patch_jump_to(&mut self, patch_pos: usize, target: usize) {
791        let target = target as u16;
792        self.code[patch_pos] = (target >> 8) as u8;
793        self.code[patch_pos + 1] = (target & 0xFF) as u8;
794    }
795
796    /// Read a u16 argument at the given position.
797    pub fn read_u16(&self, pos: usize) -> u16 {
798        ((self.code[pos] as u16) << 8) | (self.code[pos + 1] as u16)
799    }
800
801    fn register_inline_cache(&mut self, op_offset: usize) {
802        if self.inline_cache_slots.contains_key(&op_offset) {
803            return;
804        }
805        let mut entries = self.inline_caches.borrow_mut();
806        let slot = entries.len();
807        entries.push(InlineCacheEntry::Empty);
808        self.inline_cache_slots.insert(op_offset, slot);
809    }
810
811    pub(crate) fn inline_cache_slot(&self, op_offset: usize) -> Option<usize> {
812        self.inline_cache_slots.get(&op_offset).copied()
813    }
814
815    pub(crate) fn inline_cache_entry(&self, slot: usize) -> InlineCacheEntry {
816        self.inline_caches
817            .borrow()
818            .get(slot)
819            .cloned()
820            .unwrap_or(InlineCacheEntry::Empty)
821    }
822
823    pub(crate) fn set_inline_cache_entry(&self, slot: usize, entry: InlineCacheEntry) {
824        if let Some(existing) = self.inline_caches.borrow_mut().get_mut(slot) {
825            *existing = entry;
826        }
827    }
828
829    pub(crate) fn freeze_for_cache(&self) -> CachedChunk {
830        CachedChunk {
831            code: self.code.clone(),
832            constants: self.constants.clone(),
833            lines: self.lines.clone(),
834            columns: self.columns.clone(),
835            source_file: self.source_file.clone(),
836            current_col: self.current_col,
837            functions: self
838                .functions
839                .iter()
840                .map(|function| function.freeze_for_cache())
841                .collect(),
842            inline_cache_slots: self.inline_cache_slots.clone(),
843            local_slots: self.local_slots.clone(),
844        }
845    }
846
847    pub(crate) fn from_cached(cached: &CachedChunk) -> Self {
848        let inline_cache_count = cached.inline_cache_slots.len();
849        Self {
850            code: cached.code.clone(),
851            constants: cached.constants.clone(),
852            lines: cached.lines.clone(),
853            columns: cached.columns.clone(),
854            source_file: cached.source_file.clone(),
855            current_col: cached.current_col,
856            functions: cached
857                .functions
858                .iter()
859                .map(|function| Rc::new(CompiledFunction::from_cached(function)))
860                .collect(),
861            inline_cache_slots: cached.inline_cache_slots.clone(),
862            inline_caches: Rc::new(RefCell::new(vec![
863                InlineCacheEntry::Empty;
864                inline_cache_count
865            ])),
866            local_slots: cached.local_slots.clone(),
867        }
868    }
869
870    pub(crate) fn add_local_slot(
871        &mut self,
872        name: String,
873        mutable: bool,
874        scope_depth: usize,
875    ) -> u16 {
876        let idx = self.local_slots.len();
877        self.local_slots.push(LocalSlotInfo {
878            name,
879            mutable,
880            scope_depth,
881        });
882        idx as u16
883    }
884
885    #[cfg(test)]
886    pub(crate) fn inline_cache_entries(&self) -> Vec<InlineCacheEntry> {
887        self.inline_caches.borrow().clone()
888    }
889
890    /// Read a u64 argument at the given position.
891    pub fn read_u64(&self, pos: usize) -> u64 {
892        u64::from_be_bytes([
893            self.code[pos],
894            self.code[pos + 1],
895            self.code[pos + 2],
896            self.code[pos + 3],
897            self.code[pos + 4],
898            self.code[pos + 5],
899            self.code[pos + 6],
900            self.code[pos + 7],
901        ])
902    }
903
904    /// Disassemble for debugging.
905    pub fn disassemble(&self, name: &str) -> String {
906        let mut out = format!("== {name} ==\n");
907        let mut ip = 0;
908        while ip < self.code.len() {
909            let op = self.code[ip];
910            let line = self.lines.get(ip).copied().unwrap_or(0);
911            out.push_str(&format!("{:04} [{:>4}] ", ip, line));
912            ip += 1;
913
914            match op {
915                x if x == Op::Constant as u8 => {
916                    let idx = self.read_u16(ip);
917                    ip += 2;
918                    let val = &self.constants[idx as usize];
919                    out.push_str(&format!("CONSTANT {:>4} ({})\n", idx, val));
920                }
921                x if x == Op::Nil as u8 => out.push_str("NIL\n"),
922                x if x == Op::True as u8 => out.push_str("TRUE\n"),
923                x if x == Op::False as u8 => out.push_str("FALSE\n"),
924                x if x == Op::GetVar as u8 => {
925                    let idx = self.read_u16(ip);
926                    ip += 2;
927                    out.push_str(&format!(
928                        "GET_VAR {:>4} ({})\n",
929                        idx, self.constants[idx as usize]
930                    ));
931                }
932                x if x == Op::DefLet as u8 => {
933                    let idx = self.read_u16(ip);
934                    ip += 2;
935                    out.push_str(&format!(
936                        "DEF_LET {:>4} ({})\n",
937                        idx, self.constants[idx as usize]
938                    ));
939                }
940                x if x == Op::DefVar as u8 => {
941                    let idx = self.read_u16(ip);
942                    ip += 2;
943                    out.push_str(&format!(
944                        "DEF_VAR {:>4} ({})\n",
945                        idx, self.constants[idx as usize]
946                    ));
947                }
948                x if x == Op::SetVar as u8 => {
949                    let idx = self.read_u16(ip);
950                    ip += 2;
951                    out.push_str(&format!(
952                        "SET_VAR {:>4} ({})\n",
953                        idx, self.constants[idx as usize]
954                    ));
955                }
956                x if x == Op::GetLocalSlot as u8 => {
957                    let slot = self.read_u16(ip);
958                    ip += 2;
959                    out.push_str(&format!("GET_LOCAL_SLOT {:>4}", slot));
960                    if let Some(info) = self.local_slots.get(slot as usize) {
961                        out.push_str(&format!(" ({})", info.name));
962                    }
963                    out.push('\n');
964                }
965                x if x == Op::DefLocalSlot as u8 => {
966                    let slot = self.read_u16(ip);
967                    ip += 2;
968                    out.push_str(&format!("DEF_LOCAL_SLOT {:>4}", slot));
969                    if let Some(info) = self.local_slots.get(slot as usize) {
970                        out.push_str(&format!(" ({})", info.name));
971                    }
972                    out.push('\n');
973                }
974                x if x == Op::SetLocalSlot as u8 => {
975                    let slot = self.read_u16(ip);
976                    ip += 2;
977                    out.push_str(&format!("SET_LOCAL_SLOT {:>4}", slot));
978                    if let Some(info) = self.local_slots.get(slot as usize) {
979                        out.push_str(&format!(" ({})", info.name));
980                    }
981                    out.push('\n');
982                }
983                x if x == Op::PushScope as u8 => out.push_str("PUSH_SCOPE\n"),
984                x if x == Op::PopScope as u8 => out.push_str("POP_SCOPE\n"),
985                x if x == Op::Add as u8 => out.push_str("ADD\n"),
986                x if x == Op::Sub as u8 => out.push_str("SUB\n"),
987                x if x == Op::Mul as u8 => out.push_str("MUL\n"),
988                x if x == Op::Div as u8 => out.push_str("DIV\n"),
989                x if x == Op::Mod as u8 => out.push_str("MOD\n"),
990                x if x == Op::Pow as u8 => out.push_str("POW\n"),
991                x if x == Op::Negate as u8 => out.push_str("NEGATE\n"),
992                x if x == Op::Equal as u8 => out.push_str("EQUAL\n"),
993                x if x == Op::NotEqual as u8 => out.push_str("NOT_EQUAL\n"),
994                x if x == Op::Less as u8 => out.push_str("LESS\n"),
995                x if x == Op::Greater as u8 => out.push_str("GREATER\n"),
996                x if x == Op::LessEqual as u8 => out.push_str("LESS_EQUAL\n"),
997                x if x == Op::GreaterEqual as u8 => out.push_str("GREATER_EQUAL\n"),
998                x if x == Op::Contains as u8 => out.push_str("CONTAINS\n"),
999                x if x == Op::Not as u8 => out.push_str("NOT\n"),
1000                x if x == Op::Jump as u8 => {
1001                    let target = self.read_u16(ip);
1002                    ip += 2;
1003                    out.push_str(&format!("JUMP {:>4}\n", target));
1004                }
1005                x if x == Op::JumpIfFalse as u8 => {
1006                    let target = self.read_u16(ip);
1007                    ip += 2;
1008                    out.push_str(&format!("JUMP_IF_FALSE {:>4}\n", target));
1009                }
1010                x if x == Op::JumpIfTrue as u8 => {
1011                    let target = self.read_u16(ip);
1012                    ip += 2;
1013                    out.push_str(&format!("JUMP_IF_TRUE {:>4}\n", target));
1014                }
1015                x if x == Op::Pop as u8 => out.push_str("POP\n"),
1016                x if x == Op::Call as u8 => {
1017                    let argc = self.code[ip];
1018                    ip += 1;
1019                    out.push_str(&format!("CALL {:>4}\n", argc));
1020                }
1021                x if x == Op::TailCall as u8 => {
1022                    let argc = self.code[ip];
1023                    ip += 1;
1024                    out.push_str(&format!("TAIL_CALL {:>4}\n", argc));
1025                }
1026                x if x == Op::Return as u8 => out.push_str("RETURN\n"),
1027                x if x == Op::Closure as u8 => {
1028                    let idx = self.read_u16(ip);
1029                    ip += 2;
1030                    out.push_str(&format!("CLOSURE {:>4}\n", idx));
1031                }
1032                x if x == Op::BuildList as u8 => {
1033                    let count = self.read_u16(ip);
1034                    ip += 2;
1035                    out.push_str(&format!("BUILD_LIST {:>4}\n", count));
1036                }
1037                x if x == Op::BuildDict as u8 => {
1038                    let count = self.read_u16(ip);
1039                    ip += 2;
1040                    out.push_str(&format!("BUILD_DICT {:>4}\n", count));
1041                }
1042                x if x == Op::Subscript as u8 => out.push_str("SUBSCRIPT\n"),
1043                x if x == Op::SubscriptOpt as u8 => out.push_str("SUBSCRIPT_OPT\n"),
1044                x if x == Op::Slice as u8 => out.push_str("SLICE\n"),
1045                x if x == Op::GetProperty as u8 => {
1046                    let idx = self.read_u16(ip);
1047                    ip += 2;
1048                    out.push_str(&format!(
1049                        "GET_PROPERTY {:>4} ({})\n",
1050                        idx, self.constants[idx as usize]
1051                    ));
1052                }
1053                x if x == Op::GetPropertyOpt as u8 => {
1054                    let idx = self.read_u16(ip);
1055                    ip += 2;
1056                    out.push_str(&format!(
1057                        "GET_PROPERTY_OPT {:>4} ({})\n",
1058                        idx, self.constants[idx as usize]
1059                    ));
1060                }
1061                x if x == Op::SetProperty as u8 => {
1062                    let idx = self.read_u16(ip);
1063                    ip += 2;
1064                    out.push_str(&format!(
1065                        "SET_PROPERTY {:>4} ({})\n",
1066                        idx, self.constants[idx as usize]
1067                    ));
1068                }
1069                x if x == Op::SetSubscript as u8 => {
1070                    let idx = self.read_u16(ip);
1071                    ip += 2;
1072                    out.push_str(&format!(
1073                        "SET_SUBSCRIPT {:>4} ({})\n",
1074                        idx, self.constants[idx as usize]
1075                    ));
1076                }
1077                x if x == Op::MethodCall as u8 => {
1078                    let idx = self.read_u16(ip);
1079                    ip += 2;
1080                    let argc = self.code[ip];
1081                    ip += 1;
1082                    out.push_str(&format!(
1083                        "METHOD_CALL {:>4} ({}) argc={}\n",
1084                        idx, self.constants[idx as usize], argc
1085                    ));
1086                }
1087                x if x == Op::MethodCallOpt as u8 => {
1088                    let idx = self.read_u16(ip);
1089                    ip += 2;
1090                    let argc = self.code[ip];
1091                    ip += 1;
1092                    out.push_str(&format!(
1093                        "METHOD_CALL_OPT {:>4} ({}) argc={}\n",
1094                        idx, self.constants[idx as usize], argc
1095                    ));
1096                }
1097                x if x == Op::Concat as u8 => {
1098                    let count = self.read_u16(ip);
1099                    ip += 2;
1100                    out.push_str(&format!("CONCAT {:>4}\n", count));
1101                }
1102                x if x == Op::IterInit as u8 => out.push_str("ITER_INIT\n"),
1103                x if x == Op::IterNext as u8 => {
1104                    let target = self.read_u16(ip);
1105                    ip += 2;
1106                    out.push_str(&format!("ITER_NEXT {:>4}\n", target));
1107                }
1108                x if x == Op::Throw as u8 => out.push_str("THROW\n"),
1109                x if x == Op::TryCatchSetup as u8 => {
1110                    let target = self.read_u16(ip);
1111                    ip += 2;
1112                    out.push_str(&format!("TRY_CATCH_SETUP {:>4}\n", target));
1113                }
1114                x if x == Op::PopHandler as u8 => out.push_str("POP_HANDLER\n"),
1115                x if x == Op::Pipe as u8 => out.push_str("PIPE\n"),
1116                x if x == Op::Parallel as u8 => out.push_str("PARALLEL\n"),
1117                x if x == Op::ParallelMap as u8 => out.push_str("PARALLEL_MAP\n"),
1118                x if x == Op::ParallelMapStream as u8 => out.push_str("PARALLEL_MAP_STREAM\n"),
1119                x if x == Op::ParallelSettle as u8 => out.push_str("PARALLEL_SETTLE\n"),
1120                x if x == Op::Spawn as u8 => out.push_str("SPAWN\n"),
1121                x if x == Op::Import as u8 => {
1122                    let idx = self.read_u16(ip);
1123                    ip += 2;
1124                    out.push_str(&format!(
1125                        "IMPORT {:>4} ({})\n",
1126                        idx, self.constants[idx as usize]
1127                    ));
1128                }
1129                x if x == Op::SelectiveImport as u8 => {
1130                    let path_idx = self.read_u16(ip);
1131                    ip += 2;
1132                    let names_idx = self.read_u16(ip);
1133                    ip += 2;
1134                    out.push_str(&format!(
1135                        "SELECTIVE_IMPORT {:>4} ({}) names: {:>4} ({})\n",
1136                        path_idx,
1137                        self.constants[path_idx as usize],
1138                        names_idx,
1139                        self.constants[names_idx as usize]
1140                    ));
1141                }
1142                x if x == Op::SyncMutexEnter as u8 => {
1143                    let idx = self.read_u16(ip);
1144                    ip += 2;
1145                    out.push_str(&format!(
1146                        "SYNC_MUTEX_ENTER {:>4} ({})\n",
1147                        idx, self.constants[idx as usize]
1148                    ));
1149                }
1150                x if x == Op::DeadlineSetup as u8 => out.push_str("DEADLINE_SETUP\n"),
1151                x if x == Op::DeadlineEnd as u8 => out.push_str("DEADLINE_END\n"),
1152                x if x == Op::BuildEnum as u8 => {
1153                    let enum_idx = self.read_u16(ip);
1154                    ip += 2;
1155                    let variant_idx = self.read_u16(ip);
1156                    ip += 2;
1157                    let field_count = self.read_u16(ip);
1158                    ip += 2;
1159                    out.push_str(&format!(
1160                        "BUILD_ENUM {:>4} ({}) {:>4} ({}) fields={}\n",
1161                        enum_idx,
1162                        self.constants[enum_idx as usize],
1163                        variant_idx,
1164                        self.constants[variant_idx as usize],
1165                        field_count
1166                    ));
1167                }
1168                x if x == Op::MatchEnum as u8 => {
1169                    let enum_idx = self.read_u16(ip);
1170                    ip += 2;
1171                    let variant_idx = self.read_u16(ip);
1172                    ip += 2;
1173                    out.push_str(&format!(
1174                        "MATCH_ENUM {:>4} ({}) {:>4} ({})\n",
1175                        enum_idx,
1176                        self.constants[enum_idx as usize],
1177                        variant_idx,
1178                        self.constants[variant_idx as usize]
1179                    ));
1180                }
1181                x if x == Op::PopIterator as u8 => out.push_str("POP_ITERATOR\n"),
1182                x if x == Op::TryUnwrap as u8 => out.push_str("TRY_UNWRAP\n"),
1183                x if x == Op::TryWrapOk as u8 => out.push_str("TRY_WRAP_OK\n"),
1184                x if x == Op::CallSpread as u8 => out.push_str("CALL_SPREAD\n"),
1185                x if x == Op::CallBuiltin as u8 => {
1186                    let id = self.read_u64(ip);
1187                    ip += 8;
1188                    let idx = self.read_u16(ip);
1189                    ip += 2;
1190                    let argc = self.code[ip];
1191                    ip += 1;
1192                    out.push_str(&format!(
1193                        "CALL_BUILTIN {id:#018x} {:>4} ({}) argc={}\n",
1194                        idx, self.constants[idx as usize], argc
1195                    ));
1196                }
1197                x if x == Op::CallBuiltinSpread as u8 => {
1198                    let id = self.read_u64(ip);
1199                    ip += 8;
1200                    let idx = self.read_u16(ip);
1201                    ip += 2;
1202                    out.push_str(&format!(
1203                        "CALL_BUILTIN_SPREAD {id:#018x} {:>4} ({})\n",
1204                        idx, self.constants[idx as usize]
1205                    ));
1206                }
1207                x if x == Op::MethodCallSpread as u8 => {
1208                    let idx = self.read_u16(ip + 1);
1209                    ip += 2;
1210                    out.push_str(&format!("METHOD_CALL_SPREAD {idx}\n"));
1211                }
1212                x if x == Op::Dup as u8 => out.push_str("DUP\n"),
1213                x if x == Op::Swap as u8 => out.push_str("SWAP\n"),
1214                x if x == Op::AddInt as u8 => out.push_str("ADD_INT\n"),
1215                x if x == Op::SubInt as u8 => out.push_str("SUB_INT\n"),
1216                x if x == Op::MulInt as u8 => out.push_str("MUL_INT\n"),
1217                x if x == Op::DivInt as u8 => out.push_str("DIV_INT\n"),
1218                x if x == Op::ModInt as u8 => out.push_str("MOD_INT\n"),
1219                x if x == Op::AddFloat as u8 => out.push_str("ADD_FLOAT\n"),
1220                x if x == Op::SubFloat as u8 => out.push_str("SUB_FLOAT\n"),
1221                x if x == Op::MulFloat as u8 => out.push_str("MUL_FLOAT\n"),
1222                x if x == Op::DivFloat as u8 => out.push_str("DIV_FLOAT\n"),
1223                x if x == Op::ModFloat as u8 => out.push_str("MOD_FLOAT\n"),
1224                x if x == Op::EqualInt as u8 => out.push_str("EQUAL_INT\n"),
1225                x if x == Op::NotEqualInt as u8 => out.push_str("NOT_EQUAL_INT\n"),
1226                x if x == Op::LessInt as u8 => out.push_str("LESS_INT\n"),
1227                x if x == Op::GreaterInt as u8 => out.push_str("GREATER_INT\n"),
1228                x if x == Op::LessEqualInt as u8 => out.push_str("LESS_EQUAL_INT\n"),
1229                x if x == Op::GreaterEqualInt as u8 => out.push_str("GREATER_EQUAL_INT\n"),
1230                x if x == Op::EqualFloat as u8 => out.push_str("EQUAL_FLOAT\n"),
1231                x if x == Op::NotEqualFloat as u8 => out.push_str("NOT_EQUAL_FLOAT\n"),
1232                x if x == Op::LessFloat as u8 => out.push_str("LESS_FLOAT\n"),
1233                x if x == Op::GreaterFloat as u8 => out.push_str("GREATER_FLOAT\n"),
1234                x if x == Op::LessEqualFloat as u8 => out.push_str("LESS_EQUAL_FLOAT\n"),
1235                x if x == Op::GreaterEqualFloat as u8 => out.push_str("GREATER_EQUAL_FLOAT\n"),
1236                x if x == Op::EqualBool as u8 => out.push_str("EQUAL_BOOL\n"),
1237                x if x == Op::NotEqualBool as u8 => out.push_str("NOT_EQUAL_BOOL\n"),
1238                x if x == Op::EqualString as u8 => out.push_str("EQUAL_STRING\n"),
1239                x if x == Op::NotEqualString as u8 => out.push_str("NOT_EQUAL_STRING\n"),
1240                x if x == Op::Yield as u8 => out.push_str("YIELD\n"),
1241                _ => {
1242                    out.push_str(&format!("UNKNOWN(0x{:02x})\n", op));
1243                }
1244            }
1245        }
1246        out
1247    }
1248}
1249
1250impl Default for Chunk {
1251    fn default() -> Self {
1252        Self::new()
1253    }
1254}
1255
1256#[cfg(test)]
1257mod tests {
1258    use super::Op;
1259
1260    #[test]
1261    fn op_from_byte_matches_repr_order() {
1262        for (byte, op) in Op::ALL.iter().copied().enumerate() {
1263            assert_eq!(byte as u8, op as u8);
1264            assert_eq!(Op::from_byte(byte as u8), Some(op));
1265        }
1266        assert_eq!(Op::from_byte(Op::ALL.len() as u8), None);
1267    }
1268}