Skip to main content

harn_vm/
chunk.rs

1use std::fmt;
2
3/// Bytecode opcodes for the Harn VM.
4#[derive(Debug, Clone, Copy, PartialEq, Eq)]
5#[repr(u8)]
6pub enum Op {
7    /// Push a constant from the constant pool onto the stack.
8    Constant, // arg: u16 constant index
9    /// Push nil onto the stack.
10    Nil,
11    /// Push true onto the stack.
12    True,
13    /// Push false onto the stack.
14    False,
15
16    // --- Variable operations ---
17    /// Get a variable by name (from constant pool).
18    GetVar, // arg: u16 constant index (name)
19    /// Define a new immutable variable. Pops value from stack.
20    DefLet, // arg: u16 constant index (name)
21    /// Define a new mutable variable. Pops value from stack.
22    DefVar, // arg: u16 constant index (name)
23    /// Assign to an existing mutable variable. Pops value from stack.
24    SetVar, // arg: u16 constant index (name)
25    /// Push a new lexical scope onto the environment stack.
26    PushScope,
27    /// Pop the current lexical scope from the environment stack.
28    PopScope,
29
30    // --- Arithmetic ---
31    Add,
32    Sub,
33    Mul,
34    Div,
35    Mod,
36    Pow,
37    Negate,
38
39    // --- Comparison ---
40    Equal,
41    NotEqual,
42    Less,
43    Greater,
44    LessEqual,
45    GreaterEqual,
46
47    // --- Logical ---
48    Not,
49
50    // --- Control flow ---
51    /// Jump unconditionally. arg: u16 offset.
52    Jump,
53    /// Jump if top of stack is falsy. Does not pop. arg: u16 offset.
54    JumpIfFalse,
55    /// Jump if top of stack is truthy. Does not pop. arg: u16 offset.
56    JumpIfTrue,
57    /// Pop top of stack (discard).
58    Pop,
59
60    // --- Functions ---
61    /// Call a function/builtin. arg: u8 = arg count. Name is on stack below args.
62    Call,
63    /// Tail call: like Call, but replaces the current frame instead of pushing
64    /// a new one. Used for `return f(x)` to enable tail call optimization.
65    /// For builtins, behaves like a regular Call (no frame to replace).
66    TailCall,
67    /// Return from current function. Pops return value.
68    Return,
69    /// Create a closure. arg: u16 = chunk index in function table.
70    Closure,
71
72    // --- Collections ---
73    /// Build a list. arg: u16 = element count. Elements are on stack.
74    BuildList,
75    /// Build a dict. arg: u16 = entry count. Key-value pairs on stack.
76    BuildDict,
77    /// Subscript access: stack has [object, index]. Pushes result.
78    Subscript,
79    /// Slice access: stack has [object, start_or_nil, end_or_nil]. Pushes sublist/substring.
80    Slice,
81
82    // --- Object operations ---
83    /// Property access. arg: u16 = constant index (property name).
84    GetProperty,
85    /// Optional property access (?.). Like GetProperty but returns nil
86    /// instead of erroring when the object is nil. arg: u16 = constant index.
87    GetPropertyOpt,
88    /// Property assignment. arg: u16 = constant index (property name).
89    /// Stack: [value] → assigns to the named variable's property.
90    SetProperty,
91    /// Subscript assignment. arg: u16 = constant index (variable name).
92    /// Stack: [index, value] → assigns to variable[index] = value.
93    SetSubscript,
94    /// Method call. arg1: u16 = constant index (method name), arg2: u8 = arg count.
95    MethodCall,
96    /// Optional method call (?.). Like MethodCall but returns nil if the
97    /// receiver is nil instead of dispatching. arg1: u16, arg2: u8.
98    MethodCallOpt,
99
100    // --- String ---
101    /// String concatenation of N parts. arg: u16 = part count.
102    Concat,
103
104    // --- Iteration ---
105    /// Set up a for-in loop. Expects iterable on stack. Pushes iterator state.
106    IterInit,
107    /// Advance iterator. If exhausted, jumps. arg: u16 = jump offset.
108    /// Pushes next value and the variable name is set via DefVar before the loop.
109    IterNext,
110
111    // --- Pipe ---
112    /// Pipe: pops [value, callable], invokes callable(value).
113    Pipe,
114
115    // --- Error handling ---
116    /// Pop value, raise as error.
117    Throw,
118    /// Push exception handler. arg: u16 = offset to catch handler.
119    TryCatchSetup,
120    /// Remove top exception handler (end of try body).
121    PopHandler,
122
123    // --- Concurrency ---
124    /// Execute closure N times sequentially, push results as list.
125    /// Stack: count, closure → result_list
126    Parallel,
127    /// Execute closure for each item in list, push results as list.
128    /// Stack: list, closure → result_list
129    ParallelMap,
130    /// Like ParallelMap but wraps each result in Result.Ok/Err, never fails.
131    /// Stack: list, closure → {results: [Result], succeeded: int, failed: int}
132    ParallelSettle,
133    /// Store closure for deferred execution, push TaskHandle.
134    /// Stack: closure → TaskHandle
135    Spawn,
136
137    // --- Imports ---
138    /// Import a file. arg: u16 = constant index (path string).
139    Import,
140    /// Selective import. arg1: u16 = path string, arg2: u16 = names list constant.
141    SelectiveImport,
142
143    // --- Deadline ---
144    /// Pop duration value, push deadline onto internal deadline stack.
145    DeadlineSetup,
146    /// Pop deadline from internal deadline stack.
147    DeadlineEnd,
148
149    // --- Enum ---
150    /// Build an enum variant value.
151    /// arg1: u16 = constant index (enum name), arg2: u16 = constant index (variant name),
152    /// arg3: u16 = field count. Fields are on stack.
153    BuildEnum,
154
155    // --- Match ---
156    /// Match an enum pattern. Checks enum_name + variant on the top of stack (dup'd match value).
157    /// arg1: u16 = constant index (enum name), arg2: u16 = constant index (variant name).
158    /// If match succeeds, pushes true; else pushes false.
159    MatchEnum,
160
161    // --- Loop control ---
162    /// Pop the top iterator from the iterator stack (cleanup on break from for-in).
163    PopIterator,
164
165    // --- Defaults ---
166    /// Push the number of arguments passed to the current function call.
167    GetArgc,
168
169    // --- Type checking ---
170    /// Runtime type check on a variable.
171    /// arg1: u16 = constant index (variable name),
172    /// arg2: u16 = constant index (expected type name).
173    /// Throws a TypeError if the variable's type doesn't match.
174    CheckType,
175
176    // --- Result try operator ---
177    /// Try-unwrap: if top is Result.Ok(v), replace with v. If Result.Err(e), return it.
178    TryUnwrap,
179
180    // --- Spread call ---
181    /// Call with spread arguments. Stack: [callee, args_list] -> result.
182    CallSpread,
183    /// Method call with spread arguments. Stack: [object, args_list] -> result.
184    /// Followed by 2 bytes for method name constant index.
185    MethodCallSpread,
186
187    // --- Misc ---
188    /// Duplicate top of stack.
189    Dup,
190    /// Swap top two stack values.
191    Swap,
192    /// Membership test: stack has [item, collection]. Pushes bool.
193    /// Works for lists (item in list), dicts (key in dict), strings (substr in string), and sets.
194    Contains,
195
196    /// Yield a value from a generator. Pops value, sends through channel, suspends.
197    Yield,
198}
199
200/// A constant value in the constant pool.
201#[derive(Debug, Clone, PartialEq)]
202pub enum Constant {
203    Int(i64),
204    Float(f64),
205    String(String),
206    Bool(bool),
207    Nil,
208    Duration(u64),
209}
210
211impl fmt::Display for Constant {
212    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
213        match self {
214            Constant::Int(n) => write!(f, "{n}"),
215            Constant::Float(n) => write!(f, "{n}"),
216            Constant::String(s) => write!(f, "\"{s}\""),
217            Constant::Bool(b) => write!(f, "{b}"),
218            Constant::Nil => write!(f, "nil"),
219            Constant::Duration(ms) => write!(f, "{ms}ms"),
220        }
221    }
222}
223
224/// A compiled chunk of bytecode.
225#[derive(Debug, Clone)]
226pub struct Chunk {
227    /// The bytecode instructions.
228    pub code: Vec<u8>,
229    /// Constant pool.
230    pub constants: Vec<Constant>,
231    /// Source line numbers for each instruction (for error reporting).
232    pub lines: Vec<u32>,
233    /// Source column numbers for each instruction (for error reporting).
234    /// Parallel to `lines`; 0 means no column info available.
235    pub columns: Vec<u32>,
236    /// Source file that this chunk was compiled from, when known. Set for
237    /// chunks compiled from imported modules so runtime errors can report
238    /// the correct file path for each frame instead of always pointing at
239    /// the entry-point pipeline.
240    pub source_file: Option<String>,
241    /// Current column to use when emitting instructions (set by compiler).
242    current_col: u32,
243    /// Compiled function bodies (for closures).
244    pub functions: Vec<CompiledFunction>,
245}
246
247/// A compiled function (closure body).
248#[derive(Debug, Clone)]
249pub struct CompiledFunction {
250    pub name: String,
251    pub params: Vec<String>,
252    /// Index of the first parameter with a default value, or None if all required.
253    pub default_start: Option<usize>,
254    pub chunk: Chunk,
255    /// True if the function body contains `yield` expressions (generator function).
256    pub is_generator: bool,
257    /// True if the last parameter is a rest parameter (`...name`).
258    pub has_rest_param: bool,
259}
260
261impl Chunk {
262    pub fn new() -> Self {
263        Self {
264            code: Vec::new(),
265            constants: Vec::new(),
266            lines: Vec::new(),
267            columns: Vec::new(),
268            source_file: None,
269            current_col: 0,
270            functions: Vec::new(),
271        }
272    }
273
274    /// Set the current column for subsequent emit calls.
275    pub fn set_column(&mut self, col: u32) {
276        self.current_col = col;
277    }
278
279    /// Add a constant and return its index.
280    pub fn add_constant(&mut self, constant: Constant) -> u16 {
281        // Reuse existing constant if possible
282        for (i, c) in self.constants.iter().enumerate() {
283            if c == &constant {
284                return i as u16;
285            }
286        }
287        let idx = self.constants.len();
288        self.constants.push(constant);
289        idx as u16
290    }
291
292    /// Emit a single-byte instruction.
293    pub fn emit(&mut self, op: Op, line: u32) {
294        let col = self.current_col;
295        self.code.push(op as u8);
296        self.lines.push(line);
297        self.columns.push(col);
298    }
299
300    /// Emit an instruction with a u16 argument.
301    pub fn emit_u16(&mut self, op: Op, arg: u16, line: u32) {
302        let col = self.current_col;
303        self.code.push(op as u8);
304        self.code.push((arg >> 8) as u8);
305        self.code.push((arg & 0xFF) as u8);
306        self.lines.push(line);
307        self.lines.push(line);
308        self.lines.push(line);
309        self.columns.push(col);
310        self.columns.push(col);
311        self.columns.push(col);
312    }
313
314    /// Emit an instruction with a u8 argument.
315    pub fn emit_u8(&mut self, op: Op, arg: u8, line: u32) {
316        let col = self.current_col;
317        self.code.push(op as u8);
318        self.code.push(arg);
319        self.lines.push(line);
320        self.lines.push(line);
321        self.columns.push(col);
322        self.columns.push(col);
323    }
324
325    /// Emit a method call: op + u16 (method name) + u8 (arg count).
326    pub fn emit_method_call(&mut self, name_idx: u16, arg_count: u8, line: u32) {
327        self.emit_method_call_inner(Op::MethodCall, name_idx, arg_count, line);
328    }
329
330    /// Emit an optional method call (?.) — returns nil if receiver is nil.
331    pub fn emit_method_call_opt(&mut self, name_idx: u16, arg_count: u8, line: u32) {
332        self.emit_method_call_inner(Op::MethodCallOpt, name_idx, arg_count, line);
333    }
334
335    fn emit_method_call_inner(&mut self, op: Op, name_idx: u16, arg_count: u8, line: u32) {
336        let col = self.current_col;
337        self.code.push(op as u8);
338        self.code.push((name_idx >> 8) as u8);
339        self.code.push((name_idx & 0xFF) as u8);
340        self.code.push(arg_count);
341        self.lines.push(line);
342        self.lines.push(line);
343        self.lines.push(line);
344        self.lines.push(line);
345        self.columns.push(col);
346        self.columns.push(col);
347        self.columns.push(col);
348        self.columns.push(col);
349    }
350
351    /// Current code offset (for jump patching).
352    pub fn current_offset(&self) -> usize {
353        self.code.len()
354    }
355
356    /// Emit a jump instruction with a placeholder offset. Returns the position to patch.
357    pub fn emit_jump(&mut self, op: Op, line: u32) -> usize {
358        let col = self.current_col;
359        self.code.push(op as u8);
360        let patch_pos = self.code.len();
361        self.code.push(0xFF);
362        self.code.push(0xFF);
363        self.lines.push(line);
364        self.lines.push(line);
365        self.lines.push(line);
366        self.columns.push(col);
367        self.columns.push(col);
368        self.columns.push(col);
369        patch_pos
370    }
371
372    /// Patch a jump instruction at the given position to jump to the current offset.
373    pub fn patch_jump(&mut self, patch_pos: usize) {
374        let target = self.code.len() as u16;
375        self.code[patch_pos] = (target >> 8) as u8;
376        self.code[patch_pos + 1] = (target & 0xFF) as u8;
377    }
378
379    /// Patch a jump to a specific target position.
380    pub fn patch_jump_to(&mut self, patch_pos: usize, target: usize) {
381        let target = target as u16;
382        self.code[patch_pos] = (target >> 8) as u8;
383        self.code[patch_pos + 1] = (target & 0xFF) as u8;
384    }
385
386    /// Read a u16 argument at the given position.
387    pub fn read_u16(&self, pos: usize) -> u16 {
388        ((self.code[pos] as u16) << 8) | (self.code[pos + 1] as u16)
389    }
390
391    /// Disassemble for debugging.
392    pub fn disassemble(&self, name: &str) -> String {
393        let mut out = format!("== {name} ==\n");
394        let mut ip = 0;
395        while ip < self.code.len() {
396            let op = self.code[ip];
397            let line = self.lines.get(ip).copied().unwrap_or(0);
398            out.push_str(&format!("{:04} [{:>4}] ", ip, line));
399            ip += 1;
400
401            match op {
402                x if x == Op::Constant as u8 => {
403                    let idx = self.read_u16(ip);
404                    ip += 2;
405                    let val = &self.constants[idx as usize];
406                    out.push_str(&format!("CONSTANT {:>4} ({})\n", idx, val));
407                }
408                x if x == Op::Nil as u8 => out.push_str("NIL\n"),
409                x if x == Op::True as u8 => out.push_str("TRUE\n"),
410                x if x == Op::False as u8 => out.push_str("FALSE\n"),
411                x if x == Op::GetVar as u8 => {
412                    let idx = self.read_u16(ip);
413                    ip += 2;
414                    out.push_str(&format!(
415                        "GET_VAR {:>4} ({})\n",
416                        idx, self.constants[idx as usize]
417                    ));
418                }
419                x if x == Op::DefLet as u8 => {
420                    let idx = self.read_u16(ip);
421                    ip += 2;
422                    out.push_str(&format!(
423                        "DEF_LET {:>4} ({})\n",
424                        idx, self.constants[idx as usize]
425                    ));
426                }
427                x if x == Op::DefVar as u8 => {
428                    let idx = self.read_u16(ip);
429                    ip += 2;
430                    out.push_str(&format!(
431                        "DEF_VAR {:>4} ({})\n",
432                        idx, self.constants[idx as usize]
433                    ));
434                }
435                x if x == Op::SetVar as u8 => {
436                    let idx = self.read_u16(ip);
437                    ip += 2;
438                    out.push_str(&format!(
439                        "SET_VAR {:>4} ({})\n",
440                        idx, self.constants[idx as usize]
441                    ));
442                }
443                x if x == Op::PushScope as u8 => out.push_str("PUSH_SCOPE\n"),
444                x if x == Op::PopScope as u8 => out.push_str("POP_SCOPE\n"),
445                x if x == Op::Add as u8 => out.push_str("ADD\n"),
446                x if x == Op::Sub as u8 => out.push_str("SUB\n"),
447                x if x == Op::Mul as u8 => out.push_str("MUL\n"),
448                x if x == Op::Div as u8 => out.push_str("DIV\n"),
449                x if x == Op::Mod as u8 => out.push_str("MOD\n"),
450                x if x == Op::Pow as u8 => out.push_str("POW\n"),
451                x if x == Op::Negate as u8 => out.push_str("NEGATE\n"),
452                x if x == Op::Equal as u8 => out.push_str("EQUAL\n"),
453                x if x == Op::NotEqual as u8 => out.push_str("NOT_EQUAL\n"),
454                x if x == Op::Less as u8 => out.push_str("LESS\n"),
455                x if x == Op::Greater as u8 => out.push_str("GREATER\n"),
456                x if x == Op::LessEqual as u8 => out.push_str("LESS_EQUAL\n"),
457                x if x == Op::GreaterEqual as u8 => out.push_str("GREATER_EQUAL\n"),
458                x if x == Op::Contains as u8 => out.push_str("CONTAINS\n"),
459                x if x == Op::Not as u8 => out.push_str("NOT\n"),
460                x if x == Op::Jump as u8 => {
461                    let target = self.read_u16(ip);
462                    ip += 2;
463                    out.push_str(&format!("JUMP {:>4}\n", target));
464                }
465                x if x == Op::JumpIfFalse as u8 => {
466                    let target = self.read_u16(ip);
467                    ip += 2;
468                    out.push_str(&format!("JUMP_IF_FALSE {:>4}\n", target));
469                }
470                x if x == Op::JumpIfTrue as u8 => {
471                    let target = self.read_u16(ip);
472                    ip += 2;
473                    out.push_str(&format!("JUMP_IF_TRUE {:>4}\n", target));
474                }
475                x if x == Op::Pop as u8 => out.push_str("POP\n"),
476                x if x == Op::Call as u8 => {
477                    let argc = self.code[ip];
478                    ip += 1;
479                    out.push_str(&format!("CALL {:>4}\n", argc));
480                }
481                x if x == Op::TailCall as u8 => {
482                    let argc = self.code[ip];
483                    ip += 1;
484                    out.push_str(&format!("TAIL_CALL {:>4}\n", argc));
485                }
486                x if x == Op::Return as u8 => out.push_str("RETURN\n"),
487                x if x == Op::Closure as u8 => {
488                    let idx = self.read_u16(ip);
489                    ip += 2;
490                    out.push_str(&format!("CLOSURE {:>4}\n", idx));
491                }
492                x if x == Op::BuildList as u8 => {
493                    let count = self.read_u16(ip);
494                    ip += 2;
495                    out.push_str(&format!("BUILD_LIST {:>4}\n", count));
496                }
497                x if x == Op::BuildDict as u8 => {
498                    let count = self.read_u16(ip);
499                    ip += 2;
500                    out.push_str(&format!("BUILD_DICT {:>4}\n", count));
501                }
502                x if x == Op::Subscript as u8 => out.push_str("SUBSCRIPT\n"),
503                x if x == Op::Slice as u8 => out.push_str("SLICE\n"),
504                x if x == Op::GetProperty as u8 => {
505                    let idx = self.read_u16(ip);
506                    ip += 2;
507                    out.push_str(&format!(
508                        "GET_PROPERTY {:>4} ({})\n",
509                        idx, self.constants[idx as usize]
510                    ));
511                }
512                x if x == Op::GetPropertyOpt as u8 => {
513                    let idx = self.read_u16(ip);
514                    ip += 2;
515                    out.push_str(&format!(
516                        "GET_PROPERTY_OPT {:>4} ({})\n",
517                        idx, self.constants[idx as usize]
518                    ));
519                }
520                x if x == Op::SetProperty as u8 => {
521                    let idx = self.read_u16(ip);
522                    ip += 2;
523                    out.push_str(&format!(
524                        "SET_PROPERTY {:>4} ({})\n",
525                        idx, self.constants[idx as usize]
526                    ));
527                }
528                x if x == Op::SetSubscript as u8 => {
529                    let idx = self.read_u16(ip);
530                    ip += 2;
531                    out.push_str(&format!(
532                        "SET_SUBSCRIPT {:>4} ({})\n",
533                        idx, self.constants[idx as usize]
534                    ));
535                }
536                x if x == Op::MethodCall as u8 => {
537                    let idx = self.read_u16(ip);
538                    ip += 2;
539                    let argc = self.code[ip];
540                    ip += 1;
541                    out.push_str(&format!(
542                        "METHOD_CALL {:>4} ({}) argc={}\n",
543                        idx, self.constants[idx as usize], argc
544                    ));
545                }
546                x if x == Op::MethodCallOpt as u8 => {
547                    let idx = self.read_u16(ip);
548                    ip += 2;
549                    let argc = self.code[ip];
550                    ip += 1;
551                    out.push_str(&format!(
552                        "METHOD_CALL_OPT {:>4} ({}) argc={}\n",
553                        idx, self.constants[idx as usize], argc
554                    ));
555                }
556                x if x == Op::Concat as u8 => {
557                    let count = self.read_u16(ip);
558                    ip += 2;
559                    out.push_str(&format!("CONCAT {:>4}\n", count));
560                }
561                x if x == Op::IterInit as u8 => out.push_str("ITER_INIT\n"),
562                x if x == Op::IterNext as u8 => {
563                    let target = self.read_u16(ip);
564                    ip += 2;
565                    out.push_str(&format!("ITER_NEXT {:>4}\n", target));
566                }
567                x if x == Op::Throw as u8 => out.push_str("THROW\n"),
568                x if x == Op::TryCatchSetup as u8 => {
569                    let target = self.read_u16(ip);
570                    ip += 2;
571                    out.push_str(&format!("TRY_CATCH_SETUP {:>4}\n", target));
572                }
573                x if x == Op::PopHandler as u8 => out.push_str("POP_HANDLER\n"),
574                x if x == Op::Pipe as u8 => out.push_str("PIPE\n"),
575                x if x == Op::Parallel as u8 => out.push_str("PARALLEL\n"),
576                x if x == Op::ParallelMap as u8 => out.push_str("PARALLEL_MAP\n"),
577                x if x == Op::ParallelSettle as u8 => out.push_str("PARALLEL_SETTLE\n"),
578                x if x == Op::Spawn as u8 => out.push_str("SPAWN\n"),
579                x if x == Op::Import as u8 => {
580                    let idx = self.read_u16(ip);
581                    ip += 2;
582                    out.push_str(&format!(
583                        "IMPORT {:>4} ({})\n",
584                        idx, self.constants[idx as usize]
585                    ));
586                }
587                x if x == Op::SelectiveImport as u8 => {
588                    let path_idx = self.read_u16(ip);
589                    ip += 2;
590                    let names_idx = self.read_u16(ip);
591                    ip += 2;
592                    out.push_str(&format!(
593                        "SELECTIVE_IMPORT {:>4} ({}) names: {:>4} ({})\n",
594                        path_idx,
595                        self.constants[path_idx as usize],
596                        names_idx,
597                        self.constants[names_idx as usize]
598                    ));
599                }
600                x if x == Op::DeadlineSetup as u8 => out.push_str("DEADLINE_SETUP\n"),
601                x if x == Op::DeadlineEnd as u8 => out.push_str("DEADLINE_END\n"),
602                x if x == Op::BuildEnum as u8 => {
603                    let enum_idx = self.read_u16(ip);
604                    ip += 2;
605                    let variant_idx = self.read_u16(ip);
606                    ip += 2;
607                    let field_count = self.read_u16(ip);
608                    ip += 2;
609                    out.push_str(&format!(
610                        "BUILD_ENUM {:>4} ({}) {:>4} ({}) fields={}\n",
611                        enum_idx,
612                        self.constants[enum_idx as usize],
613                        variant_idx,
614                        self.constants[variant_idx as usize],
615                        field_count
616                    ));
617                }
618                x if x == Op::MatchEnum as u8 => {
619                    let enum_idx = self.read_u16(ip);
620                    ip += 2;
621                    let variant_idx = self.read_u16(ip);
622                    ip += 2;
623                    out.push_str(&format!(
624                        "MATCH_ENUM {:>4} ({}) {:>4} ({})\n",
625                        enum_idx,
626                        self.constants[enum_idx as usize],
627                        variant_idx,
628                        self.constants[variant_idx as usize]
629                    ));
630                }
631                x if x == Op::PopIterator as u8 => out.push_str("POP_ITERATOR\n"),
632                x if x == Op::TryUnwrap as u8 => out.push_str("TRY_UNWRAP\n"),
633                x if x == Op::CallSpread as u8 => out.push_str("CALL_SPREAD\n"),
634                x if x == Op::MethodCallSpread as u8 => {
635                    let idx = self.read_u16(ip + 1);
636                    ip += 2;
637                    out.push_str(&format!("METHOD_CALL_SPREAD {idx}\n"));
638                }
639                x if x == Op::Dup as u8 => out.push_str("DUP\n"),
640                x if x == Op::Swap as u8 => out.push_str("SWAP\n"),
641                x if x == Op::Yield as u8 => out.push_str("YIELD\n"),
642                _ => {
643                    out.push_str(&format!("UNKNOWN(0x{:02x})\n", op));
644                }
645            }
646        }
647        out
648    }
649}
650
651impl Default for Chunk {
652    fn default() -> Self {
653        Self::new()
654    }
655}