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