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