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