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