Skip to main content

stryke/
bytecode.rs

1use serde::{Deserialize, Serialize};
2
3use crate::ast::{Block, ClassDef, EnumDef, Expr, MatchArm, StructDef, SubSigParam, TraitDef};
4use crate::value::PerlValue;
5
6/// `splice` operand tuple: array expr, offset, length, replacement list (see [`Chunk::splice_expr_entries`]).
7pub(crate) type SpliceExprEntry = (Expr, Option<Expr>, Option<Expr>, Vec<Expr>);
8
9/// `sub` body registered at run time (e.g. `BEGIN { sub f { ... } }`), mirrored from
10/// [`crate::interpreter::Interpreter::exec_statement`] `StmtKind::SubDecl`.
11#[derive(Debug, Clone, Serialize, Deserialize)]
12pub struct RuntimeSubDecl {
13    pub name: String,
14    pub params: Vec<SubSigParam>,
15    pub body: Block,
16    pub prototype: Option<String>,
17}
18
19/// Stack-based bytecode instruction set for the stryke VM.
20/// Operands use u16 for pool indices (64k names/constants) and i32 for jumps.
21#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
22pub enum Op {
23    Nop,
24    // ── Constants ──
25    LoadInt(i64),
26    LoadFloat(f64),
27    LoadConst(u16), // index into constant pool
28    LoadUndef,
29
30    // ── Stack ──
31    Pop,
32    Dup,
33    /// Duplicate the top two stack values: \[a, b\] (b on top) → \[a, b, a, b\].
34    Dup2,
35    /// Swap the top two stack values (PerlValue).
36    Swap,
37    /// Rotate the top three values upward (FORTH `rot`): `[a, b, c]` (c on top) → `[b, c, a]`.
38    Rot,
39    /// Pop one value; push [`PerlValue::scalar_context`] of that value (Perl aggregate rules).
40    ValueScalarContext,
41
42    // ── Scalars (u16 = name pool index) ──
43    GetScalar(u16),
44    /// Like `GetScalar` but reads `scope.get_scalar` only (no Perl special-variable dispatch).
45    GetScalarPlain(u16),
46    SetScalar(u16),
47    /// Like `SetScalar` but calls `scope.set_scalar` only (no special-variable dispatch).
48    SetScalarPlain(u16),
49    DeclareScalar(u16),
50    /// Like `DeclareScalar` but the binding is immutable after initialization.
51    DeclareScalarFrozen(u16),
52    /// `typed my $x : Type` — u8 encodes [`crate::ast::PerlTypeName`] (0=Int,1=Str,2=Float).
53    DeclareScalarTyped(u16, u8),
54    /// `frozen typed my $x : Type` — immutable after initialization + type-checked.
55    DeclareScalarTypedFrozen(u16, u8),
56
57    // ── State variables (persist across calls) ──
58    /// `state $x = EXPR` — pop TOS as initializer on first call only.
59    /// On subsequent calls the persisted value is used as the local binding.
60    /// Key: (sub entry IP, name_idx) in VM's state_vars table.
61    DeclareStateScalar(u16),
62    /// `state @arr = (...)` — array variant.
63    DeclareStateArray(u16),
64    /// `state %hash = (...)` — hash variant.
65    DeclareStateHash(u16),
66
67    // ── Arrays ──
68    GetArray(u16),
69    SetArray(u16),
70    DeclareArray(u16),
71    DeclareArrayFrozen(u16),
72    GetArrayElem(u16), // stack: [index] → value
73    SetArrayElem(u16), // stack: [value, index]
74    /// Like [`Op::SetArrayElem`] but leaves the assigned value on the stack (e.g. `$a[$i] //=`).
75    SetArrayElemKeep(u16),
76    PushArray(u16),  // stack: [value] → push to named array
77    PopArray(u16),   // → popped value
78    ShiftArray(u16), // → shifted value
79    ArrayLen(u16),   // → integer length
80    /// Pop index spec (scalar or array from [`Op::Range`]); push one `PerlValue::array` of elements
81    /// read from the named array. Used for `@name[...]` slice rvalues.
82    ArraySlicePart(u16),
83    /// Pop `b`, pop `a` (arrays); push concatenation `a` followed by `b` (Perl slice / list glue).
84    ArrayConcatTwo,
85    /// `exists $a[$i]` — stack: `[index]` → 0/1 (stash-qualified array name pool index).
86    ExistsArrayElem(u16),
87    /// `delete $a[$i]` — stack: `[index]` → deleted value (or undef).
88    DeleteArrayElem(u16),
89
90    // ── Hashes ──
91    GetHash(u16),
92    SetHash(u16),
93    DeclareHash(u16),
94    DeclareHashFrozen(u16),
95    /// Dynamic `local $x` — save previous binding, assign TOS (same stack shape as DeclareScalar).
96    LocalDeclareScalar(u16),
97    LocalDeclareArray(u16),
98    LocalDeclareHash(u16),
99    /// `local $h{key} = val` — stack: `[value, key]` (key on top), same as [`Op::SetHashElem`].
100    LocalDeclareHashElement(u16),
101    /// `local $a[i] = val` — stack: `[value, index]` (index on top), same as [`Op::SetArrayElem`].
102    LocalDeclareArrayElement(u16),
103    /// `local *name` or `local *name = *other` — second pool index is `Some(rhs)` when aliasing.
104    LocalDeclareTypeglob(u16, Option<u16>),
105    /// `local *{EXPR}` / `local *$x` — LHS glob name string on stack (TOS); optional static `*rhs` pool index.
106    LocalDeclareTypeglobDynamic(Option<u16>),
107    GetHashElem(u16), // stack: [key] → value
108    SetHashElem(u16), // stack: [value, key]
109    /// Like [`Op::SetHashElem`] but leaves the assigned value on the stack (e.g. `$h{k} //=`).
110    SetHashElemKeep(u16),
111    DeleteHashElem(u16), // stack: [key] → deleted value
112    ExistsHashElem(u16), // stack: [key] → 0/1
113    /// `delete $href->{key}` — stack: `[container, key]` (key on top) → deleted value.
114    DeleteArrowHashElem,
115    /// `exists $href->{key}` — stack: `[container, key]` → 0/1.
116    ExistsArrowHashElem,
117    /// `exists $aref->[$i]` — stack: `[container, index]` (index on top, int-coerced).
118    ExistsArrowArrayElem,
119    /// `delete $aref->[$i]` — stack: `[container, index]` → deleted value (or undef).
120    DeleteArrowArrayElem,
121    HashKeys(u16),   // → array of keys
122    HashValues(u16), // → array of values
123    /// Scalar `keys %h` — push integer key count.
124    HashKeysScalar(u16),
125    /// Scalar `values %h` — push integer value count.
126    HashValuesScalar(u16),
127    /// `keys EXPR` after operand evaluated in list context — stack: `[value]` → key list array.
128    KeysFromValue,
129    /// Scalar `keys EXPR` after operand — stack: `[value]` → key count.
130    KeysFromValueScalar,
131    /// `values EXPR` after operand evaluated in list context — stack: `[value]` → values array.
132    ValuesFromValue,
133    /// Scalar `values EXPR` after operand — stack: `[value]` → value count.
134    ValuesFromValueScalar,
135
136    /// `push @$aref, ITEM` — stack: `[aref, item]` (item on top); mutates; pushes `aref` back.
137    PushArrayDeref,
138    /// After `push @$aref, …` — stack: `[aref]` → `[len]` (consumes aref).
139    ArrayDerefLen,
140    /// `pop @$aref` — stack: `[aref]` → popped value.
141    PopArrayDeref,
142    /// `shift @$aref` — stack: `[aref]` → shifted value.
143    ShiftArrayDeref,
144    /// `unshift @$aref, LIST` — stack `[aref, v1, …, vn]` (vn on top); `n` extra values.
145    UnshiftArrayDeref(u8),
146    /// `splice @$aref, off, len, LIST` — stack top: replacements, then `len`, `off`, `aref` (`len` may be undef).
147    SpliceArrayDeref(u8),
148
149    // ── Arithmetic ──
150    Add,
151    Sub,
152    Mul,
153    Div,
154    Mod,
155    Pow,
156    Negate,
157    /// `inc EXPR` — pop value, push value + 1 (integer if input is integer, else float).
158    Inc,
159    /// `dec EXPR` — pop value, push value - 1.
160    Dec,
161
162    // ── String ──
163    Concat,
164    /// Pop array (or value coerced with [`PerlValue::to_list`]), join element strings with
165    /// [`Interpreter::list_separator`] (`$"`), push one string. Used for `@a` in `"` / `qq`.
166    ArrayStringifyListSep,
167    StringRepeat,
168    /// Pop string, apply `\U` / `\L` / `\u` / `\l` / `\Q` / `\E` case escapes, push result.
169    ProcessCaseEscapes,
170
171    // ── Comparison (numeric) ──
172    NumEq,
173    NumNe,
174    NumLt,
175    NumGt,
176    NumLe,
177    NumGe,
178    Spaceship,
179
180    // ── Comparison (string) ──
181    StrEq,
182    StrNe,
183    StrLt,
184    StrGt,
185    StrLe,
186    StrGe,
187    StrCmp,
188
189    // ── Logical / Bitwise ──
190    LogNot,
191    BitAnd,
192    BitOr,
193    BitXor,
194    BitNot,
195    Shl,
196    Shr,
197
198    // ── Control flow (absolute target addresses) ──
199    Jump(usize),
200    JumpIfTrue(usize),
201    JumpIfFalse(usize),
202    /// Jump if TOS is falsy WITHOUT popping (for short-circuit &&)
203    JumpIfFalseKeep(usize),
204    /// Jump if TOS is truthy WITHOUT popping (for short-circuit ||)
205    JumpIfTrueKeep(usize),
206    /// Jump if TOS is defined WITHOUT popping (for //)
207    JumpIfDefinedKeep(usize),
208
209    // ── Increment / Decrement ──
210    PreInc(u16),
211    PreDec(u16),
212    PostInc(u16),
213    PostDec(u16),
214    /// Pre-increment on a frame slot entry (compiled `my $x` fast path).
215    PreIncSlot(u8),
216    PreDecSlot(u8),
217    PostIncSlot(u8),
218    PostDecSlot(u8),
219
220    // ── Functions ──
221    /// Call subroutine: name index, arg count, `WantarrayCtx` discriminant as `u8`
222    Call(u16, u8, u8),
223    /// Like [`Op::Call`] but with a compile-time-resolved entry: `sid` indexes [`Chunk::static_sub_calls`]
224    /// (entry IP + stack-args); `name_idx` duplicates the stash pool index for closure restore / JIT
225    /// (same as in the table; kept in the opcode so JIT does not need the side table).
226    CallStaticSubId(u16, u16, u8, u8),
227    Return,
228    ReturnValue,
229    /// End of a compiled `map` / `grep` / `sort` block body (empty block or last statement an expression).
230    /// Pops the synthetic call frame from [`crate::vm::VM::run_block_region`] and unwinds the
231    /// block-local scope (`scope_push_hook` per iteration, like [`crate::interpreter::Interpreter::exec_block`]);
232    /// not subroutine `return` and not a closure capture.
233    BlockReturnValue,
234    /// At runtime statement position: capture current lexicals into [`crate::value::PerlSub::closure_env`]
235    /// for a sub already registered in [`Interpreter::subs`] (see `prepare_program_top_level`).
236    BindSubClosure(u16),
237
238    // ── Scope ──
239    PushFrame,
240    PopFrame,
241
242    // ── I/O ──
243    /// `print [HANDLE] LIST` — `None` uses [`crate::interpreter::Interpreter::default_print_handle`].
244    Print(Option<u16>, u8),
245    Say(Option<u16>, u8),
246
247    // ── Built-in function calls ──
248    /// Calls a registered built-in: (builtin_id, arg_count)
249    CallBuiltin(u16, u8),
250    /// Save [`crate::interpreter::Interpreter::wantarray_kind`] and set from `u8`
251    /// ([`crate::interpreter::WantarrayCtx::as_byte`]). Used for `splice` / similar where the
252    /// dynamic context must match the expression's compile-time [`WantarrayCtx`] (e.g. `print splice…`).
253    WantarrayPush(u8),
254    /// Restore after [`Op::WantarrayPush`].
255    WantarrayPop,
256
257    // ── List / Range ──
258    MakeArray(u16), // pop N values, push as Array
259    /// `@$href{k1,k2}` — stack: `[container, key1, …, keyN]` (TOS = last key); pops `N+1` values; pushes array of slot values.
260    HashSliceDeref(u16),
261    /// `@$aref[i1,i2,...]` — stack: `[array_ref, spec1, …, specN]` (TOS = last spec); each spec is a
262    /// scalar index or array of indices (list-context `..` / `qw`/list). Pops `N+1`; pushes elements.
263    ArrowArraySlice(u16),
264    /// `@$href{k1,k2} = VALUE` — stack: `[value, container, key1, …, keyN]` (TOS = last key); pops `N+2` values.
265    SetHashSliceDeref(u16),
266    /// `%name{k1,k2} = VALUE` — stack: `[value, key1, …, keyN]` (TOS = last key); pops `N+1`. Pool: hash name, key count.
267    SetHashSlice(u16, u16),
268    /// `@h{k1,k2}` read — stack: `[key1, …, keyN]` (TOS = last key); pops `N` values; pushes array of slot values.
269    /// Each key value may be a scalar or array (from list-context range); arrays are flattened into individual keys.
270    /// Pool: hash name index, key-expression count.
271    GetHashSlice(u16, u16),
272    /// `@$href{k1,k2} OP= VALUE` — stack: `[rhs, container, key1, …, keyN]` (TOS = last key); pops `N+2`, pushes the new value.
273    /// `u8` = [`crate::compiler::scalar_compound_op_to_byte`] encoding of the binop.
274    /// Perl 5 applies the op only to the **last** key’s element.
275    HashSliceDerefCompound(u8, u16),
276    /// `++@$href{k1,k2}` / `--...` / `@$href{k1,k2}++` / `...--` — stack: `[container, key1, …, keyN]`;
277    /// pops `N+1`. Pre-forms push the new last-element value; post-forms push the **old** last value.
278    /// `u8` encodes kind: 0=PreInc, 1=PreDec, 2=PostInc, 3=PostDec. Only the last key is updated.
279    HashSliceDerefIncDec(u8, u16),
280    /// `@name{k1,k2} OP= rhs` — stack: `[rhs, key1, …, keyN]` (TOS = last key); pops `N+1`, pushes the new value.
281    /// Pool: compound-op byte ([`crate::compiler::scalar_compound_op_to_byte`]), stash hash name, key-slot count.
282    /// Only the **last** flattened key is updated (same as [`Op::HashSliceDerefCompound`]).
283    NamedHashSliceCompound(u8, u16, u16),
284    /// `++@name{k1,k2}` / `--…` / `@name{k1,k2}++` / `…--` — stack: `[key1, …, keyN]`; pops `N`.
285    /// `u8` kind matches [`Op::HashSliceDerefIncDec`]. Only the last key is updated.
286    NamedHashSliceIncDec(u8, u16, u16),
287    /// Multi-key `@h{k1,k2} //=` / `||=` / `&&=` — stack `[key1, …, keyN]` unchanged; pushes the **last**
288    /// flattened slot (Perl only tests that slot). Pool: hash name, key-slot count.
289    NamedHashSlicePeekLast(u16, u16),
290    /// Stack `[key1, …, keyN, cur]` — pop `N` key slots, keep `cur` (short-circuit path).
291    NamedHashSliceDropKeysKeepCur(u16),
292    /// Assign list RHS’s last element to the **last** flattened key; stack `[val, key1, …, keyN]` (TOS = last key). Pushes `val`.
293    SetNamedHashSliceLastKeep(u16, u16),
294    /// Multi-key `@$href{k1,k2} //=` — stack `[container, key1, …, keyN]`; pushes last slice element (see [`Op::ArrowArraySlicePeekLast`]).
295    HashSliceDerefPeekLast(u16),
296    /// `[container, key1, …, keyN, val]` → `[val, container, key1, …, keyN]` for [`Op::HashSliceDerefSetLastKeep`].
297    HashSliceDerefRollValUnderKeys(u16),
298    /// Assign to last flattened key only; stack `[val, container, key1, …, keyN]`. Pushes `val`.
299    HashSliceDerefSetLastKeep(u16),
300    /// Stack `[container, key1, …, keyN, cur]` — drop container and keys; keep `cur`.
301    HashSliceDerefDropKeysKeepCur(u16),
302    /// `@$aref[i1,i2,...] = LIST` — stack: `[value, aref, spec1, …, specN]` (TOS = last spec);
303    /// pops `N+2`. Delegates to [`crate::interpreter::Interpreter::assign_arrow_array_slice`].
304    SetArrowArraySlice(u16),
305    /// `@$aref[i1,i2,...] OP= rhs` — stack: `[rhs, aref, spec1, …, specN]`; pops `N+2`, pushes new value.
306    /// `u8` = [`crate::compiler::scalar_compound_op_to_byte`] encoding of the binop.
307    /// Perl 5 applies the op only to the **last** index. Delegates to [`crate::interpreter::Interpreter::compound_assign_arrow_array_slice`].
308    ArrowArraySliceCompound(u8, u16),
309    /// `++@$aref[i1,i2,...]` / `--...` / `...++` / `...--` — stack: `[aref, spec1, …, specN]`;
310    /// pops `N+1`. Pre-forms push the new last-element value; post-forms push the old last value.
311    /// `u8` kind matches [`Op::HashSliceDerefIncDec`]. Only the last index is updated. Delegates to
312    /// [`crate::interpreter::Interpreter::arrow_array_slice_inc_dec`].
313    ArrowArraySliceIncDec(u8, u16),
314    /// Read the element at the **last** flattened index of `@$aref[spec1,…]` without popping `aref`
315    /// or specs. Stack: `[aref, spec1, …, specN]` (TOS = last spec) → same plus pushed scalar.
316    /// Used for `@$r[i,j] //=` / `||=` / `&&=` short-circuit tests (Perl only tests the last slot).
317    ArrowArraySlicePeekLast(u16),
318    /// Stack: `[aref, spec1, …, specN, cur]` — pop slice keys and container, keep `cur` (short-circuit
319    /// result). `u16` = number of spec slots (same as [`Op::ArrowArraySlice`]).
320    ArrowArraySliceDropKeysKeepCur(u16),
321    /// Reorder `[aref, spec1, …, specN, val]` → `[val, aref, spec1, …, specN]` for
322    /// [`Op::SetArrowArraySliceLastKeep`].
323    ArrowArraySliceRollValUnderSpecs(u16),
324    /// Assign `val` to the **last** flattened index only; stack `[val, aref, spec1, …, specN]`
325    /// (TOS = last spec). Pushes `val` (like [`Op::SetArrowArrayKeep`]).
326    SetArrowArraySliceLastKeep(u16),
327    /// Like [`Op::ArrowArraySliceIncDec`] but for a **named** stash array (`@a[i1,i2,...]`).
328    /// Stack: `[spec1, …, specN]` (TOS = last spec). `u16` = name pool index (stash-qualified).
329    /// Delegates to [`crate::interpreter::Interpreter::named_array_slice_inc_dec`].
330    NamedArraySliceIncDec(u8, u16, u16),
331    /// `@name[spec1,…] OP= rhs` — stack `[rhs, spec1, …, specN]` (TOS = last spec); pops `N+1`.
332    /// Only the **last** flattened index is updated (same as [`Op::ArrowArraySliceCompound`]).
333    NamedArraySliceCompound(u8, u16, u16),
334    /// Read the **last** flattened slot of `@name[spec1,…]` without popping specs. Stack:
335    /// `[spec1, …, specN]` → same plus pushed scalar. `u16` pairs: name pool index, spec count.
336    NamedArraySlicePeekLast(u16, u16),
337    /// Stack: `[spec1, …, specN, cur]` — pop specs, keep `cur` (short-circuit). `u16` = spec count.
338    NamedArraySliceDropKeysKeepCur(u16),
339    /// `[spec1, …, specN, val]` → `[val, spec1, …, specN]` for [`Op::SetNamedArraySliceLastKeep`].
340    NamedArraySliceRollValUnderSpecs(u16),
341    /// Assign to the **last** index only; stack `[val, spec1, …, specN]`. Pushes `val`.
342    SetNamedArraySliceLastKeep(u16, u16),
343    /// `@name[spec1,…] = LIST` — stack `[value, spec1, …, specN]` (TOS = last spec); pops `N+1`.
344    /// Element-wise like [`Op::SetArrowArraySlice`]. Pool indices: stash-qualified array name, spec count.
345    SetNamedArraySlice(u16, u16),
346    /// `BAREWORD` as an rvalue — at run time, look up a subroutine with this name; if found,
347    /// call it with no args (nullary), otherwise push the name as a string (Perl's bareword-as-
348    /// stringifies behavior). `u16` is a name-pool index. Delegates to
349    /// [`crate::interpreter::Interpreter::resolve_bareword_rvalue`].
350    BarewordRvalue(u16),
351    /// Throw `PerlError::runtime` with the message at constant pool index `u16`. Used by the compiler
352    /// to hard-reject constructs whose only valid response is a runtime error
353    /// (e.g. `++@$r`, `%{...}--`) without AST fallback.
354    RuntimeErrorConst(u16),
355    MakeHash(u16), // pop N key-value pairs, push as Hash
356    Range,         // stack: [from, to] → Array
357    RangeStep,     // stack: [from, to, step] → Array (stepped range)
358    /// Array slice via colon range — `@arr[FROM:TO:STEP]` / `@arr[::-1]`.
359    /// Stack: `[from, to, step]` — each may be `Undef` to mean "omitted" (uses array bounds).
360    /// `u16` is the array name pool index. Endpoints must coerce to integer cleanly; otherwise
361    /// runtime aborts (`die "slice: non-integer endpoint in array slice"`). Pushes the sliced array.
362    ArraySliceRange(u16),
363    /// Hash slice via colon range — `@h{FROM:TO:STEP}` (keys auto-quote like fat comma `=>`).
364    /// Stack: `[from, to, step]` — open ends die (no notion of "all keys" in unordered hash).
365    /// Endpoints stringify to hash keys; expansion uses numeric or magic-string-increment
366    /// depending on whether both ends parse as numbers. `u16` is the hash name pool index.
367    /// Pushes the array of slot values for the expanded keys.
368    HashSliceRange(u16),
369    /// Scalar `..` / `...` flip-flop (numeric bounds vs `$.` — [`Interpreter::scalar_flipflop_dot_line`]).
370    /// Stack: `[from, to]` (ints); pushes `1` or `0`. `u16` indexes flip-flop slots; `u8` is `1` for `...`
371    /// (exclusive: right bound only after `$.` is strictly past the line where the left bound matched).
372    ScalarFlipFlop(u16, u8),
373    /// Regex `..` / `...` flip-flop: both bounds are pattern literals; tests use `$_` and `$.` like Perl
374    /// (`Interpreter::regex_flip_flop_eval`). Operand order: `slot`, `exclusive`, left pattern, left flags,
375    /// right pattern, right flags (constant pool indices). No stack operands; pushes `0`/`1`.
376    RegexFlipFlop(u16, u8, u16, u16, u16, u16),
377    /// Regex `..` / `...` flip-flop with `eof` as the right operand (no arguments). Left bound matches `$_`;
378    /// right bound is [`Interpreter::eof_without_arg_is_true`] (Perl `eof` in `-n`/`-p`). Operand order:
379    /// `slot`, `exclusive`, left pattern, left flags.
380    RegexEofFlipFlop(u16, u8, u16, u16),
381    /// Regex `..` / `...` with a non-literal right operand (e.g. `m/a/ ... (m/b/ or m/c/)`). Left bound is
382    /// pattern + flags; right is evaluated in boolean context each line (pool index into
383    /// [`Chunk::regex_flip_flop_rhs_expr_entries`] / bytecode ranges). Operand order: `slot`, `exclusive`,
384    /// left pattern, left flags, rhs expr index.
385    RegexFlipFlopExprRhs(u16, u8, u16, u16, u16),
386    /// Regex `..` / `...` with a numeric right operand (Perl: right bound is [`Interpreter::scalar_flipflop_dot_line`]
387    /// vs literal line). Constant pool index holds the RHS line as [`PerlValue::integer`]. Operand order:
388    /// `slot`, `exclusive`, left pattern, left flags, rhs line constant index.
389    RegexFlipFlopDotLineRhs(u16, u8, u16, u16, u16),
390
391    // ── Regex ──
392    /// Match: pattern_const_idx, flags_const_idx, scalar_g, pos_key_name_idx (`u16::MAX` = `$_`);
393    /// stack: string operand → result
394    RegexMatch(u16, u16, bool, u16),
395    /// Substitution `s///`: pattern, replacement, flags constant indices; lvalue index into chunk.
396    /// stack: string (subject from LHS expr) → replacement count
397    RegexSubst(u16, u16, u16, u16),
398    /// Transliterate `tr///`: from, to, flags constant indices; lvalue index into chunk.
399    /// stack: string → transliteration count
400    RegexTransliterate(u16, u16, u16, u16),
401    /// Dynamic `=~` / `!~`: pattern from RHS, subject from LHS; empty flags.
402    /// stack: `[subject, pattern]` (pattern on top) → 0/1; `true` = negate (`!~`).
403    RegexMatchDyn(bool),
404    /// Regex literal as a value (`qr/PAT/FLAGS`) — pattern and flags string pool indices.
405    LoadRegex(u16, u16),
406    /// After [`RegexMatchDyn`] for bare `m//` in `&&` / `||`: pop 0/1; push `""` or `1` (Perl scalar).
407    RegexBoolToScalar,
408    /// `pos $var = EXPR` / `pos = EXPR` (implicit `$_`). Stack: `[value, key]` (key string on top).
409    SetRegexPos,
410
411    // ── Assign helpers ──
412    /// SetScalar that also leaves the value on the stack (for chained assignment)
413    SetScalarKeep(u16),
414    /// `SetScalarKeep` for non-special scalars (see `SetScalarPlain`).
415    SetScalarKeepPlain(u16),
416
417    // ── Block-based operations (u16 = index into chunk.blocks) ──
418    /// map { BLOCK } @list — block_idx; stack: \[list\] → \[mapped\]
419    MapWithBlock(u16),
420    /// flat_map { BLOCK } @list — like [`Op::MapWithBlock`] but peels one ARRAY ref per iteration ([`PerlValue::map_flatten_outputs`])
421    FlatMapWithBlock(u16),
422    /// grep { BLOCK } @list — block_idx; stack: \[list\] → \[filtered\]
423    GrepWithBlock(u16),
424    /// each { BLOCK } @list — block_idx; stack: \[list\] → \[count\]
425    ForEachWithBlock(u16),
426    /// map EXPR, LIST — index into [`Chunk::map_expr_entries`] / [`Chunk::map_expr_bytecode_ranges`];
427    /// stack: \[list\] → \[mapped\]
428    MapWithExpr(u16),
429    /// flat_map EXPR, LIST — same pools as [`Op::MapWithExpr`]; stack: \[list\] → \[mapped\]
430    FlatMapWithExpr(u16),
431    /// grep EXPR, LIST — index into [`Chunk::grep_expr_entries`] / [`Chunk::grep_expr_bytecode_ranges`];
432    /// stack: \[list\] → \[filtered\]
433    GrepWithExpr(u16),
434    /// `group_by { BLOCK } LIST` / `chunk_by { BLOCK } LIST` — consecutive runs where the block’s
435    /// return value stringifies the same as the previous (`str_eq`); stack: \[list\] → \[arrayrefs\]
436    ChunkByWithBlock(u16),
437    /// `group_by EXPR, LIST` / `chunk_by EXPR, LIST` — same as [`Op::ChunkByWithBlock`] but key from
438    /// `EXPR` with `$_` set each iteration; uses [`Chunk::map_expr_entries`].
439    ChunkByWithExpr(u16),
440    /// sort { BLOCK } @list — block_idx; stack: \[list\] → \[sorted\]
441    SortWithBlock(u16),
442    /// sort @list (no block) — stack: \[list\] → \[sorted\]
443    SortNoBlock,
444    /// sort $coderef LIST — stack: \[list, coderef\] (coderef on top); `u8` = wantarray for comparator calls.
445    SortWithCodeComparator(u8),
446    /// `{ $a <=> $b }` (0), `{ $a cmp $b }` (1), `{ $b <=> $a }` (2), `{ $b cmp $a }` (3)
447    SortWithBlockFast(u8),
448    /// `map { $_ * k }` with integer `k` — stack: \[list\] → \[mapped\]
449    MapIntMul(i64),
450    /// `grep { $_ % m == r }` with integer `m` (non-zero), `r` — stack: \[list\] → \[filtered\]
451    GrepIntModEq(i64, i64),
452    /// Parallel sort, same fast modes as [`Op::SortWithBlockFast`].
453    PSortWithBlockFast(u8),
454    /// `read(FH, $buf, LEN [, OFFSET])` — reads into a named variable.
455    /// Stack: [filehandle, length] (offset optional via `ReadIntoVarOffset`).
456    /// Writes result into `$name[u16]`, pushes bytes-read count (or undef on error).
457    ReadIntoVar(u16),
458    /// `chomp` on assignable expr: stack has value → chomped count; uses `chunk.lvalues[idx]`.
459    ChompInPlace(u16),
460    /// `chop` on assignable expr: stack has value → chopped char; uses `chunk.lvalues[idx]`.
461    ChopInPlace(u16),
462    /// Four-arg `substr LHS, OFF, LEN, REPL` — index into [`Chunk::substr_four_arg_entries`]; stack: \[\] → extracted slice string
463    SubstrFourArg(u16),
464    /// `keys EXPR` when `EXPR` is not a bare `%h` — [`Chunk::keys_expr_entries`] /
465    /// [`Chunk::keys_expr_bytecode_ranges`]
466    KeysExpr(u16),
467    /// `values EXPR` when not a bare `%h` — [`Chunk::values_expr_entries`] /
468    /// [`Chunk::values_expr_bytecode_ranges`]
469    ValuesExpr(u16),
470    /// Scalar `keys EXPR` (dynamic) — same pools as [`Op::KeysExpr`].
471    KeysExprScalar(u16),
472    /// Scalar `values EXPR` — same pools as [`Op::ValuesExpr`].
473    ValuesExprScalar(u16),
474    /// `delete EXPR` when not a fast `%h{...}` — index into [`Chunk::delete_expr_entries`]
475    DeleteExpr(u16),
476    /// `exists EXPR` when not a fast `%h{...}` — index into [`Chunk::exists_expr_entries`]
477    ExistsExpr(u16),
478    /// `push EXPR, ...` when not a bare `@name` — [`Chunk::push_expr_entries`]
479    PushExpr(u16),
480    /// `pop EXPR` when not a bare `@name` — [`Chunk::pop_expr_entries`]
481    PopExpr(u16),
482    /// `shift EXPR` when not a bare `@name` — [`Chunk::shift_expr_entries`]
483    ShiftExpr(u16),
484    /// `unshift EXPR, ...` when not a bare `@name` — [`Chunk::unshift_expr_entries`]
485    UnshiftExpr(u16),
486    /// `splice EXPR, ...` when not a bare `@name` — [`Chunk::splice_expr_entries`]
487    SpliceExpr(u16),
488    /// `$var .= expr` — append to scalar string in-place without cloning.
489    /// Stack: \[value_to_append\] → \[resulting_string\]. u16 = name pool index of target scalar.
490    ConcatAppend(u16),
491    /// Slot-indexed `$var .= expr` — avoids frame walking and string comparison.
492    /// Stack: \[value_to_append\] → \[resulting_string\]. u8 = slot index.
493    ConcatAppendSlot(u8),
494    /// Fused `$slot_a += $slot_b` — no stack traffic. Pushes result.
495    AddAssignSlotSlot(u8, u8),
496    /// Fused `$slot_a -= $slot_b` — no stack traffic. Pushes result.
497    SubAssignSlotSlot(u8, u8),
498    /// Fused `$slot_a *= $slot_b` — no stack traffic. Pushes result.
499    MulAssignSlotSlot(u8, u8),
500    /// Fused `if ($slot < INT) goto target` — replaces GetScalarSlot + LoadInt + NumLt + JumpIfFalse.
501    /// (slot, i32_limit, jump_target)
502    SlotLtIntJumpIfFalse(u8, i32, usize),
503    /// Void-context `$slot_a += $slot_b` — no stack push. Replaces AddAssignSlotSlot + Pop.
504    AddAssignSlotSlotVoid(u8, u8),
505    /// Void-context `++$slot` — no stack push. Replaces PreIncSlot + Pop.
506    PreIncSlotVoid(u8),
507    /// Void-context `$slot .= expr` — no stack push. Replaces ConcatAppendSlot + Pop.
508    ConcatAppendSlotVoid(u8),
509    /// Fused loop backedge: `$slot += 1; if $slot < limit jump body_target; else fall through`.
510    ///
511    /// Replaces the trailing `PreIncSlotVoid(s) + Jump(top)` of a C-style `for (my $i=0; $i<N; $i=$i+1)`
512    /// loop whose top op is a `SlotLtIntJumpIfFalse(s, limit, exit)`. The initial iteration still
513    /// goes through the top check; this op handles all subsequent iterations in a single dispatch,
514    /// halving the number of ops per loop trip for the `bench_loop`/`bench_string`/`bench_array` shape.
515    /// (slot, i32_limit, body_target)
516    SlotIncLtIntJumpBack(u8, i32, usize),
517    /// Fused accumulator loop: `while $i < limit { $sum += $i; $i += 1 }` — runs the entire
518    /// remaining counted-sum loop in native Rust, eliminating op dispatch per iteration.
519    ///
520    /// Fused when a `for (my $i = a; $i < N; $i = $i + 1) { $sum += $i }` body compiles down to
521    /// exactly `AddAssignSlotSlotVoid(sum, i) + SlotIncLtIntJumpBack(i, limit, body_target)` with
522    /// `body_target` pointing at the AddAssign — i.e. the body is 1 Perl statement. Both slots are
523    /// left as integers on exit (same coercion as `AddAssignSlotSlotVoid` + `PreIncSlotVoid`).
524    /// (sum_slot, i_slot, i32_limit)
525    AccumSumLoop(u8, u8, i32),
526    /// Fused string-append counted loop: `while $i < limit { $s .= CONST; $i += 1 }` — extends
527    /// the `String` buffer in place once and pushes the literal `(limit - i)` times in a tight
528    /// Rust loop, with `Arc::get_mut` → `reserve` → `push_str`. Falls back to the regular op
529    /// sequence if the slot is not a uniquely-owned heap `String`.
530    ///
531    /// Fused when the loop body is exactly `LoadConst(c) + ConcatAppendSlotVoid(s) +
532    /// SlotIncLtIntJumpBack(i, limit, body_target)` with `body_target` pointing at the `LoadConst`.
533    /// (const_idx, s_slot, i_slot, i32_limit)
534    ConcatConstSlotLoop(u16, u8, u8, i32),
535    /// Fused array-push counted loop: `while $i < limit { push @a, $i; $i += 1 }` — reserves the
536    /// target `Vec` once and pushes `PerlValue::integer(i)` in a tight Rust loop. Emitted when
537    /// the loop body is exactly `GetScalarSlot(i) + PushArray(arr) + ArrayLen(arr) + Pop +
538    /// SlotIncLtIntJumpBack(i, limit, body_target)` with `body_target` pointing at the
539    /// `GetScalarSlot` (i.e. the body is one `push` statement whose return is discarded).
540    /// (arr_name_idx, i_slot, i32_limit)
541    PushIntRangeToArrayLoop(u16, u8, i32),
542    /// Fused hash-insert counted loop: `while $i < limit { $h{$i} = $i * k; $i += 1 }` — runs the
543    /// entire insert loop natively, reserving hash capacity once and writing `(stringified i, i*k)`
544    /// pairs in tight Rust. Emitted when the body is exactly
545    /// `GetScalarSlot(i) + LoadInt(k) + Mul + GetScalarSlot(i) + SetHashElem(h) + Pop +
546    /// SlotIncLtIntJumpBack(i, limit, body_target)` with `body_target` at the first `GetScalarSlot`.
547    /// (hash_name_idx, i_slot, i32_multiplier, i32_limit)
548    SetHashIntTimesLoop(u16, u8, i32, i32),
549    /// Fused `$sum += $h{$k}` body op for the inner loop of `for my $k (keys %h) { $sum += $h{$k} }`.
550    ///
551    /// Replaces the 6-op sequence `GetScalarSlot(sum) + GetScalarPlain(k) + GetHashElem(h) + Add +
552    /// SetScalarSlotKeep(sum) + Pop` with a single dispatch that reads the hash element directly
553    /// into the slot without going through the VM stack. (sum_slot, k_name_idx, h_name_idx)
554    AddHashElemPlainKeyToSlot(u8, u16, u16),
555    /// Like [`Op::AddHashElemPlainKeyToSlot`] but the key variable lives in a slot (`for my $k`
556    /// in slot-mode foreach). Pure slot read + hash lookup + slot write with zero VM stack traffic.
557    /// (sum_slot, k_slot, h_name_idx)
558    AddHashElemSlotKeyToSlot(u8, u8, u16),
559    /// Fused `for my $k (keys %h) { $sum += $h{$k} }` — walks `hash.values()` in a tight native
560    /// loop, accumulating integer or float sums directly into `sum_slot`. Emitted by the
561    /// bytecode-level peephole when the foreach shape + `AddHashElemSlotKeyToSlot` body + slot
562    /// counter/var declarations are detected. `h_name_idx` is the source hash's name pool index.
563    /// (sum_slot, h_name_idx)
564    SumHashValuesToSlot(u8, u16),
565
566    // ── Frame-local scalar slots (O(1) access, no string lookup) ──
567    /// Read scalar from current frame's slot array. u8 = slot index.
568    GetScalarSlot(u8),
569    /// Write scalar to current frame's slot array (pop, discard). u8 = slot index.
570    SetScalarSlot(u8),
571    /// Write scalar to current frame's slot array (pop, keep on stack). u8 = slot index.
572    SetScalarSlotKeep(u8),
573    /// Declare + initialize scalar in current frame's slot array. u8 = slot index; u16 = name pool
574    /// index (bare name) for closure capture.
575    DeclareScalarSlot(u8, u16),
576    /// Read argument from caller's stack region: push stack\[call_frame.stack_base + idx\].
577    /// Avoids @_ allocation + string-based shift for compiled sub argument passing.
578    GetArg(u8),
579    /// `reverse` in list context — stack: \[list\] → \[reversed list\]
580    ReverseListOp,
581    /// `scalar reverse` — stack: \[list\] → concatenated string with chars reversed (Perl).
582    ReverseScalarOp,
583    /// `rev` in list context — reverse list, preserve iterators lazily.
584    RevListOp,
585    /// `rev` in scalar context — char-reverse string.
586    RevScalarOp,
587    /// Pop TOS (array/list), push `to_list().len()` as integer (Perl `scalar` on map/grep result).
588    StackArrayLen,
589    /// Pop list-slice result array; push last element (Perl `scalar (LIST)[i,...]`).
590    ListSliceToScalar,
591    /// pmap { BLOCK } @list — block_idx; stack: \[progress_flag, list\] → \[mapped\] (`progress_flag` is 0/1)
592    PMapWithBlock(u16),
593    /// pflat_map { BLOCK } @list — flatten array results; output in **input order**; stack same as [`Op::PMapWithBlock`]
594    PFlatMapWithBlock(u16),
595    /// pmaps { BLOCK } LIST — streaming parallel map; stack: \[list\] → \[iterator\]
596    PMapsWithBlock(u16),
597    /// pflat_maps { BLOCK } LIST — streaming parallel flat map; stack: \[list\] → \[iterator\]
598    PFlatMapsWithBlock(u16),
599    /// `pmap_on` / `pflat_map_on` over SSH — stack: \[progress_flag, list, cluster\] → \[mapped\]; `flat` = 1 for flatten
600    PMapRemote {
601        block_idx: u16,
602        flat: u8,
603    },
604    /// puniq LIST — hash-partition parallel distinct (first occurrence order); stack: \[progress_flag, list\] → \[array\]
605    Puniq,
606    /// pfirst { BLOCK } LIST — short-circuit parallel; stack: \[progress_flag, list\] → value or undef
607    PFirstWithBlock(u16),
608    /// pany { BLOCK } LIST — short-circuit parallel; stack: \[progress_flag, list\] → 0/1
609    PAnyWithBlock(u16),
610    /// pmap_chunked N { BLOCK } @list — block_idx; stack: \[progress_flag, chunk_n, list\] → \[mapped\]
611    PMapChunkedWithBlock(u16),
612    /// pgrep { BLOCK } @list — block_idx; stack: \[progress_flag, list\] → \[filtered\]
613    PGrepWithBlock(u16),
614    /// pgreps { BLOCK } LIST — streaming parallel grep; stack: \[list\] → \[iterator\]
615    PGrepsWithBlock(u16),
616    /// pfor { BLOCK } @list — block_idx; stack: \[progress_flag, list\] → \[\]
617    PForWithBlock(u16),
618    /// psort { BLOCK } @list — block_idx; stack: \[progress_flag, list\] → \[sorted\]
619    PSortWithBlock(u16),
620    /// psort @list (no block) — stack: \[progress_flag, list\] → \[sorted\]
621    PSortNoBlockParallel,
622    /// `reduce { BLOCK } @list` — block_idx; stack: \[list\] → \[accumulator\]
623    ReduceWithBlock(u16),
624    /// `preduce { BLOCK } @list` — block_idx; stack: \[progress_flag, list\] → \[accumulator\]
625    PReduceWithBlock(u16),
626    /// `preduce_init EXPR, { BLOCK } @list` — block_idx; stack: \[progress_flag, list, init\] → \[accumulator\]
627    PReduceInitWithBlock(u16),
628    /// `pmap_reduce { MAP } { REDUCE } @list` — map and reduce block indices; stack: \[progress_flag, list\] → \[scalar\]
629    PMapReduceWithBlocks(u16, u16),
630    /// `pcache { BLOCK } @list` — block_idx; stack: \[progress_flag, list\] → \[array\]
631    PcacheWithBlock(u16),
632    /// `pselect($rx1, ... [, timeout => SECS])` — stack: \[rx0, …, rx_{n-1}\] with optional timeout on top
633    Pselect {
634        n_rx: u8,
635        has_timeout: bool,
636    },
637    /// `par_lines PATH, fn { } [, progress => EXPR]` — index into [`Chunk::par_lines_entries`]; stack: \[\] → `undef`
638    ParLines(u16),
639    /// `par_walk PATH, fn { } [, progress => EXPR]` — index into [`Chunk::par_walk_entries`]; stack: \[\] → `undef`
640    ParWalk(u16),
641    /// `pwatch GLOB, fn { }` — index into [`Chunk::pwatch_entries`]; stack: \[\] → result
642    Pwatch(u16),
643    /// fan N { BLOCK } — block_idx; stack: \[progress_flag, count\] (`progress_flag` is 0/1)
644    FanWithBlock(u16),
645    /// fan { BLOCK } — block_idx; stack: \[progress_flag\]; COUNT = rayon pool size (`stryke -j`)
646    FanWithBlockAuto(u16),
647    /// fan_cap N { BLOCK } — like fan; stack: \[progress_flag, count\] → array of block return values
648    FanCapWithBlock(u16),
649    /// fan_cap { BLOCK } — like fan; stack: \[progress_flag\] → array
650    FanCapWithBlockAuto(u16),
651    /// `do { BLOCK }` — block_idx + wantarray byte ([`crate::interpreter::WantarrayCtx::as_byte`]);
652    /// stack: \[\] → result
653    EvalBlock(u16, u8),
654    /// `trace { BLOCK }` — block_idx; stack: \[\] → block value (stderr tracing for mysync mutations)
655    TraceBlock(u16),
656    /// `timer { BLOCK }` — block_idx; stack: \[\] → elapsed ms as float
657    TimerBlock(u16),
658    /// `bench { BLOCK } N` — block_idx; stack: \[iterations\] → benchmark summary string
659    BenchBlock(u16),
660    /// `given (EXPR) { when ... default ... }` — [`Chunk::given_entries`] /
661    /// [`Chunk::given_topic_bytecode_ranges`]; stack: \[\] → topic result
662    Given(u16),
663    /// `eval_timeout SECS { ... }` — index into [`Chunk::eval_timeout_entries`] /
664    /// [`Chunk::eval_timeout_expr_bytecode_ranges`]; stack: \[\] → block value
665    EvalTimeout(u16),
666    /// Algebraic `match (SUBJECT) { ... }` — [`Chunk::algebraic_match_entries`] /
667    /// [`Chunk::algebraic_match_subject_bytecode_ranges`]; stack: \[\] → arm value
668    AlgebraicMatch(u16),
669    /// `async { BLOCK }` / `spawn { BLOCK }` — block_idx; stack: \[\] → AsyncTask
670    AsyncBlock(u16),
671    /// `await EXPR` — stack: \[value\] → result
672    Await,
673    /// `__SUB__` — push reference to currently executing sub (for anonymous recursion).
674    LoadCurrentSub,
675    /// `defer { BLOCK }` — register a block to run when the current scope exits.
676    /// Stack: `[coderef]` → `[]`. The coderef is pushed to the frame's defer list.
677    DeferBlock,
678    /// Make a scalar reference from TOS (copies value into a new `RwLock`).
679    MakeScalarRef,
680    /// `\$name` when `name` is a plain scalar variable — ref aliases the live binding (same as tree `scalar_binding_ref`).
681    MakeScalarBindingRef(u16),
682    /// `\@name` — ref aliases the live array in scope (name pool index, stash-qualified like [`Op::GetArray`]).
683    MakeArrayBindingRef(u16),
684    /// `\%name` — ref aliases the live hash in scope.
685    MakeHashBindingRef(u16),
686    /// `\@{ EXPR }` after `EXPR` is on the stack — ARRAY ref aliasing the same storage as Perl (ref to existing ref or package array).
687    MakeArrayRefAlias,
688    /// `\%{ EXPR }` — HASH ref alias (same semantics as [`Op::MakeArrayRefAlias`] for hashes).
689    MakeHashRefAlias,
690    /// Make an array reference from TOS (which should be an Array)
691    MakeArrayRef,
692    /// Make a hash reference from TOS (which should be a Hash)
693    MakeHashRef,
694    /// Make an anonymous sub from a block — block_idx; stack: \[\] → CodeRef
695    /// Anonymous `sub` / coderef: block pool index + [`Chunk::code_ref_sigs`] index (may be empty vec).
696    MakeCodeRef(u16, u16),
697    /// Push a code reference to a named sub (`\&foo`) — name pool index; resolves at run time.
698    LoadNamedSubRef(u16),
699    /// `\&{ EXPR }` — stack: \[sub name string\] → code ref (resolves at run time).
700    LoadDynamicSubRef,
701    /// `*{ EXPR }` — stack: \[stash / glob name string\] → resolved handle string (IO alias map + identity).
702    LoadDynamicTypeglob,
703    /// `*lhs = *rhs` — copy stash slots (sub, scalar, array, hash, IO alias); name pool indices for both sides.
704    CopyTypeglobSlots(u16, u16),
705    /// `*name = $coderef` — stack: pop value, install subroutine in typeglob, push value back (assignment result).
706    TypeglobAssignFromValue(u16),
707    /// `*{LHS} = $coderef` — stack: pop value, pop LHS glob name string, install sub, push value back.
708    TypeglobAssignFromValueDynamic,
709    /// `*{LHS} = *rhs` — stack: pop LHS glob name string; RHS name is pool index; copies stash like [`Op::CopyTypeglobSlots`].
710    CopyTypeglobSlotsDynamicLhs(u16),
711    /// Symbolic deref (`$$r`, `@{...}`, `%{...}`, `*{...}`): stack: \[ref or name value\] → result.
712    /// Byte: `0` = [`crate::ast::Sigil::Scalar`], `1` = Array, `2` = Hash, `3` = Typeglob.
713    SymbolicDeref(u8),
714    /// Dereference arrow: ->\[\] — stack: \[ref, index\] → value
715    ArrowArray,
716    /// Dereference arrow: ->{} — stack: \[ref, key\] → value
717    ArrowHash,
718    /// Assign to `->{}`: stack: \[value, ref, key\] (key on top) — consumes three values.
719    SetArrowHash,
720    /// Assign to `->[]`: stack: \[value, ref, index\] (index on top) — consumes three values.
721    SetArrowArray,
722    /// Like [`Op::SetArrowArray`] but leaves the assigned value on the stack (for `++$aref->[$i]` value).
723    SetArrowArrayKeep,
724    /// Like [`Op::SetArrowHash`] but leaves the assigned value on the stack (for `++$href->{k}` value).
725    SetArrowHashKeep,
726    /// Postfix `++` / `--` on `->[]`: stack \[ref, index\] (index on top) → old value; mutates slot.
727    /// Byte: `0` = increment, `1` = decrement.
728    ArrowArrayPostfix(u8),
729    /// Postfix `++` / `--` on `->{}`: stack \[ref, key\] (key on top) → old value; mutates slot.
730    /// Byte: `0` = increment, `1` = decrement.
731    ArrowHashPostfix(u8),
732    /// `$$r = $val` — stack: \[value, ref\] (ref on top).
733    SetSymbolicScalarRef,
734    /// Like [`Op::SetSymbolicScalarRef`] but leaves the assigned value on the stack.
735    SetSymbolicScalarRefKeep,
736    /// `@{ EXPR } = LIST` — stack: \[list value, ref-or-name\] (top = ref / package name); delegates to
737    /// [`Interpreter::assign_symbolic_array_ref_deref`](crate::interpreter::Interpreter::assign_symbolic_array_ref_deref).
738    SetSymbolicArrayRef,
739    /// `%{ EXPR } = LIST` — stack: \[list value, ref-or-name\]; pairs from list like `%h = (k => v, …)`.
740    SetSymbolicHashRef,
741    /// `*{ EXPR } = RHS` — stack: \[value, ref-or-name\] (top = symbolic glob name); coderef install or `*lhs = *rhs` copy.
742    SetSymbolicTypeglobRef,
743    /// Postfix `++` / `--` on symbolic scalar ref (`$$r`); stack \[ref\] → old value. Byte: `0` = increment, `1` = decrement.
744    SymbolicScalarRefPostfix(u8),
745    /// Dereference arrow: ->() — stack: \[ref, args_array\] → value
746    /// `$cr->(...)` — wantarray byte (see VM `WantarrayCtx` threading on `Call` / `MethodCall`).
747    ArrowCall(u8),
748    /// Indirect call `$coderef(ARG...)` / `&$coderef(ARG...)` — stack (bottom→top): `target`, then
749    /// `argc` argument values (first arg pushed first). Third byte: `1` = ignore stack args and use
750    /// caller `@_` (`argc` must be `0`).
751    IndirectCall(u8, u8, u8),
752    /// Method call: stack: \[object, args...\] → result; name_idx, argc, wantarray
753    MethodCall(u16, u8, u8),
754    /// Like [`Op::MethodCall`] but uses SUPER / C3 parent chain (see interpreter method resolution for `SUPER`).
755    MethodCallSuper(u16, u8, u8),
756    /// File test: -e, -f, -d, etc. — test char; stack: \[path\] → 0/1
757    FileTestOp(u8),
758
759    // ── try / catch / finally (VM exception handling; see [`VM::try_recover_from_exception`]) ──
760    /// Push a [`crate::vm::TryFrame`]; `catch_ip` / `after_ip` patched via [`Chunk::patch_try_push_catch`]
761    /// / [`Chunk::patch_try_push_after`]; `finally_ip` via [`Chunk::patch_try_push_finally`].
762    TryPush {
763        catch_ip: usize,
764        finally_ip: Option<usize>,
765        after_ip: usize,
766        catch_var_idx: u16,
767    },
768    /// Normal completion from try or catch body (jump to finally or merge).
769    TryContinueNormal,
770    /// End of `finally` block: pop try frame and jump to `after_ip`.
771    TryFinallyEnd,
772    /// Enter catch: consume [`crate::vm::VM::pending_catch_error`], pop try scope, push catch scope, bind `$var`.
773    CatchReceive(u16),
774
775    // ── `mysync` (thread-safe shared bindings; see [`StmtKind::MySync`]) ──
776    /// Stack: `[init]` → `[]`. Declares `${name}` as `PerlValue::atomic` (or deque/heap unwrapped).
777    DeclareMySyncScalar(u16),
778    /// Stack: `[init_list]` → `[]`. Declares `@name` as atomic array.
779    DeclareMySyncArray(u16),
780    /// Stack: `[init_list]` → `[]`. Declares `%name` as atomic hash.
781    DeclareMySyncHash(u16),
782    /// Register [`RuntimeSubDecl`] at index (nested `sub`, including inside `BEGIN`).
783    RuntimeSubDecl(u16),
784    /// `tie $x | @arr | %h, 'Class', ...` — stack bottom = class expr, then user args; `argc` = `1 + args.len()`.
785    /// `target_kind`: 0 = scalar (`TIESCALAR`), 1 = array (`TIEARRAY`), 2 = hash (`TIEHASH`). `name_idx` = bare name.
786    Tie {
787        target_kind: u8,
788        name_idx: u16,
789        argc: u8,
790    },
791    /// `format NAME =` … — index into [`Chunk::format_decls`]; installs into current package at run time.
792    FormatDecl(u16),
793    /// `use overload 'op' => 'method', …` — index into [`Chunk::use_overload_entries`].
794    UseOverload(u16),
795    /// Scalar `$x OP= $rhs` — uses [`Scope::atomic_mutate`] so `mysync` scalars are RMW-safe.
796    /// Stack: `[rhs]` → `[result]`. `op` byte is from [`crate::compiler::scalar_compound_op_to_byte`].
797    ScalarCompoundAssign {
798        name_idx: u16,
799        op: u8,
800    },
801
802    // ── Special ──
803    /// Set `${^GLOBAL_PHASE}` on the interpreter. See [`GP_START`] … [`GP_END`].
804    SetGlobalPhase(u8),
805    Halt,
806    /// Delegate an AST expression to `Interpreter::eval_expr_ctx` at runtime.
807    /// Operand is an index into [`Chunk::ast_eval_exprs`].
808    EvalAstExpr(u16),
809
810    // ── Streaming map (appended — do not reorder earlier op tags) ─────────────
811    /// `maps { BLOCK } LIST` — stack: \[list\] → lazy iterator (pull-based; stryke extension).
812    MapsWithBlock(u16),
813    /// `flat_maps { BLOCK } LIST` — like [`Op::MapsWithBlock`] with `flat_map`-style flattening.
814    MapsFlatMapWithBlock(u16),
815    /// `maps EXPR, LIST` — index into [`Chunk::map_expr_entries`]; stack: \[list\] → iterator.
816    MapsWithExpr(u16),
817    /// `flat_maps EXPR, LIST` — same pools as [`Op::MapsWithExpr`].
818    MapsFlatMapWithExpr(u16),
819    /// `filter` / `fi` `{ BLOCK } LIST` — stack: \[list\] → lazy iterator (stryke; `grep` remains eager).
820    FilterWithBlock(u16),
821    /// `filter` / `fi` `EXPR, LIST` — index into [`Chunk::grep_expr_entries`]; stack: \[list\] → iterator.
822    FilterWithExpr(u16),
823}
824
825/// `${^GLOBAL_PHASE}` values emitted with [`Op::SetGlobalPhase`] (matches Perl’s phase strings).
826pub const GP_START: u8 = 0;
827/// Reserved; stock Perl 5 keeps `${^GLOBAL_PHASE}` as **`START`** during `UNITCHECK` blocks.
828pub const GP_UNITCHECK: u8 = 1;
829pub const GP_CHECK: u8 = 2;
830pub const GP_INIT: u8 = 3;
831pub const GP_RUN: u8 = 4;
832pub const GP_END: u8 = 5;
833
834/// Built-in function IDs for CallBuiltin dispatch.
835#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
836#[repr(u16)]
837pub enum BuiltinId {
838    // String
839    Length = 0,
840    Chomp,
841    Chop,
842    Substr,
843    Index,
844    Rindex,
845    Uc,
846    Lc,
847    Ucfirst,
848    Lcfirst,
849    Chr,
850    Ord,
851    Hex,
852    Oct,
853    Join,
854    Split,
855    Sprintf,
856
857    // Numeric
858    Abs,
859    Int,
860    Sqrt,
861
862    // Type
863    Defined,
864    Ref,
865    Scalar,
866
867    // Array
868    Splice,
869    Reverse,
870    Sort,
871    Unshift,
872
873    // Hash
874
875    // I/O
876    Open,
877    Close,
878    Eof,
879    ReadLine,
880    Printf,
881
882    // System
883    System,
884    Exec,
885    Exit,
886    Die,
887    Warn,
888    Chdir,
889    Mkdir,
890    Unlink,
891
892    // Control
893    Eval,
894    Do,
895    Require,
896
897    // OOP
898    Bless,
899    Caller,
900
901    // Parallel
902    PMap,
903    PGrep,
904    PFor,
905    PSort,
906    Fan,
907
908    // Map/Grep (block-based — need special handling)
909    MapBlock,
910    GrepBlock,
911    SortBlock,
912
913    // Math (appended — do not reorder earlier IDs)
914    Sin,
915    Cos,
916    Atan2,
917    Exp,
918    Log,
919    Rand,
920    Srand,
921
922    // String (appended)
923    Crypt,
924    Fc,
925    Pos,
926    Study,
927
928    Stat,
929    Lstat,
930    Link,
931    Symlink,
932    Readlink,
933    Glob,
934
935    Opendir,
936    Readdir,
937    Closedir,
938    Rewinddir,
939    Telldir,
940    Seekdir,
941    /// Read entire file as UTF-8 (`slurp $path`).
942    Slurp,
943    /// Blocking HTTP GET (`fetch_url $url`).
944    FetchUrl,
945    /// `pchannel()` — `(tx, rx)` as a two-element list.
946    Pchannel,
947    /// Parallel recursive glob (`glob_par`).
948    GlobPar,
949    /// `deque()` — empty deque.
950    DequeNew,
951    /// `heap(fn { })` — empty heap with comparator.
952    HeapNew,
953    /// `pipeline(...)` — lazy iterator (filter/map/take/collect).
954    Pipeline,
955    /// `capture("cmd")` — structured stdout/stderr/exit (via `sh -c`).
956    Capture,
957    /// `ppool(N)` — persistent thread pool (`submit` / `collect`).
958    Ppool,
959    /// Scalar/list context query (`wantarray`).
960    Wantarray,
961    /// `rename OLD, NEW`
962    Rename,
963    /// `chmod MODE, ...`
964    Chmod,
965    /// `chown UID, GID, ...`
966    Chown,
967    /// `pselect($rx1, $rx2, ...)` — multiplexed recv; returns `(value, index)`.
968    Pselect,
969    /// `barrier(N)` — thread barrier (`->wait`).
970    BarrierNew,
971    /// `par_pipeline(...)` — list form: same as `pipeline` but parallel `filter`/`map` on `collect()`.
972    ParPipeline,
973    /// `glob_par(..., progress => EXPR)` — last stack arg is truthy progress flag.
974    GlobParProgress,
975    /// `par_pipeline_stream(...)` — streaming pipeline with bounded channels between stages.
976    ParPipelineStream,
977    /// `par_sed(PATTERN, REPLACEMENT, FILES...)` — parallel in-place regex substitution per file.
978    ParSed,
979    /// `par_sed(..., progress => EXPR)` — last stack arg is truthy progress flag.
980    ParSedProgress,
981    /// `each EXPR` — returns empty list.
982    Each,
983    /// `` `cmd` `` / `qx{...}` — stdout string via `sh -c` (Perl readpipe); sets `$?`.
984    Readpipe,
985    /// `readline` / `<HANDLE>` in **list** context — all remaining lines until EOF (Perl `readline` list semantics).
986    ReadLineList,
987    /// `readdir` in **list** context — all names not yet returned (Perl drains the rest of the stream).
988    ReaddirList,
989    /// `ssh HOST, CMD, …` / `ssh(HOST, …)` — `execvp` style `ssh` only (no shell).
990    Ssh,
991    /// `rmdir LIST` — remove empty directories; returns count removed (appended ID).
992    Rmdir,
993    /// `utime ATIME, MTIME, LIST` — set access/mod times (Unix).
994    Utime,
995    /// `umask EXPR` / `umask()` — process file mode creation mask (Unix).
996    Umask,
997    /// `getcwd` / `pwd` — bare-name builtin returning the absolute current working directory.
998    Getcwd,
999    /// `pipe READHANDLE, WRITEHANDLE` — OS pipe ends (Unix).
1000    Pipe,
1001    /// `files` / `files DIR` — list file names in a directory (default: `.`).
1002    Files,
1003    /// `filesf` / `filesf DIR` / `f` — list only regular file names in a directory (default: `.`).
1004    Filesf,
1005    /// `fr DIR` — list only regular file names recursively (default: `.`).
1006    FilesfRecursive,
1007    /// `dirs` / `dirs DIR` / `d` — list subdirectory names in a directory (default: `.`).
1008    Dirs,
1009    /// `dr DIR` — list subdirectory paths recursively (default: `.`).
1010    DirsRecursive,
1011    /// `sym_links` / `sym_links DIR` — list symlink names in a directory (default: `.`).
1012    SymLinks,
1013    /// `sockets` / `sockets DIR` — list Unix socket names in a directory (default: `.`).
1014    Sockets,
1015    /// `pipes` / `pipes DIR` — list named-pipe (FIFO) names in a directory (default: `.`).
1016    Pipes,
1017    /// `block_devices` / `block_devices DIR` — list block device names in a directory (default: `.`).
1018    BlockDevices,
1019    /// `char_devices` / `char_devices DIR` — list character device names in a directory (default: `.`).
1020    CharDevices,
1021    /// `exe` / `exe DIR` — list executable file names in a directory (default: `.`).
1022    Executables,
1023}
1024
1025impl BuiltinId {
1026    pub fn from_u16(v: u16) -> Option<Self> {
1027        if v <= Self::Executables as u16 {
1028            Some(unsafe { std::mem::transmute::<u16, BuiltinId>(v) })
1029        } else {
1030            None
1031        }
1032    }
1033}
1034
1035/// A compiled chunk of bytecode with its constant pools.
1036#[derive(Debug, Clone, Serialize, Deserialize)]
1037pub struct Chunk {
1038    pub ops: Vec<Op>,
1039    /// Constant pool: string literals, regex patterns, etc.
1040    #[serde(with = "crate::script_cache::constants_pool_codec")]
1041    pub constants: Vec<PerlValue>,
1042    /// Name pool: variable names, sub names (interned/deduped).
1043    pub names: Vec<String>,
1044    /// Source line for each op (parallel array for error reporting).
1045    pub lines: Vec<usize>,
1046    /// Optional link from each op to the originating [`Expr`] (pool index into [`Self::ast_expr_pool`]).
1047    /// Filled for ops emitted from [`crate::compiler::Compiler::compile_expr_ctx`]; other paths leave `None`.
1048    pub op_ast_expr: Vec<Option<u32>>,
1049    /// Interned [`Expr`] nodes referenced by [`Self::op_ast_expr`] (for debugging / tooling).
1050    pub ast_expr_pool: Vec<Expr>,
1051    /// Compiled subroutine entry points: (name_index, op_index, uses_stack_args).
1052    /// When `uses_stack_args` is true, the Call op leaves arguments on the value
1053    /// stack and the sub reads them via `GetArg(idx)` instead of `shift @_`.
1054    pub sub_entries: Vec<(u16, usize, bool)>,
1055    /// AST blocks for map/grep/sort/parallel operations.
1056    /// Referenced by block-based opcodes via u16 index.
1057    pub blocks: Vec<Block>,
1058    /// When `Some((start, end))`, `blocks[i]` is also lowered to `ops[start..end]` (exclusive `end`)
1059    /// with trailing [`Op::BlockReturnValue`]. VM uses opcodes; otherwise the AST in `blocks[i]`.
1060    pub block_bytecode_ranges: Vec<Option<(usize, usize)>>,
1061    /// Resolved [`Op::CallStaticSubId`] targets: subroutine entry IP, stack-args calling convention,
1062    /// and stash name pool index (qualified key matching [`Interpreter::subs`]).
1063    pub static_sub_calls: Vec<(usize, bool, u16)>,
1064    /// Assign targets for `s///` / `tr///` bytecode (LHS expressions).
1065    pub lvalues: Vec<Expr>,
1066    /// AST expressions delegated to interpreter at runtime via [`Op::EvalAstExpr`].
1067    pub ast_eval_exprs: Vec<Expr>,
1068    /// Instruction pointer where the main program body starts (after BEGIN/CHECK/INIT phase blocks).
1069    /// Used by `-n`/`-p` line mode to re-execute only the body per input line.
1070    pub body_start_ip: usize,
1071    /// `struct Name { ... }` definitions in this chunk (registered on the interpreter at VM start).
1072    pub struct_defs: Vec<StructDef>,
1073    /// `enum Name { ... }` definitions in this chunk (registered on the interpreter at VM start).
1074    pub enum_defs: Vec<EnumDef>,
1075    /// `class Name extends ... impl ... { ... }` definitions.
1076    pub class_defs: Vec<ClassDef>,
1077    /// `trait Name { ... }` definitions.
1078    pub trait_defs: Vec<TraitDef>,
1079    /// `given (topic) { body }` — topic expression + body (when/default handled by interpreter).
1080    pub given_entries: Vec<(Expr, Block)>,
1081    /// When `Some((start, end))`, `given_entries[i].0` (topic) is lowered to `ops[start..end]` +
1082    /// [`Op::BlockReturnValue`].
1083    pub given_topic_bytecode_ranges: Vec<Option<(usize, usize)>>,
1084    /// `eval_timeout timeout_expr { body }` — evaluated at runtime.
1085    pub eval_timeout_entries: Vec<(Expr, Block)>,
1086    /// When `Some((start, end))`, `eval_timeout_entries[i].0` (timeout expr) is lowered to
1087    /// `ops[start..end]` with trailing [`Op::BlockReturnValue`].
1088    pub eval_timeout_expr_bytecode_ranges: Vec<Option<(usize, usize)>>,
1089    /// Algebraic `match (subject) { arms }`.
1090    pub algebraic_match_entries: Vec<(Expr, Vec<MatchArm>)>,
1091    /// When `Some((start, end))`, `algebraic_match_entries[i].0` (subject) is lowered to
1092    /// `ops[start..end]` + [`Op::BlockReturnValue`].
1093    pub algebraic_match_subject_bytecode_ranges: Vec<Option<(usize, usize)>>,
1094    /// Nested / runtime `sub` declarations (see [`Op::RuntimeSubDecl`]).
1095    pub runtime_sub_decls: Vec<RuntimeSubDecl>,
1096    /// Stryke `fn ($a, …)` / hash-destruct params for [`Op::MakeCodeRef`] (second operand is pool index).
1097    pub code_ref_sigs: Vec<Vec<SubSigParam>>,
1098    /// `par_lines PATH, fn { } [, progress => EXPR]` — evaluated by interpreter inside VM.
1099    pub par_lines_entries: Vec<(Expr, Expr, Option<Expr>)>,
1100    /// `par_walk PATH, fn { } [, progress => EXPR]` — evaluated by interpreter inside VM.
1101    pub par_walk_entries: Vec<(Expr, Expr, Option<Expr>)>,
1102    /// `pwatch GLOB, fn { }` — evaluated by interpreter inside VM.
1103    pub pwatch_entries: Vec<(Expr, Expr)>,
1104    /// `substr $var, OFF, LEN, REPL` — four-arg form (mutates `LHS`); evaluated by interpreter inside VM.
1105    pub substr_four_arg_entries: Vec<(Expr, Expr, Option<Expr>, Expr)>,
1106    /// `keys EXPR` when `EXPR` is not bare `%h`.
1107    pub keys_expr_entries: Vec<Expr>,
1108    /// When `Some((start, end))`, `keys_expr_entries[i]` is lowered to `ops[start..end]` +
1109    /// [`Op::BlockReturnValue`] (operand only; [`Op::KeysExpr`] still applies `keys` to the value).
1110    pub keys_expr_bytecode_ranges: Vec<Option<(usize, usize)>>,
1111    /// `values EXPR` when not bare `%h`.
1112    pub values_expr_entries: Vec<Expr>,
1113    pub values_expr_bytecode_ranges: Vec<Option<(usize, usize)>>,
1114    /// `delete EXPR` when not the fast `%h{k}` lowering.
1115    pub delete_expr_entries: Vec<Expr>,
1116    /// `exists EXPR` when not the fast `%h{k}` lowering.
1117    pub exists_expr_entries: Vec<Expr>,
1118    /// `push` when the array operand is not a bare `@name` (e.g. `push $aref, ...`).
1119    pub push_expr_entries: Vec<(Expr, Vec<Expr>)>,
1120    pub pop_expr_entries: Vec<Expr>,
1121    pub shift_expr_entries: Vec<Expr>,
1122    pub unshift_expr_entries: Vec<(Expr, Vec<Expr>)>,
1123    pub splice_expr_entries: Vec<SpliceExprEntry>,
1124    /// `map EXPR, LIST` — map expression (list context) with `$_` set to each element.
1125    pub map_expr_entries: Vec<Expr>,
1126    /// When `Some((start, end))`, `map_expr_entries[i]` is lowered like [`Self::grep_expr_bytecode_ranges`].
1127    pub map_expr_bytecode_ranges: Vec<Option<(usize, usize)>>,
1128    /// `grep EXPR, LIST` — filter expression evaluated with `$_` set to each element.
1129    pub grep_expr_entries: Vec<Expr>,
1130    /// When `Some((start, end))`, `grep_expr_entries[i]` is also lowered to `ops[start..end]`
1131    /// (exclusive `end`) with trailing [`Op::BlockReturnValue`], like [`Self::block_bytecode_ranges`].
1132    pub grep_expr_bytecode_ranges: Vec<Option<(usize, usize)>>,
1133    /// Right-hand expression for [`Op::RegexFlipFlopExprRhs`] — boolean context (bare `m//` is `$_ =~ m//`).
1134    pub regex_flip_flop_rhs_expr_entries: Vec<Expr>,
1135    /// When `Some((start, end))`, `regex_flip_flop_rhs_expr_entries[i]` is lowered to `ops[start..end]` +
1136    /// [`Op::BlockReturnValue`].
1137    pub regex_flip_flop_rhs_expr_bytecode_ranges: Vec<Option<(usize, usize)>>,
1138    /// Number of flip-flop slots ([`Op::ScalarFlipFlop`], [`Op::RegexFlipFlop`], [`Op::RegexEofFlipFlop`],
1139    /// [`Op::RegexFlipFlopExprRhs`], [`Op::RegexFlipFlopDotLineRhs`]); VM resets flip-flop vectors.
1140    pub flip_flop_slots: u16,
1141    /// `format NAME =` bodies: basename + lines between `=` and `.` (see lexer).
1142    pub format_decls: Vec<(String, Vec<String>)>,
1143    /// `use overload` pair lists (installed into current package at run time).
1144    pub use_overload_entries: Vec<Vec<(String, String)>>,
1145}
1146
1147impl Chunk {
1148    /// Look up a compiled subroutine entry by stash name pool index.
1149    pub fn find_sub_entry(&self, name_idx: u16) -> Option<(usize, bool)> {
1150        self.sub_entries
1151            .iter()
1152            .find(|(n, _, _)| *n == name_idx)
1153            .map(|(_, ip, stack_args)| (*ip, *stack_args))
1154    }
1155
1156    pub fn new() -> Self {
1157        Self {
1158            ops: Vec::with_capacity(256),
1159            constants: Vec::new(),
1160            names: Vec::new(),
1161            lines: Vec::new(),
1162            op_ast_expr: Vec::new(),
1163            ast_expr_pool: Vec::new(),
1164            sub_entries: Vec::new(),
1165            blocks: Vec::new(),
1166            block_bytecode_ranges: Vec::new(),
1167            static_sub_calls: Vec::new(),
1168            lvalues: Vec::new(),
1169            ast_eval_exprs: Vec::new(),
1170            body_start_ip: 0,
1171            struct_defs: Vec::new(),
1172            enum_defs: Vec::new(),
1173            class_defs: Vec::new(),
1174            trait_defs: Vec::new(),
1175            given_entries: Vec::new(),
1176            given_topic_bytecode_ranges: Vec::new(),
1177            eval_timeout_entries: Vec::new(),
1178            eval_timeout_expr_bytecode_ranges: Vec::new(),
1179            algebraic_match_entries: Vec::new(),
1180            algebraic_match_subject_bytecode_ranges: Vec::new(),
1181            runtime_sub_decls: Vec::new(),
1182            code_ref_sigs: Vec::new(),
1183            par_lines_entries: Vec::new(),
1184            par_walk_entries: Vec::new(),
1185            pwatch_entries: Vec::new(),
1186            substr_four_arg_entries: Vec::new(),
1187            keys_expr_entries: Vec::new(),
1188            keys_expr_bytecode_ranges: Vec::new(),
1189            values_expr_entries: Vec::new(),
1190            values_expr_bytecode_ranges: Vec::new(),
1191            delete_expr_entries: Vec::new(),
1192            exists_expr_entries: Vec::new(),
1193            push_expr_entries: Vec::new(),
1194            pop_expr_entries: Vec::new(),
1195            shift_expr_entries: Vec::new(),
1196            unshift_expr_entries: Vec::new(),
1197            splice_expr_entries: Vec::new(),
1198            map_expr_entries: Vec::new(),
1199            map_expr_bytecode_ranges: Vec::new(),
1200            grep_expr_entries: Vec::new(),
1201            grep_expr_bytecode_ranges: Vec::new(),
1202            regex_flip_flop_rhs_expr_entries: Vec::new(),
1203            regex_flip_flop_rhs_expr_bytecode_ranges: Vec::new(),
1204            flip_flop_slots: 0,
1205            format_decls: Vec::new(),
1206            use_overload_entries: Vec::new(),
1207        }
1208    }
1209
1210    /// Pool index for [`Op::FormatDecl`].
1211    pub fn add_format_decl(&mut self, name: String, lines: Vec<String>) -> u16 {
1212        let idx = self.format_decls.len() as u16;
1213        self.format_decls.push((name, lines));
1214        idx
1215    }
1216
1217    /// Pool index for [`Op::UseOverload`].
1218    pub fn add_use_overload(&mut self, pairs: Vec<(String, String)>) -> u16 {
1219        let idx = self.use_overload_entries.len() as u16;
1220        self.use_overload_entries.push(pairs);
1221        idx
1222    }
1223
1224    /// Allocate a slot index for [`Op::ScalarFlipFlop`] / [`Op::RegexFlipFlop`] / [`Op::RegexEofFlipFlop`] /
1225    /// [`Op::RegexFlipFlopExprRhs`] / [`Op::RegexFlipFlopDotLineRhs`] flip-flop state.
1226    pub fn alloc_flip_flop_slot(&mut self) -> u16 {
1227        let id = self.flip_flop_slots;
1228        self.flip_flop_slots = self.flip_flop_slots.saturating_add(1);
1229        id
1230    }
1231
1232    /// `map EXPR, LIST` — pool index for [`Op::MapWithExpr`].
1233    pub fn add_map_expr_entry(&mut self, expr: Expr) -> u16 {
1234        let idx = self.map_expr_entries.len() as u16;
1235        self.map_expr_entries.push(expr);
1236        idx
1237    }
1238
1239    /// `grep EXPR, LIST` — pool index for [`Op::GrepWithExpr`].
1240    pub fn add_grep_expr_entry(&mut self, expr: Expr) -> u16 {
1241        let idx = self.grep_expr_entries.len() as u16;
1242        self.grep_expr_entries.push(expr);
1243        idx
1244    }
1245
1246    /// Regex flip-flop with compound RHS — pool index for [`Op::RegexFlipFlopExprRhs`].
1247    pub fn add_regex_flip_flop_rhs_expr_entry(&mut self, expr: Expr) -> u16 {
1248        let idx = self.regex_flip_flop_rhs_expr_entries.len() as u16;
1249        self.regex_flip_flop_rhs_expr_entries.push(expr);
1250        idx
1251    }
1252
1253    /// `keys EXPR` (dynamic) — pool index for [`Op::KeysExpr`].
1254    pub fn add_keys_expr_entry(&mut self, expr: Expr) -> u16 {
1255        let idx = self.keys_expr_entries.len() as u16;
1256        self.keys_expr_entries.push(expr);
1257        idx
1258    }
1259
1260    /// `values EXPR` (dynamic) — pool index for [`Op::ValuesExpr`].
1261    pub fn add_values_expr_entry(&mut self, expr: Expr) -> u16 {
1262        let idx = self.values_expr_entries.len() as u16;
1263        self.values_expr_entries.push(expr);
1264        idx
1265    }
1266
1267    /// `delete EXPR` (dynamic operand) — pool index for [`Op::DeleteExpr`].
1268    pub fn add_delete_expr_entry(&mut self, expr: Expr) -> u16 {
1269        let idx = self.delete_expr_entries.len() as u16;
1270        self.delete_expr_entries.push(expr);
1271        idx
1272    }
1273
1274    /// `exists EXPR` (dynamic operand) — pool index for [`Op::ExistsExpr`].
1275    pub fn add_exists_expr_entry(&mut self, expr: Expr) -> u16 {
1276        let idx = self.exists_expr_entries.len() as u16;
1277        self.exists_expr_entries.push(expr);
1278        idx
1279    }
1280
1281    pub fn add_push_expr_entry(&mut self, array: Expr, values: Vec<Expr>) -> u16 {
1282        let idx = self.push_expr_entries.len() as u16;
1283        self.push_expr_entries.push((array, values));
1284        idx
1285    }
1286
1287    pub fn add_pop_expr_entry(&mut self, array: Expr) -> u16 {
1288        let idx = self.pop_expr_entries.len() as u16;
1289        self.pop_expr_entries.push(array);
1290        idx
1291    }
1292
1293    pub fn add_shift_expr_entry(&mut self, array: Expr) -> u16 {
1294        let idx = self.shift_expr_entries.len() as u16;
1295        self.shift_expr_entries.push(array);
1296        idx
1297    }
1298
1299    pub fn add_unshift_expr_entry(&mut self, array: Expr, values: Vec<Expr>) -> u16 {
1300        let idx = self.unshift_expr_entries.len() as u16;
1301        self.unshift_expr_entries.push((array, values));
1302        idx
1303    }
1304
1305    pub fn add_splice_expr_entry(
1306        &mut self,
1307        array: Expr,
1308        offset: Option<Expr>,
1309        length: Option<Expr>,
1310        replacement: Vec<Expr>,
1311    ) -> u16 {
1312        let idx = self.splice_expr_entries.len() as u16;
1313        self.splice_expr_entries
1314            .push((array, offset, length, replacement));
1315        idx
1316    }
1317
1318    /// Four-arg `substr` — returns pool index for [`Op::SubstrFourArg`].
1319    pub fn add_substr_four_arg_entry(
1320        &mut self,
1321        string: Expr,
1322        offset: Expr,
1323        length: Option<Expr>,
1324        replacement: Expr,
1325    ) -> u16 {
1326        let idx = self.substr_four_arg_entries.len() as u16;
1327        self.substr_four_arg_entries
1328            .push((string, offset, length, replacement));
1329        idx
1330    }
1331
1332    /// `par_lines PATH, fn { } [, progress => EXPR]` — returns pool index for [`Op::ParLines`].
1333    pub fn add_par_lines_entry(
1334        &mut self,
1335        path: Expr,
1336        callback: Expr,
1337        progress: Option<Expr>,
1338    ) -> u16 {
1339        let idx = self.par_lines_entries.len() as u16;
1340        self.par_lines_entries.push((path, callback, progress));
1341        idx
1342    }
1343
1344    /// `par_walk PATH, fn { } [, progress => EXPR]` — returns pool index for [`Op::ParWalk`].
1345    pub fn add_par_walk_entry(
1346        &mut self,
1347        path: Expr,
1348        callback: Expr,
1349        progress: Option<Expr>,
1350    ) -> u16 {
1351        let idx = self.par_walk_entries.len() as u16;
1352        self.par_walk_entries.push((path, callback, progress));
1353        idx
1354    }
1355
1356    /// `pwatch GLOB, fn { }` — returns pool index for [`Op::Pwatch`].
1357    pub fn add_pwatch_entry(&mut self, path: Expr, callback: Expr) -> u16 {
1358        let idx = self.pwatch_entries.len() as u16;
1359        self.pwatch_entries.push((path, callback));
1360        idx
1361    }
1362
1363    /// `given (EXPR) { ... }` — returns pool index for [`Op::Given`].
1364    pub fn add_given_entry(&mut self, topic: Expr, body: Block) -> u16 {
1365        let idx = self.given_entries.len() as u16;
1366        self.given_entries.push((topic, body));
1367        idx
1368    }
1369
1370    /// `eval_timeout SECS { ... }` — returns pool index for [`Op::EvalTimeout`].
1371    pub fn add_eval_timeout_entry(&mut self, timeout: Expr, body: Block) -> u16 {
1372        let idx = self.eval_timeout_entries.len() as u16;
1373        self.eval_timeout_entries.push((timeout, body));
1374        idx
1375    }
1376
1377    /// Algebraic `match` — returns pool index for [`Op::AlgebraicMatch`].
1378    pub fn add_algebraic_match_entry(&mut self, subject: Expr, arms: Vec<MatchArm>) -> u16 {
1379        let idx = self.algebraic_match_entries.len() as u16;
1380        self.algebraic_match_entries.push((subject, arms));
1381        idx
1382    }
1383
1384    /// Store an AST block and return its index.
1385    pub fn add_block(&mut self, block: Block) -> u16 {
1386        let idx = self.blocks.len() as u16;
1387        self.blocks.push(block);
1388        idx
1389    }
1390
1391    /// Pool index for [`Op::MakeCodeRef`] signature (`stryke` extension); use empty vec for legacy `fn { }`.
1392    pub fn add_code_ref_sig(&mut self, params: Vec<SubSigParam>) -> u16 {
1393        let idx = self.code_ref_sigs.len();
1394        if idx > u16::MAX as usize {
1395            panic!("too many anonymous sub signatures in one chunk");
1396        }
1397        self.code_ref_sigs.push(params);
1398        idx as u16
1399    }
1400
1401    /// Store an assignable expression (LHS of `s///` / `tr///`) and return its index.
1402    pub fn add_lvalue_expr(&mut self, e: Expr) -> u16 {
1403        let idx = self.lvalues.len() as u16;
1404        self.lvalues.push(e);
1405        idx
1406    }
1407
1408    /// Intern a name, returning its pool index.
1409    pub fn intern_name(&mut self, name: &str) -> u16 {
1410        if let Some(idx) = self.names.iter().position(|n| n == name) {
1411            return idx as u16;
1412        }
1413        let idx = self.names.len() as u16;
1414        self.names.push(name.to_string());
1415        idx
1416    }
1417
1418    /// Add a constant to the pool, returning its index.
1419    pub fn add_constant(&mut self, val: PerlValue) -> u16 {
1420        // Dedup string constants
1421        if let Some(ref s) = val.as_str() {
1422            for (i, c) in self.constants.iter().enumerate() {
1423                if let Some(cs) = c.as_str() {
1424                    if cs == *s {
1425                        return i as u16;
1426                    }
1427                }
1428            }
1429        }
1430        let idx = self.constants.len() as u16;
1431        self.constants.push(val);
1432        idx
1433    }
1434
1435    /// Append an op with source line info.
1436    #[inline]
1437    pub fn emit(&mut self, op: Op, line: usize) -> usize {
1438        self.emit_with_ast_idx(op, line, None)
1439    }
1440
1441    /// Like [`Self::emit`] but attach an optional interned AST [`Expr`] pool index (see [`Self::op_ast_expr`]).
1442    #[inline]
1443    pub fn emit_with_ast_idx(&mut self, op: Op, line: usize, ast: Option<u32>) -> usize {
1444        let idx = self.ops.len();
1445        self.ops.push(op);
1446        self.lines.push(line);
1447        self.op_ast_expr.push(ast);
1448        idx
1449    }
1450
1451    /// Resolve the originating expression for an instruction pointer, if recorded.
1452    #[inline]
1453    pub fn ast_expr_at(&self, ip: usize) -> Option<&Expr> {
1454        let id = (*self.op_ast_expr.get(ip)?)?;
1455        self.ast_expr_pool.get(id as usize)
1456    }
1457
1458    /// Patch a jump instruction at `idx` to target the current position.
1459    pub fn patch_jump_here(&mut self, idx: usize) {
1460        let target = self.ops.len();
1461        self.patch_jump_to(idx, target);
1462    }
1463
1464    /// Patch a jump instruction at `idx` to target an explicit op address.
1465    pub fn patch_jump_to(&mut self, idx: usize, target: usize) {
1466        match &mut self.ops[idx] {
1467            Op::Jump(ref mut t)
1468            | Op::JumpIfTrue(ref mut t)
1469            | Op::JumpIfFalse(ref mut t)
1470            | Op::JumpIfFalseKeep(ref mut t)
1471            | Op::JumpIfTrueKeep(ref mut t)
1472            | Op::JumpIfDefinedKeep(ref mut t) => *t = target,
1473            _ => panic!("patch_jump_to on non-jump op at {}", idx),
1474        }
1475    }
1476
1477    pub fn patch_try_push_catch(&mut self, idx: usize, catch_ip: usize) {
1478        match &mut self.ops[idx] {
1479            Op::TryPush { catch_ip: c, .. } => *c = catch_ip,
1480            _ => panic!("patch_try_push_catch on non-TryPush op at {}", idx),
1481        }
1482    }
1483
1484    pub fn patch_try_push_finally(&mut self, idx: usize, finally_ip: Option<usize>) {
1485        match &mut self.ops[idx] {
1486            Op::TryPush { finally_ip: f, .. } => *f = finally_ip,
1487            _ => panic!("patch_try_push_finally on non-TryPush op at {}", idx),
1488        }
1489    }
1490
1491    pub fn patch_try_push_after(&mut self, idx: usize, after_ip: usize) {
1492        match &mut self.ops[idx] {
1493            Op::TryPush { after_ip: a, .. } => *a = after_ip,
1494            _ => panic!("patch_try_push_after on non-TryPush op at {}", idx),
1495        }
1496    }
1497
1498    /// Current op count (next emit position).
1499    #[inline]
1500    pub fn len(&self) -> usize {
1501        self.ops.len()
1502    }
1503
1504    #[inline]
1505    pub fn is_empty(&self) -> bool {
1506        self.ops.is_empty()
1507    }
1508
1509    /// Human-readable listing: subroutine entry points and each op with its source line (javap / `dis`-style).
1510    pub fn disassemble(&self) -> String {
1511        use std::fmt::Write;
1512        let mut out = String::new();
1513        for (i, n) in self.names.iter().enumerate() {
1514            let _ = writeln!(out, "; name[{}] = {}", i, n);
1515        }
1516        let _ = writeln!(out, "; sub_entries:");
1517        for (ni, ip, stack_args) in &self.sub_entries {
1518            let name = self
1519                .names
1520                .get(*ni as usize)
1521                .map(|s| s.as_str())
1522                .unwrap_or("?");
1523            let _ = writeln!(out, ";   {} @ {} stack_args={}", name, ip, stack_args);
1524        }
1525        for (i, op) in self.ops.iter().enumerate() {
1526            let line = self.lines.get(i).copied().unwrap_or(0);
1527            let ast = self
1528                .op_ast_expr
1529                .get(i)
1530                .copied()
1531                .flatten()
1532                .map(|id| id.to_string())
1533                .unwrap_or_else(|| "-".into());
1534            let _ = writeln!(out, "{:04} {:>5} {:>6}  {:?}", i, line, ast, op);
1535        }
1536        out
1537    }
1538
1539    /// Peephole pass: fuse common multi-op sequences into single superinstructions,
1540    /// then compact by removing Nop slots and remapping all jump targets.
1541    pub fn peephole_fuse(&mut self) {
1542        let len = self.ops.len();
1543        if len < 2 {
1544            return;
1545        }
1546        // Pass 1: fuse OP + Pop → OPVoid
1547        let mut i = 0;
1548        while i + 1 < len {
1549            if matches!(self.ops[i + 1], Op::Pop) {
1550                let replacement = match &self.ops[i] {
1551                    Op::AddAssignSlotSlot(d, s) => Some(Op::AddAssignSlotSlotVoid(*d, *s)),
1552                    Op::PreIncSlot(s) => Some(Op::PreIncSlotVoid(*s)),
1553                    Op::ConcatAppendSlot(s) => Some(Op::ConcatAppendSlotVoid(*s)),
1554                    _ => None,
1555                };
1556                if let Some(op) = replacement {
1557                    self.ops[i] = op;
1558                    self.ops[i + 1] = Op::Nop;
1559                    i += 2;
1560                    continue;
1561                }
1562            }
1563            i += 1;
1564        }
1565        // Pass 2: fuse multi-op patterns
1566        // Helper: check if any jump targets position `pos`.
1567        let has_jump_to = |ops: &[Op], pos: usize| -> bool {
1568            for op in ops {
1569                let t = match op {
1570                    Op::Jump(t)
1571                    | Op::JumpIfFalse(t)
1572                    | Op::JumpIfTrue(t)
1573                    | Op::JumpIfFalseKeep(t)
1574                    | Op::JumpIfTrueKeep(t)
1575                    | Op::JumpIfDefinedKeep(t) => Some(*t),
1576                    _ => None,
1577                };
1578                if t == Some(pos) {
1579                    return true;
1580                }
1581            }
1582            false
1583        };
1584        let len = self.ops.len();
1585        if len >= 4 {
1586            i = 0;
1587            while i + 3 < len {
1588                if let (
1589                    Op::GetScalarSlot(slot),
1590                    Op::LoadInt(n),
1591                    Op::NumLt,
1592                    Op::JumpIfFalse(target),
1593                ) = (
1594                    &self.ops[i],
1595                    &self.ops[i + 1],
1596                    &self.ops[i + 2],
1597                    &self.ops[i + 3],
1598                ) {
1599                    if let Ok(n32) = i32::try_from(*n) {
1600                        // Don't fuse if any jump targets the ops that will become Nop.
1601                        // This prevents breaking short-circuit &&/|| that jump to the
1602                        // JumpIfFalse for the while condition exit check.
1603                        if has_jump_to(&self.ops, i + 1)
1604                            || has_jump_to(&self.ops, i + 2)
1605                            || has_jump_to(&self.ops, i + 3)
1606                        {
1607                            i += 1;
1608                            continue;
1609                        }
1610                        let slot = *slot;
1611                        let target = *target;
1612                        self.ops[i] = Op::SlotLtIntJumpIfFalse(slot, n32, target);
1613                        self.ops[i + 1] = Op::Nop;
1614                        self.ops[i + 2] = Op::Nop;
1615                        self.ops[i + 3] = Op::Nop;
1616                        i += 4;
1617                        continue;
1618                    }
1619                }
1620                i += 1;
1621            }
1622        }
1623        // Compact once so that pass 3 sees a Nop-free op stream and can match
1624        // adjacent `PreIncSlotVoid + Jump` backedges produced by passes 1/2.
1625        self.compact_nops();
1626        // Pass 3: fuse loop backedge
1627        //   PreIncSlotVoid(s)  + Jump(top)
1628        // where ops[top] is SlotLtIntJumpIfFalse(s, limit, exit)
1629        // becomes
1630        //   SlotIncLtIntJumpBack(s, limit, top + 1)   // body falls through
1631        //   Nop                                       // was Jump
1632        // The first-iteration check at `top` is still reached from before the loop
1633        // (the loop's initial entry goes through the top test), so leaving
1634        // SlotLtIntJumpIfFalse in place keeps the entry path correct. All
1635        // subsequent iterations now skip both the inc op and the jump.
1636        let len = self.ops.len();
1637        if len >= 2 {
1638            let mut i = 0;
1639            while i + 1 < len {
1640                if let (Op::PreIncSlotVoid(s), Op::Jump(top)) = (&self.ops[i], &self.ops[i + 1]) {
1641                    let slot = *s;
1642                    let top = *top;
1643                    // Only fuse backward branches — the C-style `for` shape where `top` is
1644                    // the loop's `SlotLtIntJumpIfFalse` test and the body falls through to
1645                    // this trailing increment. A forward `Jump` that happens to land on a
1646                    // similar test is not the same shape and must not be rewritten.
1647                    if top < i {
1648                        if let Op::SlotLtIntJumpIfFalse(tslot, limit, exit) = &self.ops[top] {
1649                            // Safety: the top test's exit target must equal the fused op's
1650                            // fall-through (i + 2). Otherwise exiting the loop via
1651                            // "condition false" would land somewhere the unfused shape never
1652                            // exited to.
1653                            if *tslot == slot && *exit == i + 2 {
1654                                let limit = *limit;
1655                                let body_target = top + 1;
1656                                self.ops[i] = Op::SlotIncLtIntJumpBack(slot, limit, body_target);
1657                                self.ops[i + 1] = Op::Nop;
1658                                i += 2;
1659                                continue;
1660                            }
1661                        }
1662                    }
1663                }
1664                i += 1;
1665            }
1666        }
1667        // Pass 4: compact again — remove the Nops introduced by pass 3.
1668        self.compact_nops();
1669        // Pass 5: fuse counted-loop bodies down to a single native superinstruction.
1670        //
1671        // After pass 3 + compact, a `for (my $i = ..; $i < N; $i = $i + 1) { $sum += $i }`
1672        // loop looks like:
1673        //
1674        //     [top]        SlotLtIntJumpIfFalse(i, N, exit)
1675        //     [body_start] AddAssignSlotSlotVoid(sum, i)       ← target of the backedge
1676        //                  SlotIncLtIntJumpBack(i, N, body_start)
1677        //     [exit]       ...
1678        //
1679        // When the body is exactly one op, we fuse the AddAssign + backedge into
1680        // `AccumSumLoop(sum, i, N)`, whose handler runs the whole remaining loop in a
1681        // tight Rust `while`. Same scheme for the counted `$s .= CONST` pattern, fused
1682        // into `ConcatConstSlotLoop`.
1683        //
1684        // Safety gate: only fire when no op jumps *into* the body (other than the backedge
1685        // itself and the top test's fall-through, which isn't a jump). That keeps loops with
1686        // interior labels / `last LABEL` / `next LABEL` from being silently skipped.
1687        let len = self.ops.len();
1688        if len >= 2 {
1689            let has_inbound_jump = |ops: &[Op], pos: usize, ignore: usize| -> bool {
1690                for (j, op) in ops.iter().enumerate() {
1691                    if j == ignore {
1692                        continue;
1693                    }
1694                    let t = match op {
1695                        Op::Jump(t)
1696                        | Op::JumpIfFalse(t)
1697                        | Op::JumpIfTrue(t)
1698                        | Op::JumpIfFalseKeep(t)
1699                        | Op::JumpIfTrueKeep(t)
1700                        | Op::JumpIfDefinedKeep(t) => Some(*t),
1701                        Op::SlotLtIntJumpIfFalse(_, _, t) => Some(*t),
1702                        Op::SlotIncLtIntJumpBack(_, _, t) => Some(*t),
1703                        _ => None,
1704                    };
1705                    if t == Some(pos) {
1706                        return true;
1707                    }
1708                }
1709                false
1710            };
1711            // 5a: AddAssignSlotSlotVoid + SlotIncLtIntJumpBack → AccumSumLoop
1712            let mut i = 0;
1713            while i + 1 < len {
1714                if let (
1715                    Op::AddAssignSlotSlotVoid(sum_slot, src_slot),
1716                    Op::SlotIncLtIntJumpBack(inc_slot, limit, body_target),
1717                ) = (&self.ops[i], &self.ops[i + 1])
1718                {
1719                    if *src_slot == *inc_slot
1720                        && *body_target == i
1721                        && !has_inbound_jump(&self.ops, i, i + 1)
1722                        && !has_inbound_jump(&self.ops, i + 1, i + 1)
1723                    {
1724                        let sum_slot = *sum_slot;
1725                        let src_slot = *src_slot;
1726                        let limit = *limit;
1727                        self.ops[i] = Op::AccumSumLoop(sum_slot, src_slot, limit);
1728                        self.ops[i + 1] = Op::Nop;
1729                        i += 2;
1730                        continue;
1731                    }
1732                }
1733                i += 1;
1734            }
1735            // 5b: LoadConst + ConcatAppendSlotVoid + SlotIncLtIntJumpBack → ConcatConstSlotLoop
1736            if len >= 3 {
1737                let mut i = 0;
1738                while i + 2 < len {
1739                    if let (
1740                        Op::LoadConst(const_idx),
1741                        Op::ConcatAppendSlotVoid(s_slot),
1742                        Op::SlotIncLtIntJumpBack(inc_slot, limit, body_target),
1743                    ) = (&self.ops[i], &self.ops[i + 1], &self.ops[i + 2])
1744                    {
1745                        if *body_target == i
1746                            && !has_inbound_jump(&self.ops, i, i + 2)
1747                            && !has_inbound_jump(&self.ops, i + 1, i + 2)
1748                            && !has_inbound_jump(&self.ops, i + 2, i + 2)
1749                        {
1750                            let const_idx = *const_idx;
1751                            let s_slot = *s_slot;
1752                            let inc_slot = *inc_slot;
1753                            let limit = *limit;
1754                            self.ops[i] =
1755                                Op::ConcatConstSlotLoop(const_idx, s_slot, inc_slot, limit);
1756                            self.ops[i + 1] = Op::Nop;
1757                            self.ops[i + 2] = Op::Nop;
1758                            i += 3;
1759                            continue;
1760                        }
1761                    }
1762                    i += 1;
1763                }
1764            }
1765            // 5e: `$sum += $h{$k}` body op inside `for my $k (keys %h) { ... }`
1766            //   GetScalarSlot(sum) + GetScalarPlain(k) + GetHashElem(h) + Add
1767            //     + SetScalarSlotKeep(sum) + Pop
1768            //   → AddHashElemPlainKeyToSlot(sum, k, h)
1769            // Safe because `SetScalarSlotKeep + Pop` leaves nothing on the stack net; the fused
1770            // op is a drop-in for that sequence. No inbound jumps permitted to interior ops.
1771            if len >= 6 {
1772                let mut i = 0;
1773                while i + 5 < len {
1774                    if let (
1775                        Op::GetScalarSlot(sum_slot),
1776                        Op::GetScalarPlain(k_idx),
1777                        Op::GetHashElem(h_idx),
1778                        Op::Add,
1779                        Op::SetScalarSlotKeep(sum_slot2),
1780                        Op::Pop,
1781                    ) = (
1782                        &self.ops[i],
1783                        &self.ops[i + 1],
1784                        &self.ops[i + 2],
1785                        &self.ops[i + 3],
1786                        &self.ops[i + 4],
1787                        &self.ops[i + 5],
1788                    ) {
1789                        if *sum_slot == *sum_slot2
1790                            && (0..6).all(|off| !has_inbound_jump(&self.ops, i + off, usize::MAX))
1791                        {
1792                            let sum_slot = *sum_slot;
1793                            let k_idx = *k_idx;
1794                            let h_idx = *h_idx;
1795                            self.ops[i] = Op::AddHashElemPlainKeyToSlot(sum_slot, k_idx, h_idx);
1796                            for off in 1..=5 {
1797                                self.ops[i + off] = Op::Nop;
1798                            }
1799                            i += 6;
1800                            continue;
1801                        }
1802                    }
1803                    i += 1;
1804                }
1805            }
1806            // 5e-slot: slot-key variant of 5e, emitted when the compiler lowers `$k` (the foreach
1807            // loop variable) into a slot rather than a frame scalar.
1808            //   GetScalarSlot(sum) + GetScalarSlot(k) + GetHashElem(h) + Add
1809            //     + SetScalarSlotKeep(sum) + Pop
1810            //   → AddHashElemSlotKeyToSlot(sum, k, h)
1811            if len >= 6 {
1812                let mut i = 0;
1813                while i + 5 < len {
1814                    if let (
1815                        Op::GetScalarSlot(sum_slot),
1816                        Op::GetScalarSlot(k_slot),
1817                        Op::GetHashElem(h_idx),
1818                        Op::Add,
1819                        Op::SetScalarSlotKeep(sum_slot2),
1820                        Op::Pop,
1821                    ) = (
1822                        &self.ops[i],
1823                        &self.ops[i + 1],
1824                        &self.ops[i + 2],
1825                        &self.ops[i + 3],
1826                        &self.ops[i + 4],
1827                        &self.ops[i + 5],
1828                    ) {
1829                        if *sum_slot == *sum_slot2
1830                            && *sum_slot != *k_slot
1831                            && (0..6).all(|off| !has_inbound_jump(&self.ops, i + off, usize::MAX))
1832                        {
1833                            let sum_slot = *sum_slot;
1834                            let k_slot = *k_slot;
1835                            let h_idx = *h_idx;
1836                            self.ops[i] = Op::AddHashElemSlotKeyToSlot(sum_slot, k_slot, h_idx);
1837                            for off in 1..=5 {
1838                                self.ops[i + off] = Op::Nop;
1839                            }
1840                            i += 6;
1841                            continue;
1842                        }
1843                    }
1844                    i += 1;
1845                }
1846            }
1847            // 5d: counted hash-insert loop `$h{$i} = $i * K`
1848            //   GetScalarSlot(i) + LoadInt(k) + Mul + GetScalarSlot(i) + SetHashElem(h) + Pop
1849            //     + SlotIncLtIntJumpBack(i, limit, body_target)
1850            //   → SetHashIntTimesLoop(h, i, k, limit)
1851            if len >= 7 {
1852                let mut i = 0;
1853                while i + 6 < len {
1854                    if let (
1855                        Op::GetScalarSlot(gs1),
1856                        Op::LoadInt(k),
1857                        Op::Mul,
1858                        Op::GetScalarSlot(gs2),
1859                        Op::SetHashElem(h_idx),
1860                        Op::Pop,
1861                        Op::SlotIncLtIntJumpBack(inc_slot, limit, body_target),
1862                    ) = (
1863                        &self.ops[i],
1864                        &self.ops[i + 1],
1865                        &self.ops[i + 2],
1866                        &self.ops[i + 3],
1867                        &self.ops[i + 4],
1868                        &self.ops[i + 5],
1869                        &self.ops[i + 6],
1870                    ) {
1871                        if *gs1 == *inc_slot
1872                            && *gs2 == *inc_slot
1873                            && *body_target == i
1874                            && i32::try_from(*k).is_ok()
1875                            && (0..6).all(|off| !has_inbound_jump(&self.ops, i + off, i + 6))
1876                            && !has_inbound_jump(&self.ops, i + 6, i + 6)
1877                        {
1878                            let h_idx = *h_idx;
1879                            let inc_slot = *inc_slot;
1880                            let k32 = *k as i32;
1881                            let limit = *limit;
1882                            self.ops[i] = Op::SetHashIntTimesLoop(h_idx, inc_slot, k32, limit);
1883                            for off in 1..=6 {
1884                                self.ops[i + off] = Op::Nop;
1885                            }
1886                            i += 7;
1887                            continue;
1888                        }
1889                    }
1890                    i += 1;
1891                }
1892            }
1893            // 5c: GetScalarSlot + PushArray + ArrayLen + Pop + SlotIncLtIntJumpBack
1894            //      → PushIntRangeToArrayLoop
1895            // This is the compiler's `push @a, $i; $i++` shape in void context, where
1896            // the `push` expression's length return is pushed by `ArrayLen` and then `Pop`ped.
1897            if len >= 5 {
1898                let mut i = 0;
1899                while i + 4 < len {
1900                    if let (
1901                        Op::GetScalarSlot(get_slot),
1902                        Op::PushArray(push_idx),
1903                        Op::ArrayLen(len_idx),
1904                        Op::Pop,
1905                        Op::SlotIncLtIntJumpBack(inc_slot, limit, body_target),
1906                    ) = (
1907                        &self.ops[i],
1908                        &self.ops[i + 1],
1909                        &self.ops[i + 2],
1910                        &self.ops[i + 3],
1911                        &self.ops[i + 4],
1912                    ) {
1913                        if *get_slot == *inc_slot
1914                            && *push_idx == *len_idx
1915                            && *body_target == i
1916                            && !has_inbound_jump(&self.ops, i, i + 4)
1917                            && !has_inbound_jump(&self.ops, i + 1, i + 4)
1918                            && !has_inbound_jump(&self.ops, i + 2, i + 4)
1919                            && !has_inbound_jump(&self.ops, i + 3, i + 4)
1920                            && !has_inbound_jump(&self.ops, i + 4, i + 4)
1921                        {
1922                            let push_idx = *push_idx;
1923                            let inc_slot = *inc_slot;
1924                            let limit = *limit;
1925                            self.ops[i] = Op::PushIntRangeToArrayLoop(push_idx, inc_slot, limit);
1926                            self.ops[i + 1] = Op::Nop;
1927                            self.ops[i + 2] = Op::Nop;
1928                            self.ops[i + 3] = Op::Nop;
1929                            self.ops[i + 4] = Op::Nop;
1930                            i += 5;
1931                            continue;
1932                        }
1933                    }
1934                    i += 1;
1935                }
1936            }
1937        }
1938        // Pass 6: compact — remove the Nops pass 5 introduced.
1939        self.compact_nops();
1940        // Pass 7: fuse the entire `for my $k (keys %h) { $sum += $h{$k} }` loop into a single
1941        // `SumHashValuesToSlot` op that walks the hash's values in a tight native loop.
1942        //
1943        // After prior passes and compaction the shape is a 15-op block:
1944        //
1945        //     HashKeys(h)
1946        //     DeclareArray(list)
1947        //     LoadInt(0)
1948        //     DeclareScalarSlot(c, cname)
1949        //     LoadUndef
1950        //     DeclareScalarSlot(v, vname)
1951        //     [top]  GetScalarSlot(c)
1952        //            ArrayLen(list)
1953        //            NumLt
1954        //            JumpIfFalse(end)
1955        //            GetScalarSlot(c)
1956        //            GetArrayElem(list)
1957        //            SetScalarSlot(v)
1958        //            AddHashElemSlotKeyToSlot(sum, v, h)     ← fused body (pass 5e-slot)
1959        //            PreIncSlotVoid(c)
1960        //            Jump(top)
1961        //     [end]
1962        //
1963        // The counter (`__foreach_i__`), list (`__foreach_list__`), and loop var (`$k`) live
1964        // inside a `PushFrame`-isolated scope and are invisible after the loop — it is safe to
1965        // elide all of them. The fused op accumulates directly into `sum` without creating the
1966        // keys array at all.
1967        //
1968        // Safety gates:
1969        //   - `h` in HashKeys must match `h` in AddHashElemSlotKeyToSlot.
1970        //   - `list` in DeclareArray must match the loop `ArrayLen` / `GetArrayElem`.
1971        //   - `c` / `v` slots must be consistent throughout.
1972        //   - No inbound jump lands inside the 15-op window from the outside.
1973        //   - JumpIfFalse target must be i+15 (just past the Jump back-edge).
1974        //   - Jump back-edge target must be i+6 (the GetScalarSlot(c) at loop top).
1975        let len = self.ops.len();
1976        if len >= 15 {
1977            let has_inbound_jump =
1978                |ops: &[Op], pos: usize, ignore_from: usize, ignore_to: usize| -> bool {
1979                    for (j, op) in ops.iter().enumerate() {
1980                        if j >= ignore_from && j <= ignore_to {
1981                            continue;
1982                        }
1983                        let t = match op {
1984                            Op::Jump(t)
1985                            | Op::JumpIfFalse(t)
1986                            | Op::JumpIfTrue(t)
1987                            | Op::JumpIfFalseKeep(t)
1988                            | Op::JumpIfTrueKeep(t)
1989                            | Op::JumpIfDefinedKeep(t) => *t,
1990                            Op::SlotLtIntJumpIfFalse(_, _, t) => *t,
1991                            Op::SlotIncLtIntJumpBack(_, _, t) => *t,
1992                            _ => continue,
1993                        };
1994                        if t == pos {
1995                            return true;
1996                        }
1997                    }
1998                    false
1999                };
2000            let mut i = 0;
2001            while i + 15 < len {
2002                if let (
2003                    Op::HashKeys(h_idx),
2004                    Op::DeclareArray(list_idx),
2005                    Op::LoadInt(0),
2006                    Op::DeclareScalarSlot(c_slot, _c_name),
2007                    Op::LoadUndef,
2008                    Op::DeclareScalarSlot(v_slot, _v_name),
2009                    Op::GetScalarSlot(c_get1),
2010                    Op::ArrayLen(len_idx),
2011                    Op::NumLt,
2012                    Op::JumpIfFalse(end_tgt),
2013                    Op::GetScalarSlot(c_get2),
2014                    Op::GetArrayElem(elem_idx),
2015                    Op::SetScalarSlot(v_set),
2016                    Op::AddHashElemSlotKeyToSlot(sum_slot, v_in_body, h_in_body),
2017                    Op::PreIncSlotVoid(c_inc),
2018                    Op::Jump(top_tgt),
2019                ) = (
2020                    &self.ops[i],
2021                    &self.ops[i + 1],
2022                    &self.ops[i + 2],
2023                    &self.ops[i + 3],
2024                    &self.ops[i + 4],
2025                    &self.ops[i + 5],
2026                    &self.ops[i + 6],
2027                    &self.ops[i + 7],
2028                    &self.ops[i + 8],
2029                    &self.ops[i + 9],
2030                    &self.ops[i + 10],
2031                    &self.ops[i + 11],
2032                    &self.ops[i + 12],
2033                    &self.ops[i + 13],
2034                    &self.ops[i + 14],
2035                    &self.ops[i + 15],
2036                ) {
2037                    let full_end = i + 15;
2038                    if *list_idx == *len_idx
2039                        && *list_idx == *elem_idx
2040                        && *c_slot == *c_get1
2041                        && *c_slot == *c_get2
2042                        && *c_slot == *c_inc
2043                        && *v_slot == *v_set
2044                        && *v_slot == *v_in_body
2045                        && *h_idx == *h_in_body
2046                        && *top_tgt == i + 6
2047                        && *end_tgt == i + 16
2048                        && *sum_slot != *c_slot
2049                        && *sum_slot != *v_slot
2050                        && !(i..=full_end).any(|k| has_inbound_jump(&self.ops, k, i, full_end))
2051                    {
2052                        let sum_slot = *sum_slot;
2053                        let h_idx = *h_idx;
2054                        self.ops[i] = Op::SumHashValuesToSlot(sum_slot, h_idx);
2055                        for off in 1..=15 {
2056                            self.ops[i + off] = Op::Nop;
2057                        }
2058                        i += 16;
2059                        continue;
2060                    }
2061                }
2062                i += 1;
2063            }
2064        }
2065        // Pass 8: compact pass 7's Nops.
2066        self.compact_nops();
2067    }
2068
2069    /// Remove all `Nop` instructions and remap jump targets + metadata indices.
2070    fn compact_nops(&mut self) {
2071        let old_len = self.ops.len();
2072        // Build old→new index mapping.
2073        let mut remap = vec![0usize; old_len + 1];
2074        let mut new_idx = 0usize;
2075        for (old, slot) in remap[..old_len].iter_mut().enumerate() {
2076            *slot = new_idx;
2077            if !matches!(self.ops[old], Op::Nop) {
2078                new_idx += 1;
2079            }
2080        }
2081        remap[old_len] = new_idx;
2082        if new_idx == old_len {
2083            return; // nothing to compact
2084        }
2085        // Remap jump targets in all ops.
2086        for op in &mut self.ops {
2087            match op {
2088                Op::Jump(t) | Op::JumpIfFalse(t) | Op::JumpIfTrue(t) => *t = remap[*t],
2089                Op::JumpIfFalseKeep(t) | Op::JumpIfTrueKeep(t) | Op::JumpIfDefinedKeep(t) => {
2090                    *t = remap[*t]
2091                }
2092                Op::SlotLtIntJumpIfFalse(_, _, t) => *t = remap[*t],
2093                Op::SlotIncLtIntJumpBack(_, _, t) => *t = remap[*t],
2094                _ => {}
2095            }
2096        }
2097        // Remap sub entry points.
2098        for e in &mut self.sub_entries {
2099            e.1 = remap[e.1];
2100        }
2101        // Remap `CallStaticSubId` resolved entry IPs — they were recorded by
2102        // `patch_static_sub_calls` before peephole fusion ran, so any Nop
2103        // removal in front of a sub body shifts its entry and must be
2104        // reflected here; otherwise `vm_dispatch_user_call` jumps one (or
2105        // more) ops past the real sub start and silently skips the first
2106        // instruction(s) of the body.
2107        for c in &mut self.static_sub_calls {
2108            c.0 = remap[c.0];
2109        }
2110        // Remap block/grep/sort/etc bytecode ranges.
2111        fn remap_ranges(ranges: &mut [Option<(usize, usize)>], remap: &[usize]) {
2112            for r in ranges.iter_mut().flatten() {
2113                r.0 = remap[r.0];
2114                r.1 = remap[r.1];
2115            }
2116        }
2117        remap_ranges(&mut self.block_bytecode_ranges, &remap);
2118        remap_ranges(&mut self.map_expr_bytecode_ranges, &remap);
2119        remap_ranges(&mut self.grep_expr_bytecode_ranges, &remap);
2120        remap_ranges(&mut self.keys_expr_bytecode_ranges, &remap);
2121        remap_ranges(&mut self.values_expr_bytecode_ranges, &remap);
2122        remap_ranges(&mut self.eval_timeout_expr_bytecode_ranges, &remap);
2123        remap_ranges(&mut self.given_topic_bytecode_ranges, &remap);
2124        remap_ranges(&mut self.algebraic_match_subject_bytecode_ranges, &remap);
2125        remap_ranges(&mut self.regex_flip_flop_rhs_expr_bytecode_ranges, &remap);
2126        // Compact ops, lines, op_ast_expr.
2127        let mut j = 0;
2128        for old in 0..old_len {
2129            if !matches!(self.ops[old], Op::Nop) {
2130                self.ops[j] = self.ops[old].clone();
2131                if old < self.lines.len() && j < self.lines.len() {
2132                    self.lines[j] = self.lines[old];
2133                }
2134                if old < self.op_ast_expr.len() && j < self.op_ast_expr.len() {
2135                    self.op_ast_expr[j] = self.op_ast_expr[old];
2136                }
2137                j += 1;
2138            }
2139        }
2140        self.ops.truncate(j);
2141        self.lines.truncate(j);
2142        self.op_ast_expr.truncate(j);
2143    }
2144}
2145
2146impl Default for Chunk {
2147    fn default() -> Self {
2148        Self::new()
2149    }
2150}
2151
2152#[cfg(test)]
2153mod tests {
2154    use super::*;
2155    use crate::ast;
2156
2157    #[test]
2158    fn chunk_new_and_default_match() {
2159        let a = Chunk::new();
2160        let b = Chunk::default();
2161        assert!(a.ops.is_empty() && a.names.is_empty() && a.constants.is_empty());
2162        assert!(b.ops.is_empty() && b.lines.is_empty());
2163    }
2164
2165    #[test]
2166    fn intern_name_deduplicates() {
2167        let mut c = Chunk::new();
2168        let i0 = c.intern_name("foo");
2169        let i1 = c.intern_name("foo");
2170        let i2 = c.intern_name("bar");
2171        assert_eq!(i0, i1);
2172        assert_ne!(i0, i2);
2173        assert_eq!(c.names.len(), 2);
2174    }
2175
2176    #[test]
2177    fn add_constant_dedups_identical_strings() {
2178        let mut c = Chunk::new();
2179        let a = c.add_constant(PerlValue::string("x".into()));
2180        let b = c.add_constant(PerlValue::string("x".into()));
2181        assert_eq!(a, b);
2182        assert_eq!(c.constants.len(), 1);
2183    }
2184
2185    #[test]
2186    fn add_constant_distinct_strings_different_indices() {
2187        let mut c = Chunk::new();
2188        let a = c.add_constant(PerlValue::string("a".into()));
2189        let b = c.add_constant(PerlValue::string("b".into()));
2190        assert_ne!(a, b);
2191        assert_eq!(c.constants.len(), 2);
2192    }
2193
2194    #[test]
2195    fn add_constant_non_string_no_dedup_scan() {
2196        let mut c = Chunk::new();
2197        let a = c.add_constant(PerlValue::integer(1));
2198        let b = c.add_constant(PerlValue::integer(1));
2199        assert_ne!(a, b);
2200        assert_eq!(c.constants.len(), 2);
2201    }
2202
2203    #[test]
2204    fn emit_records_parallel_ops_and_lines() {
2205        let mut c = Chunk::new();
2206        c.emit(Op::LoadInt(1), 10);
2207        c.emit(Op::Pop, 11);
2208        assert_eq!(c.len(), 2);
2209        assert_eq!(c.lines, vec![10, 11]);
2210        assert_eq!(c.op_ast_expr, vec![None, None]);
2211        assert!(!c.is_empty());
2212    }
2213
2214    #[test]
2215    fn len_is_empty_track_ops() {
2216        let mut c = Chunk::new();
2217        assert!(c.is_empty());
2218        assert_eq!(c.len(), 0);
2219        c.emit(Op::Halt, 0);
2220        assert!(!c.is_empty());
2221        assert_eq!(c.len(), 1);
2222    }
2223
2224    #[test]
2225    fn patch_jump_here_updates_jump_target() {
2226        let mut c = Chunk::new();
2227        let j = c.emit(Op::Jump(0), 1);
2228        c.emit(Op::LoadInt(99), 2);
2229        c.patch_jump_here(j);
2230        assert_eq!(c.ops.len(), 2);
2231        assert!(matches!(c.ops[j], Op::Jump(2)));
2232    }
2233
2234    #[test]
2235    fn patch_jump_here_jump_if_true() {
2236        let mut c = Chunk::new();
2237        let j = c.emit(Op::JumpIfTrue(0), 1);
2238        c.emit(Op::Halt, 2);
2239        c.patch_jump_here(j);
2240        assert!(matches!(c.ops[j], Op::JumpIfTrue(2)));
2241    }
2242
2243    #[test]
2244    fn patch_jump_here_jump_if_false_keep() {
2245        let mut c = Chunk::new();
2246        let j = c.emit(Op::JumpIfFalseKeep(0), 1);
2247        c.emit(Op::Pop, 2);
2248        c.patch_jump_here(j);
2249        assert!(matches!(c.ops[j], Op::JumpIfFalseKeep(2)));
2250    }
2251
2252    #[test]
2253    fn patch_jump_here_jump_if_true_keep() {
2254        let mut c = Chunk::new();
2255        let j = c.emit(Op::JumpIfTrueKeep(0), 1);
2256        c.emit(Op::Pop, 2);
2257        c.patch_jump_here(j);
2258        assert!(matches!(c.ops[j], Op::JumpIfTrueKeep(2)));
2259    }
2260
2261    #[test]
2262    fn patch_jump_here_jump_if_defined_keep() {
2263        let mut c = Chunk::new();
2264        let j = c.emit(Op::JumpIfDefinedKeep(0), 1);
2265        c.emit(Op::Halt, 2);
2266        c.patch_jump_here(j);
2267        assert!(matches!(c.ops[j], Op::JumpIfDefinedKeep(2)));
2268    }
2269
2270    #[test]
2271    #[should_panic(expected = "patch_jump_to on non-jump op")]
2272    fn patch_jump_here_panics_on_non_jump() {
2273        let mut c = Chunk::new();
2274        let idx = c.emit(Op::LoadInt(1), 1);
2275        c.patch_jump_here(idx);
2276    }
2277
2278    #[test]
2279    fn add_block_returns_sequential_indices() {
2280        let mut c = Chunk::new();
2281        let b0: ast::Block = vec![];
2282        let b1: ast::Block = vec![];
2283        assert_eq!(c.add_block(b0), 0);
2284        assert_eq!(c.add_block(b1), 1);
2285        assert_eq!(c.blocks.len(), 2);
2286    }
2287
2288    #[test]
2289    fn builtin_id_from_u16_first_and_last() {
2290        assert_eq!(BuiltinId::from_u16(0), Some(BuiltinId::Length));
2291        assert_eq!(
2292            BuiltinId::from_u16(BuiltinId::Pselect as u16),
2293            Some(BuiltinId::Pselect)
2294        );
2295        assert_eq!(
2296            BuiltinId::from_u16(BuiltinId::BarrierNew as u16),
2297            Some(BuiltinId::BarrierNew)
2298        );
2299        assert_eq!(
2300            BuiltinId::from_u16(BuiltinId::ParPipeline as u16),
2301            Some(BuiltinId::ParPipeline)
2302        );
2303        assert_eq!(
2304            BuiltinId::from_u16(BuiltinId::GlobParProgress as u16),
2305            Some(BuiltinId::GlobParProgress)
2306        );
2307        assert_eq!(
2308            BuiltinId::from_u16(BuiltinId::Readpipe as u16),
2309            Some(BuiltinId::Readpipe)
2310        );
2311        assert_eq!(
2312            BuiltinId::from_u16(BuiltinId::ReadLineList as u16),
2313            Some(BuiltinId::ReadLineList)
2314        );
2315        assert_eq!(
2316            BuiltinId::from_u16(BuiltinId::ReaddirList as u16),
2317            Some(BuiltinId::ReaddirList)
2318        );
2319        assert_eq!(
2320            BuiltinId::from_u16(BuiltinId::Ssh as u16),
2321            Some(BuiltinId::Ssh)
2322        );
2323        assert_eq!(
2324            BuiltinId::from_u16(BuiltinId::Pipe as u16),
2325            Some(BuiltinId::Pipe)
2326        );
2327        assert_eq!(
2328            BuiltinId::from_u16(BuiltinId::Files as u16),
2329            Some(BuiltinId::Files)
2330        );
2331        assert_eq!(
2332            BuiltinId::from_u16(BuiltinId::Filesf as u16),
2333            Some(BuiltinId::Filesf)
2334        );
2335        assert_eq!(
2336            BuiltinId::from_u16(BuiltinId::Dirs as u16),
2337            Some(BuiltinId::Dirs)
2338        );
2339        assert_eq!(
2340            BuiltinId::from_u16(BuiltinId::SymLinks as u16),
2341            Some(BuiltinId::SymLinks)
2342        );
2343        assert_eq!(
2344            BuiltinId::from_u16(BuiltinId::Sockets as u16),
2345            Some(BuiltinId::Sockets)
2346        );
2347        assert_eq!(
2348            BuiltinId::from_u16(BuiltinId::Pipes as u16),
2349            Some(BuiltinId::Pipes)
2350        );
2351        assert_eq!(
2352            BuiltinId::from_u16(BuiltinId::BlockDevices as u16),
2353            Some(BuiltinId::BlockDevices)
2354        );
2355        assert_eq!(
2356            BuiltinId::from_u16(BuiltinId::CharDevices as u16),
2357            Some(BuiltinId::CharDevices)
2358        );
2359        assert_eq!(
2360            BuiltinId::from_u16(BuiltinId::Executables as u16),
2361            Some(BuiltinId::Executables)
2362        );
2363    }
2364
2365    #[test]
2366    fn builtin_id_from_u16_out_of_range() {
2367        assert_eq!(BuiltinId::from_u16(BuiltinId::Executables as u16 + 1), None);
2368        assert_eq!(BuiltinId::from_u16(u16::MAX), None);
2369    }
2370
2371    #[test]
2372    fn op_enum_clone_roundtrip() {
2373        let o = Op::Call(42, 3, 0);
2374        assert!(matches!(o.clone(), Op::Call(42, 3, 0)));
2375    }
2376
2377    #[test]
2378    fn chunk_clone_independent_ops() {
2379        let mut c = Chunk::new();
2380        c.emit(Op::Negate, 1);
2381        let mut d = c.clone();
2382        d.emit(Op::Pop, 2);
2383        assert_eq!(c.len(), 1);
2384        assert_eq!(d.len(), 2);
2385    }
2386
2387    #[test]
2388    fn chunk_disassemble_includes_ops() {
2389        let mut c = Chunk::new();
2390        c.emit(Op::LoadInt(7), 1);
2391        let s = c.disassemble();
2392        assert!(s.contains("0000"));
2393        assert!(s.contains("LoadInt(7)"));
2394        assert!(s.contains("     -")); // no ast ref column
2395    }
2396
2397    #[test]
2398    fn ast_expr_at_roundtrips_pooled_expr() {
2399        let mut c = Chunk::new();
2400        let e = ast::Expr {
2401            kind: ast::ExprKind::Integer(99),
2402            line: 3,
2403        };
2404        c.ast_expr_pool.push(e);
2405        c.emit_with_ast_idx(Op::LoadInt(99), 3, Some(0));
2406        let got = c.ast_expr_at(0).expect("ast ref");
2407        assert!(matches!(&got.kind, ast::ExprKind::Integer(99)));
2408        assert_eq!(got.line, 3);
2409    }
2410}