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