Skip to main content

harn_vm/
chunk.rs

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