Skip to main content

harn_vm/
chunk.rs

1use std::collections::{BTreeMap, HashMap};
2use std::fmt;
3use std::sync::atomic::{AtomicU64, Ordering};
4use std::sync::Arc;
5
6use harn_parser::TypeExpr;
7use parking_lot::Mutex;
8use serde::{Deserialize, Serialize};
9
10use crate::harness::HarnessKind;
11use crate::runtime_guards::RuntimeParamGuard;
12
13/// Sentinel value stored in [`Chunk::inline_cache_index`] for code offsets
14/// that have no inline-cache slot registered. Chosen as `u32::MAX` so the
15/// hot dispatch path can treat the side-table as a flat `Vec<u32>` without
16/// an `Option` wrapper — the comparison against the sentinel collapses to a
17/// single integer compare. The compile-time max useful slot count is bounded
18/// by code length (one slot per cacheable opcode), so `u32::MAX` is safely
19/// out of the addressable slot range.
20pub(crate) const NO_INLINE_CACHE_SLOT: u32 = u32::MAX;
21static NEXT_CHUNK_CACHE_ID: AtomicU64 = AtomicU64::new(1);
22
23fn next_chunk_cache_id() -> u64 {
24    NEXT_CHUNK_CACHE_ID.fetch_add(1, Ordering::Relaxed)
25}
26
27/// Bytecode opcodes for the Harn VM. The enum, the byte-to-variant
28/// mapping, the sync and async dispatch tables, the disassembly
29/// renderer, and the per-opcode classification helpers are all emitted
30/// by `harn_opcode_macros::define_opcodes!` in [`crate::vm::ops`].
31/// Re-exported here so callers that import `crate::chunk::Op` need no
32/// awareness of the macro layout.
33pub use crate::vm::ops::Op;
34pub(crate) use crate::vm::ops::{is_adaptive_binary_op, op_reads_outer_name};
35
36/// A constant value in the constant pool.
37#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
38pub enum Constant {
39    Int(i64),
40    Float(f64),
41    String(String),
42    Bool(bool),
43    Nil,
44    Duration(i64),
45}
46
47/// Identity used for constant-pool deduplication.
48///
49/// This is stricter than `PartialEq` for floats: it compares `Constant::Float`
50/// operands by their raw bits, so `+0.0` and `-0.0` (which are `==` under IEEE
51/// 754) get distinct pool slots, and each distinct NaN bit-pattern is preserved.
52/// Collapsing `+0.0`/`-0.0` onto one slot makes signed zero — and therefore the
53/// sign of `1.0 / 0.0` vs `1.0 / -0.0` — depend on which literal happened to be
54/// interned first. The derived `PartialEq` is left intact for all other uses.
55fn constants_identical(a: &Constant, b: &Constant) -> bool {
56    match (a, b) {
57        (Constant::Float(x), Constant::Float(y)) => x.to_bits() == y.to_bits(),
58        _ => a == b,
59    }
60}
61
62/// Hashable identity for constant-pool deduplication.
63///
64/// Mirrors [`constants_identical`] exactly, including bitwise float identity,
65/// so the compiler can replace the previous linear scan with an amortized O(1)
66/// side index without changing bytecode-visible constant slots.
67#[derive(Debug, Clone, PartialEq, Eq, Hash)]
68enum ConstantKey {
69    Int(i64),
70    Float(u64),
71    String(String),
72    Bool(bool),
73    Nil,
74    Duration(i64),
75}
76
77impl From<&Constant> for ConstantKey {
78    fn from(constant: &Constant) -> Self {
79        match constant {
80            Constant::Int(value) => Self::Int(*value),
81            Constant::Float(value) => Self::Float(value.to_bits()),
82            Constant::String(value) => Self::String(value.clone()),
83            Constant::Bool(value) => Self::Bool(*value),
84            Constant::Nil => Self::Nil,
85            Constant::Duration(value) => Self::Duration(*value),
86        }
87    }
88}
89
90fn build_constant_index(constants: &[Constant]) -> HashMap<ConstantKey, u16> {
91    let mut index = HashMap::with_capacity(constants.len());
92    for (slot, constant) in constants.iter().enumerate() {
93        if let Ok(slot) = u16::try_from(slot) {
94            index.entry(ConstantKey::from(constant)).or_insert(slot);
95        }
96    }
97    index
98}
99
100/// Runtime-only inline-cache state for bytecode instructions that repeatedly
101/// see the same dynamic shape. Lookup caches stay monomorphic on a name and
102/// receiver shape. Adaptive caches warm on a stable operand or call target,
103/// then fall back through the generic opcode and replace or reset state when
104/// the observed shape changes.
105///
106/// This vector is intentionally excluded from [`CachedChunk`]: bytecode cache
107/// artifacts keep the slot layout but start with empty runtime feedback in each
108/// process.
109#[derive(Debug, Clone, PartialEq, Eq)]
110pub(crate) enum InlineCacheEntry {
111    Empty,
112    Property {
113        name_idx: u16,
114        target: PropertyCacheTarget,
115    },
116    Method {
117        name_idx: u16,
118        argc: usize,
119        target: MethodCacheTarget,
120    },
121    AdaptiveBinary {
122        op: AdaptiveBinaryOp,
123        state: AdaptiveBinaryState,
124    },
125    DirectCall {
126        state: DirectCallState,
127    },
128}
129
130#[derive(Debug, Clone, Copy, PartialEq, Eq)]
131pub(crate) enum AdaptiveBinaryOp {
132    Add,
133    Sub,
134    Mul,
135    Div,
136    Mod,
137    Equal,
138    NotEqual,
139    Less,
140    Greater,
141    LessEqual,
142    GreaterEqual,
143}
144
145/// Adaptive-binary IC state. All fields are scalar `Copy` (shape is a
146/// `Copy` enum, hit/miss counters are integers), so the struct as a whole
147/// is `Copy`. This lets `execute_adaptive_binary` extract the cached state
148/// by value for the specialization check without cloning the wrapping
149/// `InlineCacheEntry` on every dispatch.
150#[derive(Debug, Clone, Copy, PartialEq, Eq)]
151pub(crate) enum AdaptiveBinaryState {
152    Warmup {
153        shape: BinaryShape,
154        hits: u8,
155    },
156    Specialized {
157        shape: BinaryShape,
158        hits: u64,
159        misses: u64,
160    },
161}
162
163#[derive(Debug, Clone, Copy, PartialEq, Eq)]
164pub(crate) enum BinaryShape {
165    Int,
166    Float,
167    Bool,
168    String,
169}
170
171#[derive(Debug, Clone)]
172pub(crate) enum DirectCallState {
173    Warmup {
174        argc: usize,
175        target: DirectCallTarget,
176        hits: u8,
177    },
178    Specialized {
179        argc: usize,
180        target: DirectCallTarget,
181        hits: u64,
182        misses: u64,
183    },
184}
185
186#[derive(Debug, Clone)]
187pub(crate) enum DirectCallTarget {
188    Closure(Arc<crate::value::VmClosure>),
189}
190
191impl PartialEq for DirectCallTarget {
192    fn eq(&self, other: &Self) -> bool {
193        match (self, other) {
194            (Self::Closure(left), Self::Closure(right)) => Arc::ptr_eq(left, right),
195        }
196    }
197}
198
199impl Eq for DirectCallTarget {}
200
201impl PartialEq for DirectCallState {
202    fn eq(&self, other: &Self) -> bool {
203        match (self, other) {
204            (
205                Self::Warmup {
206                    argc: left_argc,
207                    target: left_target,
208                    hits: left_hits,
209                },
210                Self::Warmup {
211                    argc: right_argc,
212                    target: right_target,
213                    hits: right_hits,
214                },
215            ) => left_argc == right_argc && left_target == right_target && left_hits == right_hits,
216            (
217                Self::Specialized {
218                    argc: left_argc,
219                    target: left_target,
220                    hits: left_hits,
221                    misses: left_misses,
222                },
223                Self::Specialized {
224                    argc: right_argc,
225                    target: right_target,
226                    hits: right_hits,
227                    misses: right_misses,
228                },
229            ) => {
230                left_argc == right_argc
231                    && left_target == right_target
232                    && left_hits == right_hits
233                    && left_misses == right_misses
234            }
235            _ => false,
236        }
237    }
238}
239
240impl Eq for DirectCallState {}
241
242#[derive(Debug, Clone, PartialEq, Eq)]
243pub(crate) enum PropertyCacheTarget {
244    DictField(Arc<str>),
245    StructField { field_name: Arc<str>, index: usize },
246    HarnessSubHandle(HarnessKind),
247    ListCount,
248    ListEmpty,
249    ListFirst,
250    ListLast,
251    StringCount,
252    StringEmpty,
253    PairFirst,
254    PairSecond,
255    EnumVariant,
256    EnumFields,
257}
258
259#[derive(Debug, Clone, Copy, PartialEq, Eq)]
260pub(crate) enum MethodCacheTarget {
261    Harness(HarnessKind),
262    ListCount,
263    ListEmpty,
264    ListContains,
265    StringCount,
266    StringEmpty,
267    StringContains,
268    DictCount,
269    DictHas,
270    RangeCount,
271    RangeLen,
272    RangeEmpty,
273    RangeFirst,
274    RangeLast,
275    SetCount,
276    SetLen,
277    SetEmpty,
278    SetContains,
279}
280
281/// Debug metadata for a slot-indexed local in a compiled chunk.
282#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
283pub struct LocalSlotInfo {
284    pub name: String,
285    pub mutable: bool,
286    pub scope_depth: usize,
287}
288
289impl fmt::Display for Constant {
290    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
291        match self {
292            Constant::Int(n) => write!(f, "{n}"),
293            Constant::Float(n) => write!(f, "{n}"),
294            Constant::String(s) => write!(f, "\"{s}\""),
295            Constant::Bool(b) => write!(f, "{b}"),
296            Constant::Nil => write!(f, "nil"),
297            Constant::Duration(ms) => write!(f, "{ms}ms"),
298        }
299    }
300}
301
302/// A compiled chunk of bytecode.
303#[derive(Debug)]
304pub struct Chunk {
305    /// Runtime-only identity for VM-local inline cache storage. It is not
306    /// serialized; freshly compiled or loaded chunks get new ids, while clones
307    /// keep the same id because they represent the same bytecode object.
308    cache_id: u64,
309    /// The bytecode instructions.
310    pub code: Vec<u8>,
311    /// Constant pool.
312    pub constants: Vec<Constant>,
313    /// Compile-time constant-pool dedup index, derived from
314    /// [`Chunk::constants`] and intentionally omitted from [`CachedChunk`].
315    ///
316    /// Only [`Chunk::add_constant`] reads it, so only a chunk the compiler is
317    /// still emitting into needs it. Building it eagerly on a cache load would
318    /// hash every constant of every chunk of every module on a path that never
319    /// appends another constant. `None` means "not built yet"; it is derived on
320    /// the first append.
321    constant_index: Option<HashMap<ConstantKey, u16>>,
322    /// Source line numbers for each instruction (for error reporting).
323    pub lines: Vec<u32>,
324    /// Source column numbers for each instruction (for error reporting).
325    /// Parallel to `lines`; 0 means no column info available.
326    pub columns: Vec<u32>,
327    /// Source file that this chunk was compiled from, when known. Set for
328    /// chunks compiled from imported modules so runtime errors can report
329    /// the correct file path for each frame instead of always pointing at
330    /// the entry-point pipeline.
331    pub source_file: Option<String>,
332    /// Current column to use when emitting instructions (set by compiler).
333    current_col: u32,
334    /// Compiled function bodies (for closures).
335    pub functions: Vec<CompiledFunctionRef>,
336    /// Instruction offset to inline-cache slot. Slots are assigned at emit time
337    /// for cacheable instructions while bytecode bytes remain immutable.
338    /// Preserved as the serialization-stable representation that round-trips
339    /// through [`CachedChunk`]; the runtime hot path reads
340    /// [`Chunk::inline_cache_index`] instead.
341    inline_cache_slots: BTreeMap<usize, usize>,
342    /// Flat side-table indexed by code offset that returns the inline-cache
343    /// slot index (or [`NO_INLINE_CACHE_SLOT`] for "no slot at this offset").
344    /// Built alongside [`Chunk::inline_cache_slots`] at emit/load time so the
345    /// per-dispatch lookup that fires on every adaptive binary op, `Op::Call`,
346    /// `Op::MethodCall`, and `Op::GetProperty` is one cache-friendly `Vec`
347    /// index instead of a `BTreeMap::get` (O(1) vs O(log n) with the
348    /// associated pointer chasing). Derived; intentionally not serialized.
349    inline_cache_index: Vec<u32>,
350    /// Test/bench scratch entries for validating inline-cache transitions.
351    /// Runtime execution keeps live cache entries on each `Vm` isolate so
352    /// parallel workers do not contend on shared compiled chunks.
353    inline_caches: Arc<Mutex<Vec<InlineCacheEntry>>>,
354    /// Lazily-materialized shared string cache for `Constant::String` entries,
355    /// parallel to `constants`. String constants are materialized once per
356    /// unique constant; subsequent pushes are a [`HarnStr`] refcount bump.
357    constant_strings: Arc<Mutex<Vec<Option<crate::value::HarnStr>>>>,
358    /// Source-name metadata for slot-indexed locals in this chunk.
359    pub(crate) local_slots: Vec<LocalSlotInfo>,
360    /// True when this chunk's bytecode emits an opcode that resolves a
361    /// name through the runtime env (`GetVar`, `SetVar`, `CallBuiltin`,
362    /// `CallBuiltinSpread`, `CheckType`). The closure-call hot path uses
363    /// this as a cheap static guard: if a closure body never reads
364    /// outer names by name, the caller-scope late-bind walks in
365    /// [`Vm::closure_call_env`] and
366    /// [`Vm::closure_call_env_for_current_frame`] are pure overhead and
367    /// can be skipped, leaving the closure's captured env as-is.
368    ///
369    /// Walks exist to inject late-bound closure-typed names — typically
370    /// for self/mutually-recursive local fns and for fns whose captured
371    /// env predates a sibling definition. Inline arithmetic / comparison
372    /// callbacks (the `.map(x -> x * 2)` / `.filter(x -> x % 2 == 0)`
373    /// shape) emit none of the flagged opcodes, so the walk is wasted
374    /// work on every invocation.
375    pub(crate) references_outer_names: bool,
376    /// Compile-time operand-stack-depth tracking for the debug-build
377    /// balance assertion (issue #2622). `balance_depth` is the running net
378    /// effect of every *linearly-modeled* opcode emitted so far;
379    /// `balance_nonlinear` counts emits whose effect can't be tracked by a
380    /// straight-line sum (jumps, `return`, async/handler ops, variadic ops
381    /// whose count isn't an emit argument). A statement is "balance-exact"
382    /// only when `balance_nonlinear` is unchanged across its compilation,
383    /// at which point `balance_depth`'s delta is its true net stack effect.
384    /// Transient compile-time state: reset by [`Chunk::new`], never
385    /// serialized into [`CachedChunk`], and read only by debug assertions —
386    /// so a wrong absolute value (which a non-exact statement can leave
387    /// behind) is harmless; only per-statement *deltas over exact spans*
388    /// are ever trusted.
389    #[cfg(debug_assertions)]
390    balance_depth: i32,
391    #[cfg(debug_assertions)]
392    balance_nonlinear: u32,
393}
394
395pub type ChunkRef = Arc<Chunk>;
396pub type CompiledFunctionRef = Arc<CompiledFunction>;
397
398impl Clone for Chunk {
399    fn clone(&self) -> Self {
400        Self {
401            cache_id: self.cache_id,
402            code: self.code.clone(),
403            constants: self.constants.clone(),
404            constant_index: self.constant_index.clone(),
405            lines: self.lines.clone(),
406            columns: self.columns.clone(),
407            source_file: self.source_file.clone(),
408            current_col: self.current_col,
409            functions: self.functions.clone(),
410            inline_cache_slots: self.inline_cache_slots.clone(),
411            inline_cache_index: self.inline_cache_index.clone(),
412            inline_caches: Arc::new(Mutex::new(vec![
413                InlineCacheEntry::Empty;
414                self.inline_cache_slot_count()
415            ])),
416            constant_strings: Arc::new(Mutex::new(vec![None; self.constants.len()])),
417            local_slots: self.local_slots.clone(),
418            references_outer_names: self.references_outer_names,
419            #[cfg(debug_assertions)]
420            balance_depth: self.balance_depth,
421            #[cfg(debug_assertions)]
422            balance_nonlinear: self.balance_nonlinear,
423        }
424    }
425}
426
427/// Serializable snapshot of a [`Chunk`] suitable for the on-disk bytecode
428/// cache and for in-memory stdlib artifact caches. Inline-cache state is
429/// dropped at freeze time because it warms at runtime per VM isolate; the
430/// rest of the chunk round-trips byte-identically.
431#[derive(Debug, Serialize, Deserialize)]
432pub struct CachedChunk {
433    pub(crate) code: Vec<u8>,
434    pub(crate) constants: Vec<Constant>,
435    pub(crate) lines: Vec<u32>,
436    pub(crate) columns: Vec<u32>,
437    pub(crate) source_file: Option<String>,
438    pub(crate) current_col: u32,
439    pub(crate) functions: Vec<CachedCompiledFunction>,
440    pub(crate) inline_cache_slots: BTreeMap<usize, usize>,
441    pub(crate) local_slots: Vec<LocalSlotInfo>,
442    #[serde(default)]
443    pub(crate) references_outer_names: bool,
444}
445
446#[derive(Debug, Serialize, Deserialize)]
447pub struct CachedCompiledFunction {
448    pub(crate) name: String,
449    pub(crate) type_params: Vec<String>,
450    pub(crate) nominal_type_names: Vec<String>,
451    pub(crate) params: Vec<CachedParamSlot>,
452    pub(crate) default_start: Option<usize>,
453    pub(crate) chunk: CachedChunk,
454    pub(crate) is_generator: bool,
455    pub(crate) is_stream: bool,
456    pub(crate) has_rest_param: bool,
457    pub(crate) has_runtime_type_checks: bool,
458}
459
460#[derive(Debug, Serialize, Deserialize)]
461pub(crate) struct CachedParamSlot {
462    pub(crate) name: String,
463    pub(crate) type_expr: Option<TypeExpr>,
464    pub(crate) has_default: bool,
465}
466
467impl CachedParamSlot {
468    fn thaw(self) -> ParamSlot {
469        let runtime_guard = self
470            .type_expr
471            .as_ref()
472            .map(RuntimeParamGuard::from_type_expr);
473        ParamSlot {
474            name: self.name,
475            type_expr: self.type_expr,
476            runtime_guard,
477            has_default: self.has_default,
478        }
479    }
480}
481
482/// One parameter slot of a compiled user-defined function. Carries the
483/// declared name, the (optional) declared type expression, and a flag
484/// for whether a default value was provided. The runtime consults the
485/// type expression in `bind_param_slots` to enforce declared types
486/// against the values supplied at the call site.
487#[derive(Debug, Clone, Serialize, Deserialize)]
488pub struct ParamSlot {
489    pub name: String,
490    /// Declared parameter type. `None` for untyped parameters (gradual
491    /// typing); the runtime skips type assertion when absent.
492    pub type_expr: Option<TypeExpr>,
493    /// Precomputed runtime validation metadata derived from `type_expr`.
494    /// Bytecode-cache artifacts omit this field and rebuild it at load time.
495    #[serde(skip)]
496    pub(crate) runtime_guard: Option<RuntimeParamGuard>,
497    /// True when the parameter has a default-value clause. Diagnostic
498    /// only — the canonical authority for arity ranges is
499    /// [`CompiledFunction::default_start`].
500    pub has_default: bool,
501}
502
503impl ParamSlot {
504    /// Build a [`ParamSlot`] from a parser-side [`harn_parser::TypedParam`].
505    /// Centralizes the conversion so every compile path stays in lockstep.
506    pub fn from_typed_param(param: &harn_parser::TypedParam) -> Self {
507        Self::from_typed_param_with_type(param, param.type_expr.clone())
508    }
509
510    pub(crate) fn from_typed_param_with_type(
511        param: &harn_parser::TypedParam,
512        type_expr: Option<TypeExpr>,
513    ) -> Self {
514        let runtime_guard = type_expr.as_ref().map(RuntimeParamGuard::from_type_expr);
515        Self {
516            name: param.name.clone(),
517            type_expr,
518            runtime_guard,
519            has_default: param.default_value.is_some(),
520        }
521    }
522
523    fn freeze_for_cache(&self) -> CachedParamSlot {
524        CachedParamSlot {
525            name: self.name.clone(),
526            type_expr: self.type_expr.clone(),
527            has_default: self.has_default,
528        }
529    }
530
531    /// Build a `Vec<ParamSlot>` from a slice of parser-side typed
532    /// parameters. Used pervasively at compile sites instead of
533    /// `TypedParam::names` (which discarded the type info we now need
534    /// at runtime).
535    pub fn vec_from_typed(params: &[harn_parser::TypedParam]) -> Vec<Self> {
536        params.iter().map(Self::from_typed_param).collect()
537    }
538}
539
540/// A compiled function (closure body).
541#[derive(Debug, Clone)]
542pub struct CompiledFunction {
543    pub name: String,
544    /// Generic type parameters declared by this function. Runtime
545    /// validation treats these as static-only constraints because the VM
546    /// does not monomorphize function bodies.
547    pub type_params: Vec<String>,
548    /// User-defined struct and enum names visible when this function was
549    /// compiled. These are the only non-primitive named types with runtime
550    /// nominal identity; aliases and interfaces remain static-only.
551    pub nominal_type_names: Vec<String>,
552    pub params: Vec<ParamSlot>,
553    /// Index of the first parameter with a default value, or None if all required.
554    pub default_start: Option<usize>,
555    pub chunk: ChunkRef,
556    /// True if the function body contains `yield` expressions (generator function).
557    pub is_generator: bool,
558    /// True if the function was declared as `gen fn` and should return Stream.
559    pub is_stream: bool,
560    /// True if the last parameter is a rest parameter (`...name`).
561    pub has_rest_param: bool,
562    /// True when at least one parameter has a runtime-visible type
563    /// assertion. Untyped closures dominate collection callback hot paths,
564    /// so this lets the VM skip the per-argument metadata walk after the
565    /// arity check.
566    pub has_runtime_type_checks: bool,
567}
568
569impl CompiledFunction {
570    pub(crate) fn has_runtime_type_checks_for_params(params: &[ParamSlot]) -> bool {
571        params.iter().any(|param| param.type_expr.is_some())
572    }
573
574    /// Returns just the parameter names — convenience for code paths that
575    /// don't care about types or defaults.
576    pub fn param_names(&self) -> impl Iterator<Item = &str> {
577        self.params.iter().map(|p| p.name.as_str())
578    }
579
580    /// Number of required parameters (those before `default_start`).
581    pub fn required_param_count(&self) -> usize {
582        self.default_start.unwrap_or(self.params.len())
583    }
584
585    /// Minimum number of caller-supplied arguments needed to enter the function.
586    pub(crate) fn minimum_arg_count(&self) -> usize {
587        if self.has_rest_param {
588            self.required_param_count()
589                .min(self.params.len().saturating_sub(1))
590        } else {
591            self.required_param_count()
592        }
593    }
594
595    /// Argument count visible to callee bytecode via `GetArgc`.
596    pub(crate) fn callee_arg_count(&self, supplied: usize) -> usize {
597        if self.has_rest_param {
598            supplied
599        } else {
600            supplied.min(self.params.len())
601        }
602    }
603
604    pub fn declares_type_param(&self, name: &str) -> bool {
605        self.type_params.iter().any(|param| param == name)
606    }
607
608    pub fn has_nominal_type(&self, name: &str) -> bool {
609        self.nominal_type_names.iter().any(|ty| ty == name)
610    }
611
612    pub(crate) fn freeze_for_cache(&self) -> CachedCompiledFunction {
613        CachedCompiledFunction {
614            name: self.name.clone(),
615            type_params: self.type_params.clone(),
616            nominal_type_names: self.nominal_type_names.clone(),
617            params: self
618                .params
619                .iter()
620                .map(ParamSlot::freeze_for_cache)
621                .collect(),
622            default_start: self.default_start,
623            chunk: self.chunk.freeze_for_cache(),
624            is_generator: self.is_generator,
625            is_stream: self.is_stream,
626            has_rest_param: self.has_rest_param,
627            has_runtime_type_checks: self.has_runtime_type_checks,
628        }
629    }
630
631    pub(crate) fn from_cached(cached: CachedCompiledFunction) -> Self {
632        Self {
633            name: cached.name,
634            type_params: cached.type_params,
635            nominal_type_names: cached.nominal_type_names,
636            params: cached
637                .params
638                .into_iter()
639                .map(CachedParamSlot::thaw)
640                .collect(),
641            default_start: cached.default_start,
642            chunk: Arc::new(Chunk::from_cached(cached.chunk)),
643            is_generator: cached.is_generator,
644            is_stream: cached.is_stream,
645            has_rest_param: cached.has_rest_param,
646            has_runtime_type_checks: cached.has_runtime_type_checks,
647        }
648    }
649}
650
651/// A snapshot of [`Chunk`]'s compile-time balance model, returned by
652/// [`Chunk::balance_probe`] and consumed by [`Chunk::balance_delta_since`].
653#[cfg(debug_assertions)]
654#[derive(Clone, Copy)]
655pub(crate) struct BalanceProbe {
656    depth: i32,
657    nonlinear: u32,
658}
659
660/// Net operand-stack effect (`pushes - pops`) of one emitted opcode, for
661/// the debug-build balance assertion (issue #2622). `count` is the opcode's
662/// variadic arity when that arity is the emit-call argument (`BuildList`
663/// length, `Call` argc, …) and `0` otherwise.
664///
665/// `Some(delta)` means the effect is exactly modeled. `None` marks an
666/// opcode a straight-line running sum can't track — control flow that
667/// branches or terminates (`Jump*`, `Return`, `Throw`, `TailCall`),
668/// async/handler ops, and variadic ops whose arity rides in a raw operand
669/// byte rather than the emit argument (`BuildEnum`, `MatchEnum`). Such an
670/// opcode taints its enclosing statement as non-exact, so the assertion
671/// skips it instead of risking a false trip.
672///
673/// The `match` is intentionally exhaustive with no `_` arm: adding an
674/// opcode forces a classification here (a compile error otherwise), so the
675/// balance model can't silently drift out of sync with the instruction set.
676#[cfg(debug_assertions)]
677fn op_stack_delta(op: Op, count: u16) -> Option<i32> {
678    use Op::*;
679    let count = count as i32;
680    Some(match op {
681        // Push one value.
682        Constant | Nil | True | False | RootHarness | GetVar | GetArgc | GetLocalSlot | Closure
683        | Dup => 1,
684        // Consume one value (into a binding / property / discard). `SetVar`,
685        // `SetProperty` and the local-slot stores read their target by name
686        // or slot index, so they only pop the value being stored.
687        DefLet | DefVar | DefCell | SetVar | DefLocalSlot | SetLocalSlot | SetProperty
688        | SetLocalSlotProperty | ConcatAssignLocal | Pop => -1,
689        // Value-preserving: unary ops, by-name lookups/checks, and scope /
690        // iterator / exception-handler bookkeeping (the last three touch
691        // side stacks, not the operand stack).
692        Negate | Not | GetProperty | GetPropertyOpt | CheckType | TryUnwrap | TryWrapOk | Swap
693        | PushScope | PopScope | PopIterator | PopHandler => 0,
694        // Pop two, push one.
695        Add | Sub | Mul | Div | Mod | Pow | AddInt | SubInt | MulInt | DivInt | ModInt
696        | AddFloat | SubFloat | MulFloat | DivFloat | ModFloat | Equal | NotEqual | Less
697        | Greater | LessEqual | GreaterEqual | EqualInt | NotEqualInt | LessInt | GreaterInt
698        | LessEqualInt | GreaterEqualInt | EqualFloat | NotEqualFloat | LessFloat
699        | GreaterFloat | LessEqualFloat | GreaterEqualFloat | EqualBool | NotEqualBool
700        | EqualString | NotEqualString | Contains | Subscript | SubscriptOpt => -1,
701        // `IterInit` consumes the iterable and pushes nothing (the iterator
702        // lives on a side stack).
703        IterInit => -1,
704        // Net -2: `Slice` pops object/start/end and pushes one value;
705        // subscript stores pop value/index and read the target from bytecode.
706        Slice | SetSubscript | SetLocalSlotSubscript => -2,
707        // Variadic whose arity is the emit argument: pop `count`, push one.
708        BuildList | Concat | CallBuiltin => 1 - count,
709        BuildDict => 1 - 2 * count,
710        // Calls also pop the callee/receiver beneath the args.
711        Call | MethodCall | MethodCallOpt => -count,
712        // Non-linear (see doc comment): branches, terminators, async/handler
713        // ops, and variadic ops whose arity isn't the emit argument.
714        Jump | JumpIfFalse | JumpIfTrue | IterNext | Return | TailCall | Throw | TryCatchSetup
715        | Spawn | Pipe | Parallel | ParallelMap | ParallelMapStream | ParallelSettle
716        | SyncMutexEnter | SyncMutexEnterKeyed | TaskScopeEnter | TaskScopeExit | Import
717        | SelectiveImport | NamespaceImport | DeadlineSetup | DeadlineEnd | BuildEnum
718        | MatchEnum | Yield | CallSpread | CallBuiltinSpread | MethodCallSpread => return None,
719    })
720}
721
722impl Chunk {
723    pub fn new() -> Self {
724        Self {
725            cache_id: next_chunk_cache_id(),
726            code: Vec::new(),
727            constants: Vec::new(),
728            constant_index: Some(HashMap::new()),
729            lines: Vec::new(),
730            columns: Vec::new(),
731            source_file: None,
732            current_col: 0,
733            functions: Vec::new(),
734            inline_cache_slots: BTreeMap::new(),
735            inline_cache_index: Vec::new(),
736            inline_caches: Arc::new(Mutex::new(Vec::new())),
737            constant_strings: Arc::new(Mutex::new(Vec::new())),
738            local_slots: Vec::new(),
739            references_outer_names: false,
740            #[cfg(debug_assertions)]
741            balance_depth: 0,
742            #[cfg(debug_assertions)]
743            balance_nonlinear: 0,
744        }
745    }
746
747    /// Set the current column for subsequent emit calls.
748    pub fn set_column(&mut self, col: u32) {
749        self.current_col = col;
750    }
751
752    /// Add a constant and return its index.
753    pub fn add_constant(&mut self, constant: Constant) -> u16 {
754        if self.constant_index.is_none() {
755            self.constant_index = Some(build_constant_index(&self.constants));
756        }
757        let index_map = self
758            .constant_index
759            .as_mut()
760            .expect("constant side index was just derived");
761        debug_assert!(
762            index_map.len() <= self.constants.len(),
763            "constant side index cannot outgrow the constant pool"
764        );
765        let key = ConstantKey::from(&constant);
766        if let Some(index) = index_map.get(&key) {
767            debug_assert!(
768                self.constants
769                    .get(*index as usize)
770                    .is_some_and(|existing| constants_identical(existing, &constant)),
771                "constant side index drifted from the constant pool"
772            );
773            return *index;
774        }
775        let idx = self.constants.len();
776        let idx = u16::try_from(idx).expect("constant pool exceeded u16 operand space");
777        index_map.insert(key, idx);
778        self.constants.push(constant);
779        idx
780    }
781
782    /// Emit a single-byte instruction.
783    pub fn emit(&mut self, op: Op, line: u32) {
784        #[cfg(debug_assertions)]
785        self.note_balance(op, 0);
786        let col = self.current_col;
787        let op_offset = self.code.len();
788        self.code.push(op as u8);
789        self.lines.push(line);
790        self.columns.push(col);
791        if is_adaptive_binary_op(op) {
792            self.register_inline_cache(op_offset);
793        }
794        if op_reads_outer_name(op) {
795            self.references_outer_names = true;
796        }
797    }
798
799    /// Emit an instruction with a u16 argument.
800    pub fn emit_u16(&mut self, op: Op, arg: u16, line: u32) {
801        #[cfg(debug_assertions)]
802        self.note_balance(op, arg);
803        let col = self.current_col;
804        let op_offset = self.code.len();
805        self.code.push(op as u8);
806        self.code.push((arg >> 8) as u8);
807        self.code.push((arg & 0xFF) as u8);
808        self.lines.push(line);
809        self.lines.push(line);
810        self.lines.push(line);
811        self.columns.push(col);
812        self.columns.push(col);
813        self.columns.push(col);
814        if matches!(
815            op,
816            Op::GetProperty | Op::GetPropertyOpt | Op::MethodCallSpread | Op::ConcatAssignLocal
817        ) {
818            self.register_inline_cache(op_offset);
819        }
820        if op_reads_outer_name(op) {
821            self.references_outer_names = true;
822        }
823    }
824
825    /// Emit a local-slot property assignment:
826    /// opcode + u16 property constant index + u16 local slot index.
827    pub fn emit_set_local_slot_property(&mut self, prop_idx: u16, slot: u16, line: u32) {
828        #[cfg(debug_assertions)]
829        self.note_balance(Op::SetLocalSlotProperty, 0);
830        let col = self.current_col;
831        self.code.push(Op::SetLocalSlotProperty as u8);
832        self.code.push((prop_idx >> 8) as u8);
833        self.code.push((prop_idx & 0xFF) as u8);
834        self.code.push((slot >> 8) as u8);
835        self.code.push((slot & 0xFF) as u8);
836        for _ in 0..5 {
837            self.lines.push(line);
838            self.columns.push(col);
839        }
840    }
841
842    /// Emit an instruction with a u8 argument.
843    pub fn emit_u8(&mut self, op: Op, arg: u8, line: u32) {
844        #[cfg(debug_assertions)]
845        self.note_balance(op, arg as u16);
846        let col = self.current_col;
847        let op_offset = self.code.len();
848        self.code.push(op as u8);
849        self.code.push(arg);
850        self.lines.push(line);
851        self.lines.push(line);
852        self.columns.push(col);
853        self.columns.push(col);
854        if matches!(op, Op::Call) {
855            self.register_inline_cache(op_offset);
856        }
857        if op_reads_outer_name(op) {
858            self.references_outer_names = true;
859        }
860    }
861
862    /// Emit a direct builtin call.
863    pub fn emit_call_builtin(
864        &mut self,
865        id: crate::BuiltinId,
866        name_idx: u16,
867        arg_count: u8,
868        line: u32,
869    ) {
870        #[cfg(debug_assertions)]
871        self.note_balance(Op::CallBuiltin, arg_count as u16);
872        let col = self.current_col;
873        let op_offset = self.code.len();
874        self.code.push(Op::CallBuiltin as u8);
875        self.code.extend_from_slice(&id.raw().to_be_bytes());
876        self.code.push((name_idx >> 8) as u8);
877        self.code.push((name_idx & 0xFF) as u8);
878        self.code.push(arg_count);
879        for _ in 0..12 {
880            self.lines.push(line);
881            self.columns.push(col);
882        }
883        self.register_inline_cache(op_offset);
884        self.references_outer_names = true;
885    }
886
887    /// Emit a direct builtin spread call.
888    pub fn emit_call_builtin_spread(&mut self, id: crate::BuiltinId, name_idx: u16, line: u32) {
889        #[cfg(debug_assertions)]
890        self.note_balance(Op::CallBuiltinSpread, 0);
891        let col = self.current_col;
892        self.code.push(Op::CallBuiltinSpread as u8);
893        self.code.extend_from_slice(&id.raw().to_be_bytes());
894        self.code.push((name_idx >> 8) as u8);
895        self.code.push((name_idx & 0xFF) as u8);
896        for _ in 0..11 {
897            self.lines.push(line);
898            self.columns.push(col);
899        }
900        self.references_outer_names = true;
901    }
902
903    /// Emit a method call: op + u16 (method name) + u8 (arg count).
904    pub fn emit_method_call(&mut self, name_idx: u16, arg_count: u8, line: u32) {
905        self.emit_method_call_inner(Op::MethodCall, name_idx, arg_count, line);
906    }
907
908    /// Emit an optional method call (?.) — returns nil if receiver is nil.
909    pub fn emit_method_call_opt(&mut self, name_idx: u16, arg_count: u8, line: u32) {
910        self.emit_method_call_inner(Op::MethodCallOpt, name_idx, arg_count, line);
911    }
912
913    fn emit_method_call_inner(&mut self, op: Op, name_idx: u16, arg_count: u8, line: u32) {
914        #[cfg(debug_assertions)]
915        self.note_balance(op, arg_count as u16);
916        let col = self.current_col;
917        let op_offset = self.code.len();
918        self.code.push(op as u8);
919        self.code.push((name_idx >> 8) as u8);
920        self.code.push((name_idx & 0xFF) as u8);
921        self.code.push(arg_count);
922        self.lines.push(line);
923        self.lines.push(line);
924        self.lines.push(line);
925        self.lines.push(line);
926        self.columns.push(col);
927        self.columns.push(col);
928        self.columns.push(col);
929        self.columns.push(col);
930        self.register_inline_cache(op_offset);
931    }
932
933    /// Current code offset (for jump patching).
934    pub fn current_offset(&self) -> usize {
935        self.code.len()
936    }
937
938    /// Emit a jump instruction with a placeholder offset. Returns the position to patch.
939    pub fn emit_jump(&mut self, op: Op, line: u32) -> usize {
940        #[cfg(debug_assertions)]
941        self.note_balance(op, 0);
942        let col = self.current_col;
943        self.code.push(op as u8);
944        let patch_pos = self.code.len();
945        self.code.push(0xFF);
946        self.code.push(0xFF);
947        self.lines.push(line);
948        self.lines.push(line);
949        self.lines.push(line);
950        self.columns.push(col);
951        self.columns.push(col);
952        self.columns.push(col);
953        patch_pos
954    }
955
956    /// Patch a jump instruction at the given position to jump to the current offset.
957    pub fn patch_jump(&mut self, patch_pos: usize) {
958        let target = self.code.len() as u16;
959        self.code[patch_pos] = (target >> 8) as u8;
960        self.code[patch_pos + 1] = (target & 0xFF) as u8;
961    }
962
963    /// Patch a jump to a specific target position.
964    pub fn patch_jump_to(&mut self, patch_pos: usize, target: usize) {
965        let target = target as u16;
966        self.code[patch_pos] = (target >> 8) as u8;
967        self.code[patch_pos + 1] = (target & 0xFF) as u8;
968    }
969
970    /// Read a u16 argument at the given position.
971    pub fn read_u16(&self, pos: usize) -> u16 {
972        ((self.code[pos] as u16) << 8) | (self.code[pos + 1] as u16)
973    }
974
975    /// Fold one just-emitted opcode into the compile-time operand-stack
976    /// balance model (issue #2622). See [`op_stack_delta`] for the
977    /// linear-vs-non-linear classification.
978    #[cfg(debug_assertions)]
979    fn note_balance(&mut self, op: Op, count: u16) {
980        match op_stack_delta(op, count) {
981            Some(delta) => self.balance_depth += delta,
982            None => self.balance_nonlinear += 1,
983        }
984    }
985
986    /// Snapshot the balance model before compiling a statement; pair with
987    /// [`Chunk::balance_delta_since`].
988    #[cfg(debug_assertions)]
989    pub(crate) fn balance_probe(&self) -> BalanceProbe {
990        BalanceProbe {
991            depth: self.balance_depth,
992            nonlinear: self.balance_nonlinear,
993        }
994    }
995
996    /// Net operand-stack effect emitted since `probe`, or `None` when any
997    /// non-linearly-modeled opcode was emitted in that span (which makes
998    /// the running sum untrustworthy, so callers must not assert on it).
999    /// The absolute `balance_depth` may be meaningless after a non-exact
1000    /// span — only deltas over a fully-exact span are valid.
1001    #[cfg(debug_assertions)]
1002    pub(crate) fn balance_delta_since(&self, probe: BalanceProbe) -> Option<i32> {
1003        if self.balance_nonlinear == probe.nonlinear {
1004            Some(self.balance_depth - probe.depth)
1005        } else {
1006            None
1007        }
1008    }
1009
1010    fn register_inline_cache(&mut self, op_offset: usize) {
1011        if self.inline_cache_slots.contains_key(&op_offset) {
1012            return;
1013        }
1014        let mut entries = self.inline_caches.lock();
1015        let slot = entries.len();
1016        entries.push(InlineCacheEntry::Empty);
1017        self.inline_cache_slots.insert(op_offset, slot);
1018        Self::write_inline_cache_index(&mut self.inline_cache_index, op_offset, slot);
1019    }
1020
1021    /// Fast-path side-table writer. Pulled out as an associated fn so both
1022    /// the live emit path and [`Chunk::from_cached`] share the same growth
1023    /// strategy. Cache slots fit comfortably in `u32` because the slot count
1024    /// is bounded by the cacheable-opcode count in `code`.
1025    fn write_inline_cache_index(index: &mut Vec<u32>, op_offset: usize, slot: usize) {
1026        if op_offset >= index.len() {
1027            index.resize(op_offset + 1, NO_INLINE_CACHE_SLOT);
1028        }
1029        index[op_offset] = slot as u32;
1030    }
1031
1032    /// Look up the inline-cache slot for the opcode at `op_offset`. This is
1033    /// called on every dispatch of an adaptive binary op (Add/Sub/Mul/Div/
1034    /// Mod/Eq/Neq/Less/Greater/LessEq/GreaterEq), `Op::Call`, `Op::MethodCall`
1035    /// (and `MethodCallOpt`/`MethodCallSpread`), and `Op::GetProperty`
1036    /// (`GetPropertyOpt`). Backed by [`Chunk::inline_cache_index`] — a flat
1037    /// `Vec<u32>` indexed by code offset — so the lookup is a single bounds-
1038    /// checked array read instead of the prior `BTreeMap::get` which walked
1039    /// internal nodes for every dispatched op.
1040    #[inline]
1041    pub(crate) fn inline_cache_slot(&self, op_offset: usize) -> Option<usize> {
1042        match self.inline_cache_index.get(op_offset).copied() {
1043            None | Some(NO_INLINE_CACHE_SLOT) => None,
1044            Some(slot) => Some(slot as usize),
1045        }
1046    }
1047
1048    pub(crate) fn inline_cache_slot_count(&self) -> usize {
1049        self.inline_cache_slots.len()
1050    }
1051
1052    pub(crate) fn cache_id(&self) -> u64 {
1053        self.cache_id
1054    }
1055
1056    /// Pre-optimization control path: the `BTreeMap`-backed lookup the
1057    /// dispatcher used before the flat `Vec<u32>` side-table. Exposed
1058    /// only behind the `vm-bench-internals` feature so the criterion
1059    /// microbench can A/B the two paths inside one binary on identical
1060    /// hardware. The production hot path must keep using
1061    /// [`Chunk::inline_cache_slot`].
1062    #[cfg(feature = "vm-bench-internals")]
1063    pub fn inline_cache_slot_via_btreemap_for_bench(&self, op_offset: usize) -> Option<usize> {
1064        self.inline_cache_slots.get(&op_offset).copied()
1065    }
1066
1067    /// Returns a shared string for a `Constant::String` at the given pool
1068    /// index, materializing it on first access and caching for reuse.
1069    /// Returns `None` when the constant at `idx` is not a string (the
1070    /// caller should fall back to the regular `Constant` match).
1071    pub(crate) fn constant_string_rc(&self, idx: usize) -> Option<crate::value::HarnStr> {
1072        // Borrow the side table mutably so we can lazily extend / fill
1073        // entries. The borrow is scope-confined to this function; the
1074        // VM never re-enters constant_string_rc for the same chunk
1075        // during a single materialization, so no nested-borrow risk.
1076        let mut entries = self.constant_strings.lock();
1077        if entries.len() < self.constants.len() {
1078            entries.resize(self.constants.len(), None);
1079        }
1080        if let Some(Some(existing)) = entries.get(idx) {
1081            return Some(existing.clone());
1082        }
1083        let materialized = match self.constants.get(idx)? {
1084            Constant::String(s) => crate::value::HarnStr::from(s.as_str()),
1085            _ => return None,
1086        };
1087        entries[idx] = Some(materialized.clone());
1088        Some(materialized)
1089    }
1090
1091    /// Test helper for the chunk-local scratch inline cache. Production
1092    /// dispatch reads VM-local cache sets through `Vm`.
1093    #[inline]
1094    #[cfg(test)]
1095    pub(crate) fn peek_adaptive_binary_cache(
1096        &self,
1097        slot: usize,
1098    ) -> Option<(AdaptiveBinaryOp, AdaptiveBinaryState)> {
1099        match self.inline_caches.lock().get(slot)? {
1100            &InlineCacheEntry::AdaptiveBinary { op, state } => Some((op, state)),
1101            _ => None,
1102        }
1103    }
1104
1105    /// Test helper for the chunk-local scratch inline cache. Production
1106    /// dispatch reads VM-local cache sets through `Vm`.
1107    #[inline]
1108    #[cfg(test)]
1109    pub(crate) fn peek_method_cache(&self, slot: usize) -> Option<(u16, usize, MethodCacheTarget)> {
1110        match self.inline_caches.lock().get(slot)? {
1111            &InlineCacheEntry::Method {
1112                name_idx,
1113                argc,
1114                target,
1115            } => Some((name_idx, argc, target)),
1116            _ => None,
1117        }
1118    }
1119
1120    /// Test helper for the chunk-local scratch inline cache. Production
1121    /// dispatch reads VM-local cache sets through `Vm`.
1122    #[inline]
1123    #[cfg(test)]
1124    pub(crate) fn peek_property_cache(&self, slot: usize) -> Option<(u16, PropertyCacheTarget)> {
1125        match self.inline_caches.lock().get(slot)? {
1126            InlineCacheEntry::Property { name_idx, target } => Some((*name_idx, target.clone())),
1127            _ => None,
1128        }
1129    }
1130
1131    /// Test helper for the chunk-local scratch inline cache. Production
1132    /// dispatch reads VM-local cache sets through `Vm`.
1133    #[inline]
1134    #[cfg(test)]
1135    pub(crate) fn peek_direct_call_state(&self, slot: usize) -> Option<DirectCallState> {
1136        match self.inline_caches.lock().get(slot)? {
1137            InlineCacheEntry::DirectCall { state } => Some(state.clone()),
1138            _ => None,
1139        }
1140    }
1141
1142    #[cfg(test)]
1143    pub(crate) fn set_inline_cache_entry(&self, slot: usize, entry: InlineCacheEntry) {
1144        if let Some(existing) = self.inline_caches.lock().get_mut(slot) {
1145            *existing = entry;
1146        }
1147    }
1148
1149    pub fn freeze_for_cache(&self) -> CachedChunk {
1150        CachedChunk {
1151            code: self.code.clone(),
1152            constants: self.constants.clone(),
1153            lines: self.lines.clone(),
1154            columns: self.columns.clone(),
1155            source_file: self.source_file.clone(),
1156            current_col: self.current_col,
1157            functions: self
1158                .functions
1159                .iter()
1160                .map(|function| function.freeze_for_cache())
1161                .collect(),
1162            inline_cache_slots: self.inline_cache_slots.clone(),
1163            local_slots: self.local_slots.clone(),
1164            references_outer_names: self.references_outer_names,
1165        }
1166    }
1167
1168    pub fn from_cached(cached: CachedChunk) -> Self {
1169        let CachedChunk {
1170            code,
1171            constants,
1172            lines,
1173            columns,
1174            source_file,
1175            current_col,
1176            functions,
1177            inline_cache_slots,
1178            local_slots,
1179            references_outer_names,
1180        } = cached;
1181        let inline_cache_count = inline_cache_slots.len();
1182        let constants_count = constants.len();
1183        // Project the cached `BTreeMap<op_offset, slot>` into the flat
1184        // dispatch-side lookup table. Sized to `code.len()` so the hottest
1185        // hot opcodes (binary ops at the end of a long chunk) still hit the
1186        // fast-path bounds check rather than falling through to the
1187        // none-found branch. The size is bounded by code length, so the
1188        // memory footprint is tiny — a few KB for typical chunks.
1189        let mut inline_cache_index = Vec::new();
1190        inline_cache_index.resize(code.len(), NO_INLINE_CACHE_SLOT);
1191        for (&op_offset, &slot) in &inline_cache_slots {
1192            if op_offset < inline_cache_index.len() {
1193                inline_cache_index[op_offset] = slot as u32;
1194            }
1195        }
1196        Self {
1197            cache_id: next_chunk_cache_id(),
1198            code,
1199            constants,
1200            // Derived on demand: a cache-loaded chunk is executed, not appended to.
1201            constant_index: None,
1202            lines,
1203            columns,
1204            source_file,
1205            current_col,
1206            functions: functions
1207                .into_iter()
1208                .map(|function| Arc::new(CompiledFunction::from_cached(function)))
1209                .collect(),
1210            inline_cache_slots,
1211            inline_cache_index,
1212            inline_caches: Arc::new(Mutex::new(vec![
1213                InlineCacheEntry::Empty;
1214                inline_cache_count
1215            ])),
1216            constant_strings: Arc::new(Mutex::new(vec![None; constants_count])),
1217            local_slots,
1218            references_outer_names,
1219            #[cfg(debug_assertions)]
1220            balance_depth: 0,
1221            #[cfg(debug_assertions)]
1222            balance_nonlinear: 0,
1223        }
1224    }
1225
1226    pub(crate) fn add_local_slot(
1227        &mut self,
1228        name: String,
1229        mutable: bool,
1230        scope_depth: usize,
1231    ) -> u16 {
1232        let idx = self.local_slots.len();
1233        self.local_slots.push(LocalSlotInfo {
1234            name,
1235            mutable,
1236            scope_depth,
1237        });
1238        idx as u16
1239    }
1240
1241    /// Read a u64 argument at the given position.
1242    pub fn read_u64(&self, pos: usize) -> u64 {
1243        u64::from_be_bytes([
1244            self.code[pos],
1245            self.code[pos + 1],
1246            self.code[pos + 2],
1247            self.code[pos + 3],
1248            self.code[pos + 4],
1249            self.code[pos + 5],
1250            self.code[pos + 6],
1251            self.code[pos + 7],
1252        ])
1253    }
1254
1255    /// Disassemble the chunk for debugging. The per-opcode rendering is
1256    /// macro-generated alongside the dispatch tables in
1257    /// `crate::vm::ops` — see [`Self::disassemble_op`].
1258    pub fn disassemble(&self, name: &str) -> String {
1259        let mut out = format!("== {name} ==\n");
1260        let mut ip = 0;
1261        while ip < self.code.len() {
1262            let op_byte = self.code[ip];
1263            let line = self.lines.get(ip).copied().unwrap_or(0);
1264            out.push_str(&format!("{ip:04} [{line:>4}] "));
1265            ip += 1;
1266
1267            if let Some(op) = Op::from_byte(op_byte) {
1268                self.disassemble_op(op, &mut ip, &mut out);
1269            } else {
1270                out.push_str(&format!("UNKNOWN(0x{op_byte:02x})\n"));
1271            }
1272        }
1273        out
1274    }
1275}
1276
1277/// Disassembly helpers consumed by the macro-generated
1278/// [`Chunk::disassemble_op`]. Each helper takes the current code position
1279/// (already advanced past the opcode byte), advances it over the operand
1280/// bytes the opcode carries, and renders one human-readable line without
1281/// a trailing newline (the dispatcher appends it).
1282///
1283/// Defining one helper per operand layout — and not one per opcode —
1284/// keeps adding an opcode a one-line edit in the `define_opcodes!` table
1285/// rather than a paired edit here. New layouts live with the helpers;
1286/// new opcodes live with the dispatch.
1287pub(crate) fn disasm_bare(_chunk: &Chunk, _ip: &mut usize, label: &str) -> String {
1288    label.to_string()
1289}
1290
1291pub(crate) fn disasm_u8(chunk: &Chunk, ip: &mut usize, label: &str) -> String {
1292    let arg = chunk.code[*ip];
1293    *ip += 1;
1294    format!("{label} {arg:>4}")
1295}
1296
1297pub(crate) fn disasm_u16(chunk: &Chunk, ip: &mut usize, label: &str) -> String {
1298    let arg = chunk.read_u16(*ip);
1299    *ip += 2;
1300    format!("{label} {arg:>4}")
1301}
1302
1303pub(crate) fn disasm_try_catch_setup(chunk: &Chunk, ip: &mut usize, label: &str) -> String {
1304    let catch_offset = chunk.read_u16(*ip);
1305    *ip += 2;
1306    let type_idx = chunk.read_u16(*ip);
1307    *ip += 2;
1308    if let Some(type_name) = chunk.constants.get(type_idx as usize) {
1309        format!("{label} {catch_offset:>4} type {type_idx:>4} ({type_name})")
1310    } else {
1311        format!("{label} {catch_offset:>4} type {type_idx:>4}")
1312    }
1313}
1314
1315pub(crate) fn disasm_const_pool_u16(chunk: &Chunk, ip: &mut usize, label: &str) -> String {
1316    let idx = chunk.read_u16(*ip);
1317    *ip += 2;
1318    format!("{label} {idx:>4} ({})", chunk.constants[idx as usize])
1319}
1320
1321pub(crate) fn disasm_local_slot_u16(chunk: &Chunk, ip: &mut usize, label: &str) -> String {
1322    let slot = chunk.read_u16(*ip);
1323    *ip += 2;
1324    let mut out = format!("{label} {slot:>4}");
1325    if let Some(info) = chunk.local_slots.get(slot as usize) {
1326        out.push_str(&format!(" ({})", info.name));
1327    }
1328    out
1329}
1330
1331pub(crate) fn disasm_const_pool_local_slot(chunk: &Chunk, ip: &mut usize, label: &str) -> String {
1332    let prop = chunk.read_u16(*ip);
1333    *ip += 2;
1334    let slot = chunk.read_u16(*ip);
1335    *ip += 2;
1336    let mut out = format!(
1337        "{label} prop {prop:>4} ({}) slot {slot:>4}",
1338        chunk.constants[prop as usize]
1339    );
1340    if let Some(info) = chunk.local_slots.get(slot as usize) {
1341        out.push_str(&format!(" ({})", info.name));
1342    }
1343    out
1344}
1345
1346pub(crate) fn disasm_method_call(chunk: &Chunk, ip: &mut usize, label: &str) -> String {
1347    let idx = chunk.read_u16(*ip);
1348    *ip += 2;
1349    let argc = chunk.code[*ip];
1350    *ip += 1;
1351    format!(
1352        "{label} {idx:>4} ({}) argc={argc}",
1353        chunk.constants[idx as usize]
1354    )
1355}
1356
1357pub(crate) fn disasm_match_enum(chunk: &Chunk, ip: &mut usize, label: &str) -> String {
1358    let enum_idx = chunk.read_u16(*ip);
1359    *ip += 2;
1360    let var_idx = chunk.read_u16(*ip);
1361    *ip += 2;
1362    format!(
1363        "{label} {enum_idx:>4} ({}) {var_idx:>4} ({})",
1364        chunk.constants[enum_idx as usize], chunk.constants[var_idx as usize],
1365    )
1366}
1367
1368pub(crate) fn disasm_build_enum(chunk: &Chunk, ip: &mut usize, label: &str) -> String {
1369    let enum_idx = chunk.read_u16(*ip);
1370    *ip += 2;
1371    let var_idx = chunk.read_u16(*ip);
1372    *ip += 2;
1373    let field_count = chunk.read_u16(*ip);
1374    *ip += 2;
1375    format!(
1376        "{label} {enum_idx:>4} ({}) {var_idx:>4} ({}) fields={field_count}",
1377        chunk.constants[enum_idx as usize], chunk.constants[var_idx as usize],
1378    )
1379}
1380
1381pub(crate) fn disasm_selective_import(chunk: &Chunk, ip: &mut usize, label: &str) -> String {
1382    let path_idx = chunk.read_u16(*ip);
1383    *ip += 2;
1384    let names_idx = chunk.read_u16(*ip);
1385    *ip += 2;
1386    format!(
1387        "{label} {path_idx:>4} ({}) names: {names_idx:>4} ({})",
1388        chunk.constants[path_idx as usize], chunk.constants[names_idx as usize],
1389    )
1390}
1391
1392pub(crate) fn disasm_check_type(chunk: &Chunk, ip: &mut usize, label: &str) -> String {
1393    let var_idx = chunk.read_u16(*ip);
1394    *ip += 2;
1395    let type_idx = chunk.read_u16(*ip);
1396    *ip += 2;
1397    format!(
1398        "{label} {var_idx:>4} ({}) -> {type_idx:>4} ({})",
1399        chunk.constants[var_idx as usize], chunk.constants[type_idx as usize],
1400    )
1401}
1402
1403pub(crate) fn disasm_call_builtin(chunk: &Chunk, ip: &mut usize, label: &str) -> String {
1404    let id = chunk.read_u64(*ip);
1405    *ip += 8;
1406    let idx = chunk.read_u16(*ip);
1407    *ip += 2;
1408    let argc = chunk.code[*ip];
1409    *ip += 1;
1410    format!(
1411        "{label} {id:#018x} {idx:>4} ({}) argc={argc}",
1412        chunk.constants[idx as usize],
1413    )
1414}
1415
1416pub(crate) fn disasm_call_builtin_spread(chunk: &Chunk, ip: &mut usize, label: &str) -> String {
1417    let id = chunk.read_u64(*ip);
1418    *ip += 8;
1419    let idx = chunk.read_u16(*ip);
1420    *ip += 2;
1421    format!(
1422        "{label} {id:#018x} {idx:>4} ({})",
1423        chunk.constants[idx as usize],
1424    )
1425}
1426
1427pub(crate) fn disasm_method_call_spread(chunk: &Chunk, ip: &mut usize, label: &str) -> String {
1428    // emit_u16(Op::MethodCallSpread, name_idx, ...) writes opcode + 2
1429    // bytes of u16 name_idx, so the operand is read at *ip with the
1430    // usual `read_u16`. The previous hand-written disasm read at
1431    // `ip + 1`, which displayed the wrong constant index — silently
1432    // corrupting any disassembly that hit a `MethodCallSpread` opcode.
1433    let idx = chunk.read_u16(*ip);
1434    *ip += 2;
1435    format!("{label} {idx:>4} ({})", chunk.constants[idx as usize])
1436}
1437
1438impl Default for Chunk {
1439    fn default() -> Self {
1440        Self::new()
1441    }
1442}
1443
1444#[cfg(test)]
1445#[path = "chunk_tests.rs"]
1446mod tests;