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