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