Skip to main content

stryke/
bytecode.rs

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