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