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