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 /// Register [`RuntimeSubDecl`] at index (nested `sub`, including inside `BEGIN`).
812 RuntimeSubDecl(u16),
813 /// Register [`RuntimeAdviceDecl`] at index — install AOP advice into VM `intercepts` registry.
814 RegisterAdvice(u16),
815 /// `tie $x | @arr | %h, 'Class', ...` — stack bottom = class expr, then user args; `argc` = `1 + args.len()`.
816 /// `target_kind`: 0 = scalar (`TIESCALAR`), 1 = array (`TIEARRAY`), 2 = hash (`TIEHASH`). `name_idx` = bare name.
817 Tie {
818 target_kind: u8,
819 name_idx: u16,
820 argc: u8,
821 },
822 /// `format NAME =` … — index into [`Chunk::format_decls`]; installs into current package at run time.
823 FormatDecl(u16),
824 /// `use overload 'op' => 'method', …` — index into [`Chunk::use_overload_entries`].
825 UseOverload(u16),
826 /// Scalar `$x OP= $rhs` — uses [`Scope::atomic_mutate`] so `mysync` scalars are RMW-safe.
827 /// Stack: `[rhs]` → `[result]`. `op` byte is from [`crate::compiler::scalar_compound_op_to_byte`].
828 ScalarCompoundAssign {
829 name_idx: u16,
830 op: u8,
831 },
832
833 // ── Special ──
834 /// Set `${^GLOBAL_PHASE}` on the interpreter. See [`GP_START`] … [`GP_END`].
835 SetGlobalPhase(u8),
836 Halt,
837 /// Delegate an AST expression to `Interpreter::eval_expr_ctx` at runtime.
838 /// Operand is an index into [`Chunk::ast_eval_exprs`].
839 EvalAstExpr(u16),
840
841 // ── Streaming map (appended — do not reorder earlier op tags) ─────────────
842 /// `maps { BLOCK } LIST` — stack: \[list\] → lazy iterator (pull-based; stryke extension).
843 MapsWithBlock(u16),
844 /// `flat_maps { BLOCK } LIST` — like [`Op::MapsWithBlock`] with `flat_map`-style flattening.
845 MapsFlatMapWithBlock(u16),
846 /// `maps EXPR, LIST` — index into [`Chunk::map_expr_entries`]; stack: \[list\] → iterator.
847 MapsWithExpr(u16),
848 /// `flat_maps EXPR, LIST` — same pools as [`Op::MapsWithExpr`].
849 MapsFlatMapWithExpr(u16),
850 /// `filter` / `fi` `{ BLOCK } LIST` — stack: \[list\] → lazy iterator (stryke; `grep` remains eager).
851 FilterWithBlock(u16),
852 /// `filter` / `fi` `EXPR, LIST` — index into [`Chunk::grep_expr_entries`]; stack: \[list\] → iterator.
853 FilterWithExpr(u16),
854}
855
856/// `${^GLOBAL_PHASE}` values emitted with [`Op::SetGlobalPhase`] (matches Perl’s phase strings).
857pub const GP_START: u8 = 0;
858/// Reserved; stock Perl 5 keeps `${^GLOBAL_PHASE}` as **`START`** during `UNITCHECK` blocks.
859pub const GP_UNITCHECK: u8 = 1;
860pub const GP_CHECK: u8 = 2;
861pub const GP_INIT: u8 = 3;
862pub const GP_RUN: u8 = 4;
863pub const GP_END: u8 = 5;
864
865/// Built-in function IDs for CallBuiltin dispatch.
866#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
867#[repr(u16)]
868pub enum BuiltinId {
869 // String
870 Length = 0,
871 Chomp,
872 Chop,
873 Substr,
874 Index,
875 Rindex,
876 Uc,
877 Lc,
878 Ucfirst,
879 Lcfirst,
880 Chr,
881 Ord,
882 Hex,
883 Oct,
884 Join,
885 Split,
886 Sprintf,
887
888 // Numeric
889 Abs,
890 Int,
891 Sqrt,
892
893 // Type
894 Defined,
895 Ref,
896 Scalar,
897
898 // Array
899 Splice,
900 Reverse,
901 Sort,
902 Unshift,
903
904 // Hash
905
906 // I/O
907 Open,
908 Close,
909 Eof,
910 ReadLine,
911 Printf,
912
913 // System
914 System,
915 Exec,
916 Exit,
917 Die,
918 Warn,
919 Chdir,
920 Mkdir,
921 Unlink,
922
923 // Control
924 Eval,
925 Do,
926 Require,
927
928 // OOP
929 Bless,
930 Caller,
931
932 // Parallel
933 PMap,
934 PGrep,
935 PFor,
936 PSort,
937 Fan,
938
939 // Map/Grep (block-based — need special handling)
940 MapBlock,
941 GrepBlock,
942 SortBlock,
943
944 // Math (appended — do not reorder earlier IDs)
945 Sin,
946 Cos,
947 Atan2,
948 Exp,
949 Log,
950 Rand,
951 Srand,
952
953 // String (appended)
954 Crypt,
955 Fc,
956 Pos,
957 Study,
958
959 Stat,
960 Lstat,
961 Link,
962 Symlink,
963 Readlink,
964 Glob,
965
966 Opendir,
967 Readdir,
968 Closedir,
969 Rewinddir,
970 Telldir,
971 Seekdir,
972 /// Read entire file as UTF-8 (`slurp $path`).
973 Slurp,
974 /// Blocking HTTP GET (`fetch_url $url`).
975 FetchUrl,
976 /// `pchannel()` — `(tx, rx)` as a two-element list.
977 Pchannel,
978 /// Parallel recursive glob (`glob_par`).
979 GlobPar,
980 /// `deque()` — empty deque.
981 DequeNew,
982 /// `heap(fn { })` — empty heap with comparator.
983 HeapNew,
984 /// `pipeline(...)` — lazy iterator (filter/map/take/collect).
985 Pipeline,
986 /// `capture("cmd")` — structured stdout/stderr/exit (via `sh -c`).
987 Capture,
988 /// `ppool(N)` — persistent thread pool (`submit` / `collect`).
989 Ppool,
990 /// Scalar/list context query (`wantarray`).
991 Wantarray,
992 /// `rename OLD, NEW`
993 Rename,
994 /// `chmod MODE, ...`
995 Chmod,
996 /// `chown UID, GID, ...`
997 Chown,
998 /// `pselect($rx1, $rx2, ...)` — multiplexed recv; returns `(value, index)`.
999 Pselect,
1000 /// `barrier(N)` — thread barrier (`->wait`).
1001 BarrierNew,
1002 /// `par_pipeline(...)` — list form: same as `pipeline` but parallel `filter`/`map` on `collect()`.
1003 ParPipeline,
1004 /// `glob_par(..., progress => EXPR)` — last stack arg is truthy progress flag.
1005 GlobParProgress,
1006 /// `par_pipeline_stream(...)` — streaming pipeline with bounded channels between stages.
1007 ParPipelineStream,
1008 /// `par_sed(PATTERN, REPLACEMENT, FILES...)` — parallel in-place regex substitution per file.
1009 ParSed,
1010 /// `par_sed(..., progress => EXPR)` — last stack arg is truthy progress flag.
1011 ParSedProgress,
1012 /// `each EXPR` — returns empty list.
1013 Each,
1014 /// `` `cmd` `` / `qx{...}` — stdout string via `sh -c` (Perl readpipe); sets `$?`.
1015 Readpipe,
1016 /// `readline` / `<HANDLE>` in **list** context — all remaining lines until EOF (Perl `readline` list semantics).
1017 ReadLineList,
1018 /// `readdir` in **list** context — all names not yet returned (Perl drains the rest of the stream).
1019 ReaddirList,
1020 /// `ssh HOST, CMD, …` / `ssh(HOST, …)` — `execvp` style `ssh` only (no shell).
1021 Ssh,
1022 /// `rmdir LIST` — remove empty directories; returns count removed (appended ID).
1023 Rmdir,
1024 /// `utime ATIME, MTIME, LIST` — set access/mod times (Unix).
1025 Utime,
1026 /// `umask EXPR` / `umask()` — process file mode creation mask (Unix).
1027 Umask,
1028 /// `getcwd` / `pwd` — bare-name builtin returning the absolute current working directory.
1029 Getcwd,
1030 /// `pipe READHANDLE, WRITEHANDLE` — OS pipe ends (Unix).
1031 Pipe,
1032 /// `files` / `files DIR` — list file names in a directory (default: `.`).
1033 Files,
1034 /// `filesf` / `filesf DIR` / `f` — list only regular file names in a directory (default: `.`).
1035 Filesf,
1036 /// `fr DIR` — list only regular file names recursively (default: `.`).
1037 FilesfRecursive,
1038 /// `dirs` / `dirs DIR` / `d` — list subdirectory names in a directory (default: `.`).
1039 Dirs,
1040 /// `dr DIR` — list subdirectory paths recursively (default: `.`).
1041 DirsRecursive,
1042 /// `sym_links` / `sym_links DIR` — list symlink names in a directory (default: `.`).
1043 SymLinks,
1044 /// `sockets` / `sockets DIR` — list Unix socket names in a directory (default: `.`).
1045 Sockets,
1046 /// `pipes` / `pipes DIR` — list named-pipe (FIFO) names in a directory (default: `.`).
1047 Pipes,
1048 /// `block_devices` / `block_devices DIR` — list block device names in a directory (default: `.`).
1049 BlockDevices,
1050 /// `char_devices` / `char_devices DIR` — list character device names in a directory (default: `.`).
1051 CharDevices,
1052 /// `exe` / `exe DIR` — list executable file names in a directory (default: `.`).
1053 Executables,
1054}
1055
1056impl BuiltinId {
1057 pub fn from_u16(v: u16) -> Option<Self> {
1058 if v <= Self::Executables as u16 {
1059 Some(unsafe { std::mem::transmute::<u16, BuiltinId>(v) })
1060 } else {
1061 None
1062 }
1063 }
1064}
1065
1066/// A compiled chunk of bytecode with its constant pools.
1067#[derive(Debug, Clone, Serialize, Deserialize)]
1068pub struct Chunk {
1069 pub ops: Vec<Op>,
1070 /// Constant pool: string literals, regex patterns, etc.
1071 #[serde(with = "crate::script_cache::constants_pool_codec")]
1072 pub constants: Vec<PerlValue>,
1073 /// Name pool: variable names, sub names (interned/deduped).
1074 pub names: Vec<String>,
1075 /// Source line for each op (parallel array for error reporting).
1076 pub lines: Vec<usize>,
1077 /// Optional link from each op to the originating [`Expr`] (pool index into [`Self::ast_expr_pool`]).
1078 /// Filled for ops emitted from [`crate::compiler::Compiler::compile_expr_ctx`]; other paths leave `None`.
1079 pub op_ast_expr: Vec<Option<u32>>,
1080 /// Interned [`Expr`] nodes referenced by [`Self::op_ast_expr`] (for debugging / tooling).
1081 pub ast_expr_pool: Vec<Expr>,
1082 /// Compiled subroutine entry points: (name_index, op_index, uses_stack_args).
1083 /// When `uses_stack_args` is true, the Call op leaves arguments on the value
1084 /// stack and the sub reads them via `GetArg(idx)` instead of `shift @_`.
1085 pub sub_entries: Vec<(u16, usize, bool)>,
1086 /// AST blocks for map/grep/sort/parallel operations.
1087 /// Referenced by block-based opcodes via u16 index.
1088 pub blocks: Vec<Block>,
1089 /// When `Some((start, end))`, `blocks[i]` is also lowered to `ops[start..end]` (exclusive `end`)
1090 /// with trailing [`Op::BlockReturnValue`]. VM uses opcodes; otherwise the AST in `blocks[i]`.
1091 pub block_bytecode_ranges: Vec<Option<(usize, usize)>>,
1092 /// Resolved [`Op::CallStaticSubId`] targets: subroutine entry IP, stack-args calling convention,
1093 /// and stash name pool index (qualified key matching [`Interpreter::subs`]).
1094 pub static_sub_calls: Vec<(usize, bool, u16)>,
1095 /// Assign targets for `s///` / `tr///` bytecode (LHS expressions).
1096 pub lvalues: Vec<Expr>,
1097 /// AST expressions delegated to interpreter at runtime via [`Op::EvalAstExpr`].
1098 pub ast_eval_exprs: Vec<Expr>,
1099 /// Instruction pointer where the main program body starts (after BEGIN/CHECK/INIT phase blocks).
1100 /// Used by `-n`/`-p` line mode to re-execute only the body per input line.
1101 pub body_start_ip: usize,
1102 /// `struct Name { ... }` definitions in this chunk (registered on the interpreter at VM start).
1103 pub struct_defs: Vec<StructDef>,
1104 /// `enum Name { ... }` definitions in this chunk (registered on the interpreter at VM start).
1105 pub enum_defs: Vec<EnumDef>,
1106 /// `class Name extends ... impl ... { ... }` definitions.
1107 pub class_defs: Vec<ClassDef>,
1108 /// `trait Name { ... }` definitions.
1109 pub trait_defs: Vec<TraitDef>,
1110 /// `given (topic) { body }` — topic expression + body (when/default handled by interpreter).
1111 pub given_entries: Vec<(Expr, Block)>,
1112 /// When `Some((start, end))`, `given_entries[i].0` (topic) is lowered to `ops[start..end]` +
1113 /// [`Op::BlockReturnValue`].
1114 pub given_topic_bytecode_ranges: Vec<Option<(usize, usize)>>,
1115 /// `eval_timeout timeout_expr { body }` — evaluated at runtime.
1116 pub eval_timeout_entries: Vec<(Expr, Block)>,
1117 /// When `Some((start, end))`, `eval_timeout_entries[i].0` (timeout expr) is lowered to
1118 /// `ops[start..end]` with trailing [`Op::BlockReturnValue`].
1119 pub eval_timeout_expr_bytecode_ranges: Vec<Option<(usize, usize)>>,
1120 /// Algebraic `match (subject) { arms }`.
1121 pub algebraic_match_entries: Vec<(Expr, Vec<MatchArm>)>,
1122 /// When `Some((start, end))`, `algebraic_match_entries[i].0` (subject) is lowered to
1123 /// `ops[start..end]` + [`Op::BlockReturnValue`].
1124 pub algebraic_match_subject_bytecode_ranges: Vec<Option<(usize, usize)>>,
1125 /// Nested / runtime `sub` declarations (see [`Op::RuntimeSubDecl`]).
1126 pub runtime_sub_decls: Vec<RuntimeSubDecl>,
1127 /// AOP advice declarations (see [`Op::RegisterAdvice`]).
1128 pub runtime_advice_decls: Vec<RuntimeAdviceDecl>,
1129 /// Stryke `fn ($a, …)` / hash-destruct params for [`Op::MakeCodeRef`] (second operand is pool index).
1130 pub code_ref_sigs: Vec<Vec<SubSigParam>>,
1131 /// `par_lines PATH, fn { } [, progress => EXPR]` — evaluated by interpreter inside VM.
1132 pub par_lines_entries: Vec<(Expr, Expr, Option<Expr>)>,
1133 /// `par_walk PATH, fn { } [, progress => EXPR]` — evaluated by interpreter inside VM.
1134 pub par_walk_entries: Vec<(Expr, Expr, Option<Expr>)>,
1135 /// `pwatch GLOB, fn { }` — evaluated by interpreter inside VM.
1136 pub pwatch_entries: Vec<(Expr, Expr)>,
1137 /// `substr $var, OFF, LEN, REPL` — four-arg form (mutates `LHS`); evaluated by interpreter inside VM.
1138 pub substr_four_arg_entries: Vec<(Expr, Expr, Option<Expr>, Expr)>,
1139 /// `keys EXPR` when `EXPR` is not bare `%h`.
1140 pub keys_expr_entries: Vec<Expr>,
1141 /// When `Some((start, end))`, `keys_expr_entries[i]` is lowered to `ops[start..end]` +
1142 /// [`Op::BlockReturnValue`] (operand only; [`Op::KeysExpr`] still applies `keys` to the value).
1143 pub keys_expr_bytecode_ranges: Vec<Option<(usize, usize)>>,
1144 /// `values EXPR` when not bare `%h`.
1145 pub values_expr_entries: Vec<Expr>,
1146 pub values_expr_bytecode_ranges: Vec<Option<(usize, usize)>>,
1147 /// `delete EXPR` when not the fast `%h{k}` lowering.
1148 pub delete_expr_entries: Vec<Expr>,
1149 /// `exists EXPR` when not the fast `%h{k}` lowering.
1150 pub exists_expr_entries: Vec<Expr>,
1151 /// `push` when the array operand is not a bare `@name` (e.g. `push $aref, ...`).
1152 pub push_expr_entries: Vec<(Expr, Vec<Expr>)>,
1153 pub pop_expr_entries: Vec<Expr>,
1154 pub shift_expr_entries: Vec<Expr>,
1155 pub unshift_expr_entries: Vec<(Expr, Vec<Expr>)>,
1156 pub splice_expr_entries: Vec<SpliceExprEntry>,
1157 /// `map EXPR, LIST` — map expression (list context) with `$_` set to each element.
1158 pub map_expr_entries: Vec<Expr>,
1159 /// When `Some((start, end))`, `map_expr_entries[i]` is lowered like [`Self::grep_expr_bytecode_ranges`].
1160 pub map_expr_bytecode_ranges: Vec<Option<(usize, usize)>>,
1161 /// `grep EXPR, LIST` — filter expression evaluated with `$_` set to each element.
1162 pub grep_expr_entries: Vec<Expr>,
1163 /// When `Some((start, end))`, `grep_expr_entries[i]` is also lowered to `ops[start..end]`
1164 /// (exclusive `end`) with trailing [`Op::BlockReturnValue`], like [`Self::block_bytecode_ranges`].
1165 pub grep_expr_bytecode_ranges: Vec<Option<(usize, usize)>>,
1166 /// Right-hand expression for [`Op::RegexFlipFlopExprRhs`] — boolean context (bare `m//` is `$_ =~ m//`).
1167 pub regex_flip_flop_rhs_expr_entries: Vec<Expr>,
1168 /// When `Some((start, end))`, `regex_flip_flop_rhs_expr_entries[i]` is lowered to `ops[start..end]` +
1169 /// [`Op::BlockReturnValue`].
1170 pub regex_flip_flop_rhs_expr_bytecode_ranges: Vec<Option<(usize, usize)>>,
1171 /// Number of flip-flop slots ([`Op::ScalarFlipFlop`], [`Op::RegexFlipFlop`], [`Op::RegexEofFlipFlop`],
1172 /// [`Op::RegexFlipFlopExprRhs`], [`Op::RegexFlipFlopDotLineRhs`]); VM resets flip-flop vectors.
1173 pub flip_flop_slots: u16,
1174 /// `format NAME =` bodies: basename + lines between `=` and `.` (see lexer).
1175 pub format_decls: Vec<(String, Vec<String>)>,
1176 /// `use overload` pair lists (installed into current package at run time).
1177 pub use_overload_entries: Vec<Vec<(String, String)>>,
1178}
1179
1180impl Chunk {
1181 /// Look up a compiled subroutine entry by stash name pool index.
1182 pub fn find_sub_entry(&self, name_idx: u16) -> Option<(usize, bool)> {
1183 self.sub_entries
1184 .iter()
1185 .find(|(n, _, _)| *n == name_idx)
1186 .map(|(_, ip, stack_args)| (*ip, *stack_args))
1187 }
1188
1189 pub fn new() -> Self {
1190 Self {
1191 ops: Vec::with_capacity(256),
1192 constants: Vec::new(),
1193 names: Vec::new(),
1194 lines: Vec::new(),
1195 op_ast_expr: Vec::new(),
1196 ast_expr_pool: Vec::new(),
1197 sub_entries: Vec::new(),
1198 blocks: Vec::new(),
1199 block_bytecode_ranges: Vec::new(),
1200 static_sub_calls: Vec::new(),
1201 lvalues: Vec::new(),
1202 ast_eval_exprs: Vec::new(),
1203 body_start_ip: 0,
1204 struct_defs: Vec::new(),
1205 enum_defs: Vec::new(),
1206 class_defs: Vec::new(),
1207 trait_defs: Vec::new(),
1208 given_entries: Vec::new(),
1209 given_topic_bytecode_ranges: Vec::new(),
1210 eval_timeout_entries: Vec::new(),
1211 eval_timeout_expr_bytecode_ranges: Vec::new(),
1212 algebraic_match_entries: Vec::new(),
1213 algebraic_match_subject_bytecode_ranges: Vec::new(),
1214 runtime_sub_decls: Vec::new(),
1215 runtime_advice_decls: Vec::new(),
1216 code_ref_sigs: Vec::new(),
1217 par_lines_entries: Vec::new(),
1218 par_walk_entries: Vec::new(),
1219 pwatch_entries: Vec::new(),
1220 substr_four_arg_entries: Vec::new(),
1221 keys_expr_entries: Vec::new(),
1222 keys_expr_bytecode_ranges: Vec::new(),
1223 values_expr_entries: Vec::new(),
1224 values_expr_bytecode_ranges: Vec::new(),
1225 delete_expr_entries: Vec::new(),
1226 exists_expr_entries: Vec::new(),
1227 push_expr_entries: Vec::new(),
1228 pop_expr_entries: Vec::new(),
1229 shift_expr_entries: Vec::new(),
1230 unshift_expr_entries: Vec::new(),
1231 splice_expr_entries: Vec::new(),
1232 map_expr_entries: Vec::new(),
1233 map_expr_bytecode_ranges: Vec::new(),
1234 grep_expr_entries: Vec::new(),
1235 grep_expr_bytecode_ranges: Vec::new(),
1236 regex_flip_flop_rhs_expr_entries: Vec::new(),
1237 regex_flip_flop_rhs_expr_bytecode_ranges: Vec::new(),
1238 flip_flop_slots: 0,
1239 format_decls: Vec::new(),
1240 use_overload_entries: Vec::new(),
1241 }
1242 }
1243
1244 /// Pool index for [`Op::FormatDecl`].
1245 pub fn add_format_decl(&mut self, name: String, lines: Vec<String>) -> u16 {
1246 let idx = self.format_decls.len() as u16;
1247 self.format_decls.push((name, lines));
1248 idx
1249 }
1250
1251 /// Pool index for [`Op::UseOverload`].
1252 pub fn add_use_overload(&mut self, pairs: Vec<(String, String)>) -> u16 {
1253 let idx = self.use_overload_entries.len() as u16;
1254 self.use_overload_entries.push(pairs);
1255 idx
1256 }
1257
1258 /// Allocate a slot index for [`Op::ScalarFlipFlop`] / [`Op::RegexFlipFlop`] / [`Op::RegexEofFlipFlop`] /
1259 /// [`Op::RegexFlipFlopExprRhs`] / [`Op::RegexFlipFlopDotLineRhs`] flip-flop state.
1260 pub fn alloc_flip_flop_slot(&mut self) -> u16 {
1261 let id = self.flip_flop_slots;
1262 self.flip_flop_slots = self.flip_flop_slots.saturating_add(1);
1263 id
1264 }
1265
1266 /// `map EXPR, LIST` — pool index for [`Op::MapWithExpr`].
1267 pub fn add_map_expr_entry(&mut self, expr: Expr) -> u16 {
1268 let idx = self.map_expr_entries.len() as u16;
1269 self.map_expr_entries.push(expr);
1270 idx
1271 }
1272
1273 /// `grep EXPR, LIST` — pool index for [`Op::GrepWithExpr`].
1274 pub fn add_grep_expr_entry(&mut self, expr: Expr) -> u16 {
1275 let idx = self.grep_expr_entries.len() as u16;
1276 self.grep_expr_entries.push(expr);
1277 idx
1278 }
1279
1280 /// Regex flip-flop with compound RHS — pool index for [`Op::RegexFlipFlopExprRhs`].
1281 pub fn add_regex_flip_flop_rhs_expr_entry(&mut self, expr: Expr) -> u16 {
1282 let idx = self.regex_flip_flop_rhs_expr_entries.len() as u16;
1283 self.regex_flip_flop_rhs_expr_entries.push(expr);
1284 idx
1285 }
1286
1287 /// `keys EXPR` (dynamic) — pool index for [`Op::KeysExpr`].
1288 pub fn add_keys_expr_entry(&mut self, expr: Expr) -> u16 {
1289 let idx = self.keys_expr_entries.len() as u16;
1290 self.keys_expr_entries.push(expr);
1291 idx
1292 }
1293
1294 /// `values EXPR` (dynamic) — pool index for [`Op::ValuesExpr`].
1295 pub fn add_values_expr_entry(&mut self, expr: Expr) -> u16 {
1296 let idx = self.values_expr_entries.len() as u16;
1297 self.values_expr_entries.push(expr);
1298 idx
1299 }
1300
1301 /// `delete EXPR` (dynamic operand) — pool index for [`Op::DeleteExpr`].
1302 pub fn add_delete_expr_entry(&mut self, expr: Expr) -> u16 {
1303 let idx = self.delete_expr_entries.len() as u16;
1304 self.delete_expr_entries.push(expr);
1305 idx
1306 }
1307
1308 /// `exists EXPR` (dynamic operand) — pool index for [`Op::ExistsExpr`].
1309 pub fn add_exists_expr_entry(&mut self, expr: Expr) -> u16 {
1310 let idx = self.exists_expr_entries.len() as u16;
1311 self.exists_expr_entries.push(expr);
1312 idx
1313 }
1314
1315 pub fn add_push_expr_entry(&mut self, array: Expr, values: Vec<Expr>) -> u16 {
1316 let idx = self.push_expr_entries.len() as u16;
1317 self.push_expr_entries.push((array, values));
1318 idx
1319 }
1320
1321 pub fn add_pop_expr_entry(&mut self, array: Expr) -> u16 {
1322 let idx = self.pop_expr_entries.len() as u16;
1323 self.pop_expr_entries.push(array);
1324 idx
1325 }
1326
1327 pub fn add_shift_expr_entry(&mut self, array: Expr) -> u16 {
1328 let idx = self.shift_expr_entries.len() as u16;
1329 self.shift_expr_entries.push(array);
1330 idx
1331 }
1332
1333 pub fn add_unshift_expr_entry(&mut self, array: Expr, values: Vec<Expr>) -> u16 {
1334 let idx = self.unshift_expr_entries.len() as u16;
1335 self.unshift_expr_entries.push((array, values));
1336 idx
1337 }
1338
1339 pub fn add_splice_expr_entry(
1340 &mut self,
1341 array: Expr,
1342 offset: Option<Expr>,
1343 length: Option<Expr>,
1344 replacement: Vec<Expr>,
1345 ) -> u16 {
1346 let idx = self.splice_expr_entries.len() as u16;
1347 self.splice_expr_entries
1348 .push((array, offset, length, replacement));
1349 idx
1350 }
1351
1352 /// Four-arg `substr` — returns pool index for [`Op::SubstrFourArg`].
1353 pub fn add_substr_four_arg_entry(
1354 &mut self,
1355 string: Expr,
1356 offset: Expr,
1357 length: Option<Expr>,
1358 replacement: Expr,
1359 ) -> u16 {
1360 let idx = self.substr_four_arg_entries.len() as u16;
1361 self.substr_four_arg_entries
1362 .push((string, offset, length, replacement));
1363 idx
1364 }
1365
1366 /// `par_lines PATH, fn { } [, progress => EXPR]` — returns pool index for [`Op::ParLines`].
1367 pub fn add_par_lines_entry(
1368 &mut self,
1369 path: Expr,
1370 callback: Expr,
1371 progress: Option<Expr>,
1372 ) -> u16 {
1373 let idx = self.par_lines_entries.len() as u16;
1374 self.par_lines_entries.push((path, callback, progress));
1375 idx
1376 }
1377
1378 /// `par_walk PATH, fn { } [, progress => EXPR]` — returns pool index for [`Op::ParWalk`].
1379 pub fn add_par_walk_entry(
1380 &mut self,
1381 path: Expr,
1382 callback: Expr,
1383 progress: Option<Expr>,
1384 ) -> u16 {
1385 let idx = self.par_walk_entries.len() as u16;
1386 self.par_walk_entries.push((path, callback, progress));
1387 idx
1388 }
1389
1390 /// `pwatch GLOB, fn { }` — returns pool index for [`Op::Pwatch`].
1391 pub fn add_pwatch_entry(&mut self, path: Expr, callback: Expr) -> u16 {
1392 let idx = self.pwatch_entries.len() as u16;
1393 self.pwatch_entries.push((path, callback));
1394 idx
1395 }
1396
1397 /// `given (EXPR) { ... }` — returns pool index for [`Op::Given`].
1398 pub fn add_given_entry(&mut self, topic: Expr, body: Block) -> u16 {
1399 let idx = self.given_entries.len() as u16;
1400 self.given_entries.push((topic, body));
1401 idx
1402 }
1403
1404 /// `eval_timeout SECS { ... }` — returns pool index for [`Op::EvalTimeout`].
1405 pub fn add_eval_timeout_entry(&mut self, timeout: Expr, body: Block) -> u16 {
1406 let idx = self.eval_timeout_entries.len() as u16;
1407 self.eval_timeout_entries.push((timeout, body));
1408 idx
1409 }
1410
1411 /// Algebraic `match` — returns pool index for [`Op::AlgebraicMatch`].
1412 pub fn add_algebraic_match_entry(&mut self, subject: Expr, arms: Vec<MatchArm>) -> u16 {
1413 let idx = self.algebraic_match_entries.len() as u16;
1414 self.algebraic_match_entries.push((subject, arms));
1415 idx
1416 }
1417
1418 /// Store an AST block and return its index.
1419 pub fn add_block(&mut self, block: Block) -> u16 {
1420 let idx = self.blocks.len() as u16;
1421 self.blocks.push(block);
1422 idx
1423 }
1424
1425 /// Pool index for [`Op::MakeCodeRef`] signature (`stryke` extension); use empty vec for legacy `fn { }`.
1426 pub fn add_code_ref_sig(&mut self, params: Vec<SubSigParam>) -> u16 {
1427 let idx = self.code_ref_sigs.len();
1428 if idx > u16::MAX as usize {
1429 panic!("too many anonymous sub signatures in one chunk");
1430 }
1431 self.code_ref_sigs.push(params);
1432 idx as u16
1433 }
1434
1435 /// Store an assignable expression (LHS of `s///` / `tr///`) and return its index.
1436 pub fn add_lvalue_expr(&mut self, e: Expr) -> u16 {
1437 let idx = self.lvalues.len() as u16;
1438 self.lvalues.push(e);
1439 idx
1440 }
1441
1442 /// Intern a name, returning its pool index.
1443 pub fn intern_name(&mut self, name: &str) -> u16 {
1444 if let Some(idx) = self.names.iter().position(|n| n == name) {
1445 return idx as u16;
1446 }
1447 let idx = self.names.len() as u16;
1448 self.names.push(name.to_string());
1449 idx
1450 }
1451
1452 /// Add a constant to the pool, returning its index.
1453 pub fn add_constant(&mut self, val: PerlValue) -> u16 {
1454 // Dedup string constants
1455 if let Some(ref s) = val.as_str() {
1456 for (i, c) in self.constants.iter().enumerate() {
1457 if let Some(cs) = c.as_str() {
1458 if cs == *s {
1459 return i as u16;
1460 }
1461 }
1462 }
1463 }
1464 let idx = self.constants.len() as u16;
1465 self.constants.push(val);
1466 idx
1467 }
1468
1469 /// Append an op with source line info.
1470 #[inline]
1471 pub fn emit(&mut self, op: Op, line: usize) -> usize {
1472 self.emit_with_ast_idx(op, line, None)
1473 }
1474
1475 /// Like [`Self::emit`] but attach an optional interned AST [`Expr`] pool index (see [`Self::op_ast_expr`]).
1476 #[inline]
1477 pub fn emit_with_ast_idx(&mut self, op: Op, line: usize, ast: Option<u32>) -> usize {
1478 let idx = self.ops.len();
1479 self.ops.push(op);
1480 self.lines.push(line);
1481 self.op_ast_expr.push(ast);
1482 idx
1483 }
1484
1485 /// Resolve the originating expression for an instruction pointer, if recorded.
1486 #[inline]
1487 pub fn ast_expr_at(&self, ip: usize) -> Option<&Expr> {
1488 let id = (*self.op_ast_expr.get(ip)?)?;
1489 self.ast_expr_pool.get(id as usize)
1490 }
1491
1492 /// Patch a jump instruction at `idx` to target the current position.
1493 pub fn patch_jump_here(&mut self, idx: usize) {
1494 let target = self.ops.len();
1495 self.patch_jump_to(idx, target);
1496 }
1497
1498 /// Patch a jump instruction at `idx` to target an explicit op address.
1499 pub fn patch_jump_to(&mut self, idx: usize, target: usize) {
1500 match &mut self.ops[idx] {
1501 Op::Jump(ref mut t)
1502 | Op::JumpIfTrue(ref mut t)
1503 | Op::JumpIfFalse(ref mut t)
1504 | Op::JumpIfFalseKeep(ref mut t)
1505 | Op::JumpIfTrueKeep(ref mut t)
1506 | Op::JumpIfDefinedKeep(ref mut t) => *t = target,
1507 _ => panic!("patch_jump_to on non-jump op at {}", idx),
1508 }
1509 }
1510
1511 pub fn patch_try_push_catch(&mut self, idx: usize, catch_ip: usize) {
1512 match &mut self.ops[idx] {
1513 Op::TryPush { catch_ip: c, .. } => *c = catch_ip,
1514 _ => panic!("patch_try_push_catch on non-TryPush op at {}", idx),
1515 }
1516 }
1517
1518 pub fn patch_try_push_finally(&mut self, idx: usize, finally_ip: Option<usize>) {
1519 match &mut self.ops[idx] {
1520 Op::TryPush { finally_ip: f, .. } => *f = finally_ip,
1521 _ => panic!("patch_try_push_finally on non-TryPush op at {}", idx),
1522 }
1523 }
1524
1525 pub fn patch_try_push_after(&mut self, idx: usize, after_ip: usize) {
1526 match &mut self.ops[idx] {
1527 Op::TryPush { after_ip: a, .. } => *a = after_ip,
1528 _ => panic!("patch_try_push_after on non-TryPush op at {}", idx),
1529 }
1530 }
1531
1532 /// Current op count (next emit position).
1533 #[inline]
1534 pub fn len(&self) -> usize {
1535 self.ops.len()
1536 }
1537
1538 #[inline]
1539 pub fn is_empty(&self) -> bool {
1540 self.ops.is_empty()
1541 }
1542
1543 /// Human-readable listing: subroutine entry points and each op with its source line (javap / `dis`-style).
1544 pub fn disassemble(&self) -> String {
1545 use std::fmt::Write;
1546 let mut out = String::new();
1547 for (i, n) in self.names.iter().enumerate() {
1548 let _ = writeln!(out, "; name[{}] = {}", i, n);
1549 }
1550 let _ = writeln!(out, "; sub_entries:");
1551 for (ni, ip, stack_args) in &self.sub_entries {
1552 let name = self
1553 .names
1554 .get(*ni as usize)
1555 .map(|s| s.as_str())
1556 .unwrap_or("?");
1557 let _ = writeln!(out, "; {} @ {} stack_args={}", name, ip, stack_args);
1558 }
1559 for (i, op) in self.ops.iter().enumerate() {
1560 let line = self.lines.get(i).copied().unwrap_or(0);
1561 let ast = self
1562 .op_ast_expr
1563 .get(i)
1564 .copied()
1565 .flatten()
1566 .map(|id| id.to_string())
1567 .unwrap_or_else(|| "-".into());
1568 let _ = writeln!(out, "{:04} {:>5} {:>6} {:?}", i, line, ast, op);
1569 }
1570 out
1571 }
1572
1573 /// Peephole pass: fuse common multi-op sequences into single superinstructions,
1574 /// then compact by removing Nop slots and remapping all jump targets.
1575 pub fn peephole_fuse(&mut self) {
1576 let len = self.ops.len();
1577 if len < 2 {
1578 return;
1579 }
1580 // Pass 1: fuse OP + Pop → OPVoid
1581 let mut i = 0;
1582 while i + 1 < len {
1583 if matches!(self.ops[i + 1], Op::Pop) {
1584 let replacement = match &self.ops[i] {
1585 Op::AddAssignSlotSlot(d, s) => Some(Op::AddAssignSlotSlotVoid(*d, *s)),
1586 Op::PreIncSlot(s) => Some(Op::PreIncSlotVoid(*s)),
1587 Op::ConcatAppendSlot(s) => Some(Op::ConcatAppendSlotVoid(*s)),
1588 _ => None,
1589 };
1590 if let Some(op) = replacement {
1591 self.ops[i] = op;
1592 self.ops[i + 1] = Op::Nop;
1593 i += 2;
1594 continue;
1595 }
1596 }
1597 i += 1;
1598 }
1599 // Pass 2: fuse multi-op patterns
1600 // Helper: check if any jump targets position `pos`.
1601 let has_jump_to = |ops: &[Op], pos: usize| -> bool {
1602 for op in ops {
1603 let t = match op {
1604 Op::Jump(t)
1605 | Op::JumpIfFalse(t)
1606 | Op::JumpIfTrue(t)
1607 | Op::JumpIfFalseKeep(t)
1608 | Op::JumpIfTrueKeep(t)
1609 | Op::JumpIfDefinedKeep(t) => Some(*t),
1610 _ => None,
1611 };
1612 if t == Some(pos) {
1613 return true;
1614 }
1615 }
1616 false
1617 };
1618 let len = self.ops.len();
1619 if len >= 4 {
1620 i = 0;
1621 while i + 3 < len {
1622 if let (
1623 Op::GetScalarSlot(slot),
1624 Op::LoadInt(n),
1625 Op::NumLt,
1626 Op::JumpIfFalse(target),
1627 ) = (
1628 &self.ops[i],
1629 &self.ops[i + 1],
1630 &self.ops[i + 2],
1631 &self.ops[i + 3],
1632 ) {
1633 if let Ok(n32) = i32::try_from(*n) {
1634 // Don't fuse if any jump targets the ops that will become Nop.
1635 // This prevents breaking short-circuit &&/|| that jump to the
1636 // JumpIfFalse for the while condition exit check.
1637 if has_jump_to(&self.ops, i + 1)
1638 || has_jump_to(&self.ops, i + 2)
1639 || has_jump_to(&self.ops, i + 3)
1640 {
1641 i += 1;
1642 continue;
1643 }
1644 let slot = *slot;
1645 let target = *target;
1646 self.ops[i] = Op::SlotLtIntJumpIfFalse(slot, n32, target);
1647 self.ops[i + 1] = Op::Nop;
1648 self.ops[i + 2] = Op::Nop;
1649 self.ops[i + 3] = Op::Nop;
1650 i += 4;
1651 continue;
1652 }
1653 }
1654 i += 1;
1655 }
1656 }
1657 // Compact once so that pass 3 sees a Nop-free op stream and can match
1658 // adjacent `PreIncSlotVoid + Jump` backedges produced by passes 1/2.
1659 self.compact_nops();
1660 // Pass 3: fuse loop backedge
1661 // PreIncSlotVoid(s) + Jump(top)
1662 // where ops[top] is SlotLtIntJumpIfFalse(s, limit, exit)
1663 // becomes
1664 // SlotIncLtIntJumpBack(s, limit, top + 1) // body falls through
1665 // Nop // was Jump
1666 // The first-iteration check at `top` is still reached from before the loop
1667 // (the loop's initial entry goes through the top test), so leaving
1668 // SlotLtIntJumpIfFalse in place keeps the entry path correct. All
1669 // subsequent iterations now skip both the inc op and the jump.
1670 let len = self.ops.len();
1671 if len >= 2 {
1672 let mut i = 0;
1673 while i + 1 < len {
1674 if let (Op::PreIncSlotVoid(s), Op::Jump(top)) = (&self.ops[i], &self.ops[i + 1]) {
1675 let slot = *s;
1676 let top = *top;
1677 // Only fuse backward branches — the C-style `for` shape where `top` is
1678 // the loop's `SlotLtIntJumpIfFalse` test and the body falls through to
1679 // this trailing increment. A forward `Jump` that happens to land on a
1680 // similar test is not the same shape and must not be rewritten.
1681 if top < i {
1682 if let Op::SlotLtIntJumpIfFalse(tslot, limit, exit) = &self.ops[top] {
1683 // Safety: the top test's exit target must equal the fused op's
1684 // fall-through (i + 2). Otherwise exiting the loop via
1685 // "condition false" would land somewhere the unfused shape never
1686 // exited to.
1687 if *tslot == slot && *exit == i + 2 {
1688 let limit = *limit;
1689 let body_target = top + 1;
1690 self.ops[i] = Op::SlotIncLtIntJumpBack(slot, limit, body_target);
1691 self.ops[i + 1] = Op::Nop;
1692 i += 2;
1693 continue;
1694 }
1695 }
1696 }
1697 }
1698 i += 1;
1699 }
1700 }
1701 // Pass 4: compact again — remove the Nops introduced by pass 3.
1702 self.compact_nops();
1703 // Pass 5: fuse counted-loop bodies down to a single native superinstruction.
1704 //
1705 // After pass 3 + compact, a `for (my $i = ..; $i < N; $i = $i + 1) { $sum += $i }`
1706 // loop looks like:
1707 //
1708 // [top] SlotLtIntJumpIfFalse(i, N, exit)
1709 // [body_start] AddAssignSlotSlotVoid(sum, i) ← target of the backedge
1710 // SlotIncLtIntJumpBack(i, N, body_start)
1711 // [exit] ...
1712 //
1713 // When the body is exactly one op, we fuse the AddAssign + backedge into
1714 // `AccumSumLoop(sum, i, N)`, whose handler runs the whole remaining loop in a
1715 // tight Rust `while`. Same scheme for the counted `$s .= CONST` pattern, fused
1716 // into `ConcatConstSlotLoop`.
1717 //
1718 // Safety gate: only fire when no op jumps *into* the body (other than the backedge
1719 // itself and the top test's fall-through, which isn't a jump). That keeps loops with
1720 // interior labels / `last LABEL` / `next LABEL` from being silently skipped.
1721 let len = self.ops.len();
1722 if len >= 2 {
1723 let has_inbound_jump = |ops: &[Op], pos: usize, ignore: usize| -> bool {
1724 for (j, op) in ops.iter().enumerate() {
1725 if j == ignore {
1726 continue;
1727 }
1728 let t = match op {
1729 Op::Jump(t)
1730 | Op::JumpIfFalse(t)
1731 | Op::JumpIfTrue(t)
1732 | Op::JumpIfFalseKeep(t)
1733 | Op::JumpIfTrueKeep(t)
1734 | Op::JumpIfDefinedKeep(t) => Some(*t),
1735 Op::SlotLtIntJumpIfFalse(_, _, t) => Some(*t),
1736 Op::SlotIncLtIntJumpBack(_, _, t) => Some(*t),
1737 _ => None,
1738 };
1739 if t == Some(pos) {
1740 return true;
1741 }
1742 }
1743 false
1744 };
1745 // 5a: AddAssignSlotSlotVoid + SlotIncLtIntJumpBack → AccumSumLoop
1746 let mut i = 0;
1747 while i + 1 < len {
1748 if let (
1749 Op::AddAssignSlotSlotVoid(sum_slot, src_slot),
1750 Op::SlotIncLtIntJumpBack(inc_slot, limit, body_target),
1751 ) = (&self.ops[i], &self.ops[i + 1])
1752 {
1753 if *src_slot == *inc_slot
1754 && *body_target == i
1755 && !has_inbound_jump(&self.ops, i, i + 1)
1756 && !has_inbound_jump(&self.ops, i + 1, i + 1)
1757 {
1758 let sum_slot = *sum_slot;
1759 let src_slot = *src_slot;
1760 let limit = *limit;
1761 self.ops[i] = Op::AccumSumLoop(sum_slot, src_slot, limit);
1762 self.ops[i + 1] = Op::Nop;
1763 i += 2;
1764 continue;
1765 }
1766 }
1767 i += 1;
1768 }
1769 // 5b: LoadConst + ConcatAppendSlotVoid + SlotIncLtIntJumpBack → ConcatConstSlotLoop
1770 if len >= 3 {
1771 let mut i = 0;
1772 while i + 2 < len {
1773 if let (
1774 Op::LoadConst(const_idx),
1775 Op::ConcatAppendSlotVoid(s_slot),
1776 Op::SlotIncLtIntJumpBack(inc_slot, limit, body_target),
1777 ) = (&self.ops[i], &self.ops[i + 1], &self.ops[i + 2])
1778 {
1779 if *body_target == i
1780 && !has_inbound_jump(&self.ops, i, i + 2)
1781 && !has_inbound_jump(&self.ops, i + 1, i + 2)
1782 && !has_inbound_jump(&self.ops, i + 2, i + 2)
1783 {
1784 let const_idx = *const_idx;
1785 let s_slot = *s_slot;
1786 let inc_slot = *inc_slot;
1787 let limit = *limit;
1788 self.ops[i] =
1789 Op::ConcatConstSlotLoop(const_idx, s_slot, inc_slot, limit);
1790 self.ops[i + 1] = Op::Nop;
1791 self.ops[i + 2] = Op::Nop;
1792 i += 3;
1793 continue;
1794 }
1795 }
1796 i += 1;
1797 }
1798 }
1799 // 5e: `$sum += $h{$k}` body op inside `for my $k (keys %h) { ... }`
1800 // GetScalarSlot(sum) + GetScalarPlain(k) + GetHashElem(h) + Add
1801 // + SetScalarSlotKeep(sum) + Pop
1802 // → AddHashElemPlainKeyToSlot(sum, k, h)
1803 // Safe because `SetScalarSlotKeep + Pop` leaves nothing on the stack net; the fused
1804 // op is a drop-in for that sequence. No inbound jumps permitted to interior ops.
1805 if len >= 6 {
1806 let mut i = 0;
1807 while i + 5 < len {
1808 if let (
1809 Op::GetScalarSlot(sum_slot),
1810 Op::GetScalarPlain(k_idx),
1811 Op::GetHashElem(h_idx),
1812 Op::Add,
1813 Op::SetScalarSlotKeep(sum_slot2),
1814 Op::Pop,
1815 ) = (
1816 &self.ops[i],
1817 &self.ops[i + 1],
1818 &self.ops[i + 2],
1819 &self.ops[i + 3],
1820 &self.ops[i + 4],
1821 &self.ops[i + 5],
1822 ) {
1823 if *sum_slot == *sum_slot2
1824 && (0..6).all(|off| !has_inbound_jump(&self.ops, i + off, usize::MAX))
1825 {
1826 let sum_slot = *sum_slot;
1827 let k_idx = *k_idx;
1828 let h_idx = *h_idx;
1829 self.ops[i] = Op::AddHashElemPlainKeyToSlot(sum_slot, k_idx, h_idx);
1830 for off in 1..=5 {
1831 self.ops[i + off] = Op::Nop;
1832 }
1833 i += 6;
1834 continue;
1835 }
1836 }
1837 i += 1;
1838 }
1839 }
1840 // 5e-slot: slot-key variant of 5e, emitted when the compiler lowers `$k` (the foreach
1841 // loop variable) into a slot rather than a frame scalar.
1842 // GetScalarSlot(sum) + GetScalarSlot(k) + GetHashElem(h) + Add
1843 // + SetScalarSlotKeep(sum) + Pop
1844 // → AddHashElemSlotKeyToSlot(sum, k, h)
1845 if len >= 6 {
1846 let mut i = 0;
1847 while i + 5 < len {
1848 if let (
1849 Op::GetScalarSlot(sum_slot),
1850 Op::GetScalarSlot(k_slot),
1851 Op::GetHashElem(h_idx),
1852 Op::Add,
1853 Op::SetScalarSlotKeep(sum_slot2),
1854 Op::Pop,
1855 ) = (
1856 &self.ops[i],
1857 &self.ops[i + 1],
1858 &self.ops[i + 2],
1859 &self.ops[i + 3],
1860 &self.ops[i + 4],
1861 &self.ops[i + 5],
1862 ) {
1863 if *sum_slot == *sum_slot2
1864 && *sum_slot != *k_slot
1865 && (0..6).all(|off| !has_inbound_jump(&self.ops, i + off, usize::MAX))
1866 {
1867 let sum_slot = *sum_slot;
1868 let k_slot = *k_slot;
1869 let h_idx = *h_idx;
1870 self.ops[i] = Op::AddHashElemSlotKeyToSlot(sum_slot, k_slot, h_idx);
1871 for off in 1..=5 {
1872 self.ops[i + off] = Op::Nop;
1873 }
1874 i += 6;
1875 continue;
1876 }
1877 }
1878 i += 1;
1879 }
1880 }
1881 // 5d: counted hash-insert loop `$h{$i} = $i * K`
1882 // GetScalarSlot(i) + LoadInt(k) + Mul + GetScalarSlot(i) + SetHashElem(h) + Pop
1883 // + SlotIncLtIntJumpBack(i, limit, body_target)
1884 // → SetHashIntTimesLoop(h, i, k, limit)
1885 if len >= 7 {
1886 let mut i = 0;
1887 while i + 6 < len {
1888 if let (
1889 Op::GetScalarSlot(gs1),
1890 Op::LoadInt(k),
1891 Op::Mul,
1892 Op::GetScalarSlot(gs2),
1893 Op::SetHashElem(h_idx),
1894 Op::Pop,
1895 Op::SlotIncLtIntJumpBack(inc_slot, limit, body_target),
1896 ) = (
1897 &self.ops[i],
1898 &self.ops[i + 1],
1899 &self.ops[i + 2],
1900 &self.ops[i + 3],
1901 &self.ops[i + 4],
1902 &self.ops[i + 5],
1903 &self.ops[i + 6],
1904 ) {
1905 if *gs1 == *inc_slot
1906 && *gs2 == *inc_slot
1907 && *body_target == i
1908 && i32::try_from(*k).is_ok()
1909 && (0..6).all(|off| !has_inbound_jump(&self.ops, i + off, i + 6))
1910 && !has_inbound_jump(&self.ops, i + 6, i + 6)
1911 {
1912 let h_idx = *h_idx;
1913 let inc_slot = *inc_slot;
1914 let k32 = *k as i32;
1915 let limit = *limit;
1916 self.ops[i] = Op::SetHashIntTimesLoop(h_idx, inc_slot, k32, limit);
1917 for off in 1..=6 {
1918 self.ops[i + off] = Op::Nop;
1919 }
1920 i += 7;
1921 continue;
1922 }
1923 }
1924 i += 1;
1925 }
1926 }
1927 // 5c: GetScalarSlot + PushArray + ArrayLen + Pop + SlotIncLtIntJumpBack
1928 // → PushIntRangeToArrayLoop
1929 // This is the compiler's `push @a, $i; $i++` shape in void context, where
1930 // the `push` expression's length return is pushed by `ArrayLen` and then `Pop`ped.
1931 if len >= 5 {
1932 let mut i = 0;
1933 while i + 4 < len {
1934 if let (
1935 Op::GetScalarSlot(get_slot),
1936 Op::PushArray(push_idx),
1937 Op::ArrayLen(len_idx),
1938 Op::Pop,
1939 Op::SlotIncLtIntJumpBack(inc_slot, limit, body_target),
1940 ) = (
1941 &self.ops[i],
1942 &self.ops[i + 1],
1943 &self.ops[i + 2],
1944 &self.ops[i + 3],
1945 &self.ops[i + 4],
1946 ) {
1947 if *get_slot == *inc_slot
1948 && *push_idx == *len_idx
1949 && *body_target == i
1950 && !has_inbound_jump(&self.ops, i, i + 4)
1951 && !has_inbound_jump(&self.ops, i + 1, i + 4)
1952 && !has_inbound_jump(&self.ops, i + 2, i + 4)
1953 && !has_inbound_jump(&self.ops, i + 3, i + 4)
1954 && !has_inbound_jump(&self.ops, i + 4, i + 4)
1955 {
1956 let push_idx = *push_idx;
1957 let inc_slot = *inc_slot;
1958 let limit = *limit;
1959 self.ops[i] = Op::PushIntRangeToArrayLoop(push_idx, inc_slot, limit);
1960 self.ops[i + 1] = Op::Nop;
1961 self.ops[i + 2] = Op::Nop;
1962 self.ops[i + 3] = Op::Nop;
1963 self.ops[i + 4] = Op::Nop;
1964 i += 5;
1965 continue;
1966 }
1967 }
1968 i += 1;
1969 }
1970 }
1971 }
1972 // Pass 6: compact — remove the Nops pass 5 introduced.
1973 self.compact_nops();
1974 // Pass 7: fuse the entire `for my $k (keys %h) { $sum += $h{$k} }` loop into a single
1975 // `SumHashValuesToSlot` op that walks the hash's values in a tight native loop.
1976 //
1977 // After prior passes and compaction the shape is a 15-op block:
1978 //
1979 // HashKeys(h)
1980 // DeclareArray(list)
1981 // LoadInt(0)
1982 // DeclareScalarSlot(c, cname)
1983 // LoadUndef
1984 // DeclareScalarSlot(v, vname)
1985 // [top] GetScalarSlot(c)
1986 // ArrayLen(list)
1987 // NumLt
1988 // JumpIfFalse(end)
1989 // GetScalarSlot(c)
1990 // GetArrayElem(list)
1991 // SetScalarSlot(v)
1992 // AddHashElemSlotKeyToSlot(sum, v, h) ← fused body (pass 5e-slot)
1993 // PreIncSlotVoid(c)
1994 // Jump(top)
1995 // [end]
1996 //
1997 // The counter (`__foreach_i__`), list (`__foreach_list__`), and loop var (`$k`) live
1998 // inside a `PushFrame`-isolated scope and are invisible after the loop — it is safe to
1999 // elide all of them. The fused op accumulates directly into `sum` without creating the
2000 // keys array at all.
2001 //
2002 // Safety gates:
2003 // - `h` in HashKeys must match `h` in AddHashElemSlotKeyToSlot.
2004 // - `list` in DeclareArray must match the loop `ArrayLen` / `GetArrayElem`.
2005 // - `c` / `v` slots must be consistent throughout.
2006 // - No inbound jump lands inside the 15-op window from the outside.
2007 // - JumpIfFalse target must be i+15 (just past the Jump back-edge).
2008 // - Jump back-edge target must be i+6 (the GetScalarSlot(c) at loop top).
2009 let len = self.ops.len();
2010 if len >= 15 {
2011 let has_inbound_jump =
2012 |ops: &[Op], pos: usize, ignore_from: usize, ignore_to: usize| -> bool {
2013 for (j, op) in ops.iter().enumerate() {
2014 if j >= ignore_from && j <= ignore_to {
2015 continue;
2016 }
2017 let t = match op {
2018 Op::Jump(t)
2019 | Op::JumpIfFalse(t)
2020 | Op::JumpIfTrue(t)
2021 | Op::JumpIfFalseKeep(t)
2022 | Op::JumpIfTrueKeep(t)
2023 | Op::JumpIfDefinedKeep(t) => *t,
2024 Op::SlotLtIntJumpIfFalse(_, _, t) => *t,
2025 Op::SlotIncLtIntJumpBack(_, _, t) => *t,
2026 _ => continue,
2027 };
2028 if t == pos {
2029 return true;
2030 }
2031 }
2032 false
2033 };
2034 let mut i = 0;
2035 while i + 15 < len {
2036 if let (
2037 Op::HashKeys(h_idx),
2038 Op::DeclareArray(list_idx),
2039 Op::LoadInt(0),
2040 Op::DeclareScalarSlot(c_slot, _c_name),
2041 Op::LoadUndef,
2042 Op::DeclareScalarSlot(v_slot, _v_name),
2043 Op::GetScalarSlot(c_get1),
2044 Op::ArrayLen(len_idx),
2045 Op::NumLt,
2046 Op::JumpIfFalse(end_tgt),
2047 Op::GetScalarSlot(c_get2),
2048 Op::GetArrayElem(elem_idx),
2049 Op::SetScalarSlot(v_set),
2050 Op::AddHashElemSlotKeyToSlot(sum_slot, v_in_body, h_in_body),
2051 Op::PreIncSlotVoid(c_inc),
2052 Op::Jump(top_tgt),
2053 ) = (
2054 &self.ops[i],
2055 &self.ops[i + 1],
2056 &self.ops[i + 2],
2057 &self.ops[i + 3],
2058 &self.ops[i + 4],
2059 &self.ops[i + 5],
2060 &self.ops[i + 6],
2061 &self.ops[i + 7],
2062 &self.ops[i + 8],
2063 &self.ops[i + 9],
2064 &self.ops[i + 10],
2065 &self.ops[i + 11],
2066 &self.ops[i + 12],
2067 &self.ops[i + 13],
2068 &self.ops[i + 14],
2069 &self.ops[i + 15],
2070 ) {
2071 let full_end = i + 15;
2072 if *list_idx == *len_idx
2073 && *list_idx == *elem_idx
2074 && *c_slot == *c_get1
2075 && *c_slot == *c_get2
2076 && *c_slot == *c_inc
2077 && *v_slot == *v_set
2078 && *v_slot == *v_in_body
2079 && *h_idx == *h_in_body
2080 && *top_tgt == i + 6
2081 && *end_tgt == i + 16
2082 && *sum_slot != *c_slot
2083 && *sum_slot != *v_slot
2084 && !(i..=full_end).any(|k| has_inbound_jump(&self.ops, k, i, full_end))
2085 {
2086 let sum_slot = *sum_slot;
2087 let h_idx = *h_idx;
2088 self.ops[i] = Op::SumHashValuesToSlot(sum_slot, h_idx);
2089 for off in 1..=15 {
2090 self.ops[i + off] = Op::Nop;
2091 }
2092 i += 16;
2093 continue;
2094 }
2095 }
2096 i += 1;
2097 }
2098 }
2099 // Pass 8: compact pass 7's Nops.
2100 self.compact_nops();
2101 }
2102
2103 /// Remove all `Nop` instructions and remap jump targets + metadata indices.
2104 fn compact_nops(&mut self) {
2105 let old_len = self.ops.len();
2106 // Build old→new index mapping.
2107 let mut remap = vec![0usize; old_len + 1];
2108 let mut new_idx = 0usize;
2109 for (old, slot) in remap[..old_len].iter_mut().enumerate() {
2110 *slot = new_idx;
2111 if !matches!(self.ops[old], Op::Nop) {
2112 new_idx += 1;
2113 }
2114 }
2115 remap[old_len] = new_idx;
2116 if new_idx == old_len {
2117 return; // nothing to compact
2118 }
2119 // Remap jump targets in all ops.
2120 for op in &mut self.ops {
2121 match op {
2122 Op::Jump(t) | Op::JumpIfFalse(t) | Op::JumpIfTrue(t) => *t = remap[*t],
2123 Op::JumpIfFalseKeep(t) | Op::JumpIfTrueKeep(t) | Op::JumpIfDefinedKeep(t) => {
2124 *t = remap[*t]
2125 }
2126 Op::SlotLtIntJumpIfFalse(_, _, t) => *t = remap[*t],
2127 Op::SlotIncLtIntJumpBack(_, _, t) => *t = remap[*t],
2128 _ => {}
2129 }
2130 }
2131 // Remap sub entry points.
2132 for e in &mut self.sub_entries {
2133 e.1 = remap[e.1];
2134 }
2135 // Remap `CallStaticSubId` resolved entry IPs — they were recorded by
2136 // `patch_static_sub_calls` before peephole fusion ran, so any Nop
2137 // removal in front of a sub body shifts its entry and must be
2138 // reflected here; otherwise `vm_dispatch_user_call` jumps one (or
2139 // more) ops past the real sub start and silently skips the first
2140 // instruction(s) of the body.
2141 for c in &mut self.static_sub_calls {
2142 c.0 = remap[c.0];
2143 }
2144 // Remap block/grep/sort/etc bytecode ranges.
2145 fn remap_ranges(ranges: &mut [Option<(usize, usize)>], remap: &[usize]) {
2146 for r in ranges.iter_mut().flatten() {
2147 r.0 = remap[r.0];
2148 r.1 = remap[r.1];
2149 }
2150 }
2151 remap_ranges(&mut self.block_bytecode_ranges, &remap);
2152 remap_ranges(&mut self.map_expr_bytecode_ranges, &remap);
2153 remap_ranges(&mut self.grep_expr_bytecode_ranges, &remap);
2154 remap_ranges(&mut self.keys_expr_bytecode_ranges, &remap);
2155 remap_ranges(&mut self.values_expr_bytecode_ranges, &remap);
2156 remap_ranges(&mut self.eval_timeout_expr_bytecode_ranges, &remap);
2157 remap_ranges(&mut self.given_topic_bytecode_ranges, &remap);
2158 remap_ranges(&mut self.algebraic_match_subject_bytecode_ranges, &remap);
2159 remap_ranges(&mut self.regex_flip_flop_rhs_expr_bytecode_ranges, &remap);
2160 // Compact ops, lines, op_ast_expr.
2161 let mut j = 0;
2162 for old in 0..old_len {
2163 if !matches!(self.ops[old], Op::Nop) {
2164 self.ops[j] = self.ops[old].clone();
2165 if old < self.lines.len() && j < self.lines.len() {
2166 self.lines[j] = self.lines[old];
2167 }
2168 if old < self.op_ast_expr.len() && j < self.op_ast_expr.len() {
2169 self.op_ast_expr[j] = self.op_ast_expr[old];
2170 }
2171 j += 1;
2172 }
2173 }
2174 self.ops.truncate(j);
2175 self.lines.truncate(j);
2176 self.op_ast_expr.truncate(j);
2177 }
2178}
2179
2180impl Default for Chunk {
2181 fn default() -> Self {
2182 Self::new()
2183 }
2184}
2185
2186#[cfg(test)]
2187mod tests {
2188 use super::*;
2189 use crate::ast;
2190
2191 #[test]
2192 fn chunk_new_and_default_match() {
2193 let a = Chunk::new();
2194 let b = Chunk::default();
2195 assert!(a.ops.is_empty() && a.names.is_empty() && a.constants.is_empty());
2196 assert!(b.ops.is_empty() && b.lines.is_empty());
2197 }
2198
2199 #[test]
2200 fn intern_name_deduplicates() {
2201 let mut c = Chunk::new();
2202 let i0 = c.intern_name("foo");
2203 let i1 = c.intern_name("foo");
2204 let i2 = c.intern_name("bar");
2205 assert_eq!(i0, i1);
2206 assert_ne!(i0, i2);
2207 assert_eq!(c.names.len(), 2);
2208 }
2209
2210 #[test]
2211 fn add_constant_dedups_identical_strings() {
2212 let mut c = Chunk::new();
2213 let a = c.add_constant(PerlValue::string("x".into()));
2214 let b = c.add_constant(PerlValue::string("x".into()));
2215 assert_eq!(a, b);
2216 assert_eq!(c.constants.len(), 1);
2217 }
2218
2219 #[test]
2220 fn add_constant_distinct_strings_different_indices() {
2221 let mut c = Chunk::new();
2222 let a = c.add_constant(PerlValue::string("a".into()));
2223 let b = c.add_constant(PerlValue::string("b".into()));
2224 assert_ne!(a, b);
2225 assert_eq!(c.constants.len(), 2);
2226 }
2227
2228 #[test]
2229 fn add_constant_non_string_no_dedup_scan() {
2230 let mut c = Chunk::new();
2231 let a = c.add_constant(PerlValue::integer(1));
2232 let b = c.add_constant(PerlValue::integer(1));
2233 assert_ne!(a, b);
2234 assert_eq!(c.constants.len(), 2);
2235 }
2236
2237 #[test]
2238 fn emit_records_parallel_ops_and_lines() {
2239 let mut c = Chunk::new();
2240 c.emit(Op::LoadInt(1), 10);
2241 c.emit(Op::Pop, 11);
2242 assert_eq!(c.len(), 2);
2243 assert_eq!(c.lines, vec![10, 11]);
2244 assert_eq!(c.op_ast_expr, vec![None, None]);
2245 assert!(!c.is_empty());
2246 }
2247
2248 #[test]
2249 fn len_is_empty_track_ops() {
2250 let mut c = Chunk::new();
2251 assert!(c.is_empty());
2252 assert_eq!(c.len(), 0);
2253 c.emit(Op::Halt, 0);
2254 assert!(!c.is_empty());
2255 assert_eq!(c.len(), 1);
2256 }
2257
2258 #[test]
2259 fn patch_jump_here_updates_jump_target() {
2260 let mut c = Chunk::new();
2261 let j = c.emit(Op::Jump(0), 1);
2262 c.emit(Op::LoadInt(99), 2);
2263 c.patch_jump_here(j);
2264 assert_eq!(c.ops.len(), 2);
2265 assert!(matches!(c.ops[j], Op::Jump(2)));
2266 }
2267
2268 #[test]
2269 fn patch_jump_here_jump_if_true() {
2270 let mut c = Chunk::new();
2271 let j = c.emit(Op::JumpIfTrue(0), 1);
2272 c.emit(Op::Halt, 2);
2273 c.patch_jump_here(j);
2274 assert!(matches!(c.ops[j], Op::JumpIfTrue(2)));
2275 }
2276
2277 #[test]
2278 fn patch_jump_here_jump_if_false_keep() {
2279 let mut c = Chunk::new();
2280 let j = c.emit(Op::JumpIfFalseKeep(0), 1);
2281 c.emit(Op::Pop, 2);
2282 c.patch_jump_here(j);
2283 assert!(matches!(c.ops[j], Op::JumpIfFalseKeep(2)));
2284 }
2285
2286 #[test]
2287 fn patch_jump_here_jump_if_true_keep() {
2288 let mut c = Chunk::new();
2289 let j = c.emit(Op::JumpIfTrueKeep(0), 1);
2290 c.emit(Op::Pop, 2);
2291 c.patch_jump_here(j);
2292 assert!(matches!(c.ops[j], Op::JumpIfTrueKeep(2)));
2293 }
2294
2295 #[test]
2296 fn patch_jump_here_jump_if_defined_keep() {
2297 let mut c = Chunk::new();
2298 let j = c.emit(Op::JumpIfDefinedKeep(0), 1);
2299 c.emit(Op::Halt, 2);
2300 c.patch_jump_here(j);
2301 assert!(matches!(c.ops[j], Op::JumpIfDefinedKeep(2)));
2302 }
2303
2304 #[test]
2305 #[should_panic(expected = "patch_jump_to on non-jump op")]
2306 fn patch_jump_here_panics_on_non_jump() {
2307 let mut c = Chunk::new();
2308 let idx = c.emit(Op::LoadInt(1), 1);
2309 c.patch_jump_here(idx);
2310 }
2311
2312 #[test]
2313 fn add_block_returns_sequential_indices() {
2314 let mut c = Chunk::new();
2315 let b0: ast::Block = vec![];
2316 let b1: ast::Block = vec![];
2317 assert_eq!(c.add_block(b0), 0);
2318 assert_eq!(c.add_block(b1), 1);
2319 assert_eq!(c.blocks.len(), 2);
2320 }
2321
2322 #[test]
2323 fn builtin_id_from_u16_first_and_last() {
2324 assert_eq!(BuiltinId::from_u16(0), Some(BuiltinId::Length));
2325 assert_eq!(
2326 BuiltinId::from_u16(BuiltinId::Pselect as u16),
2327 Some(BuiltinId::Pselect)
2328 );
2329 assert_eq!(
2330 BuiltinId::from_u16(BuiltinId::BarrierNew as u16),
2331 Some(BuiltinId::BarrierNew)
2332 );
2333 assert_eq!(
2334 BuiltinId::from_u16(BuiltinId::ParPipeline as u16),
2335 Some(BuiltinId::ParPipeline)
2336 );
2337 assert_eq!(
2338 BuiltinId::from_u16(BuiltinId::GlobParProgress as u16),
2339 Some(BuiltinId::GlobParProgress)
2340 );
2341 assert_eq!(
2342 BuiltinId::from_u16(BuiltinId::Readpipe as u16),
2343 Some(BuiltinId::Readpipe)
2344 );
2345 assert_eq!(
2346 BuiltinId::from_u16(BuiltinId::ReadLineList as u16),
2347 Some(BuiltinId::ReadLineList)
2348 );
2349 assert_eq!(
2350 BuiltinId::from_u16(BuiltinId::ReaddirList as u16),
2351 Some(BuiltinId::ReaddirList)
2352 );
2353 assert_eq!(
2354 BuiltinId::from_u16(BuiltinId::Ssh as u16),
2355 Some(BuiltinId::Ssh)
2356 );
2357 assert_eq!(
2358 BuiltinId::from_u16(BuiltinId::Pipe as u16),
2359 Some(BuiltinId::Pipe)
2360 );
2361 assert_eq!(
2362 BuiltinId::from_u16(BuiltinId::Files as u16),
2363 Some(BuiltinId::Files)
2364 );
2365 assert_eq!(
2366 BuiltinId::from_u16(BuiltinId::Filesf as u16),
2367 Some(BuiltinId::Filesf)
2368 );
2369 assert_eq!(
2370 BuiltinId::from_u16(BuiltinId::Dirs as u16),
2371 Some(BuiltinId::Dirs)
2372 );
2373 assert_eq!(
2374 BuiltinId::from_u16(BuiltinId::SymLinks as u16),
2375 Some(BuiltinId::SymLinks)
2376 );
2377 assert_eq!(
2378 BuiltinId::from_u16(BuiltinId::Sockets as u16),
2379 Some(BuiltinId::Sockets)
2380 );
2381 assert_eq!(
2382 BuiltinId::from_u16(BuiltinId::Pipes as u16),
2383 Some(BuiltinId::Pipes)
2384 );
2385 assert_eq!(
2386 BuiltinId::from_u16(BuiltinId::BlockDevices as u16),
2387 Some(BuiltinId::BlockDevices)
2388 );
2389 assert_eq!(
2390 BuiltinId::from_u16(BuiltinId::CharDevices as u16),
2391 Some(BuiltinId::CharDevices)
2392 );
2393 assert_eq!(
2394 BuiltinId::from_u16(BuiltinId::Executables as u16),
2395 Some(BuiltinId::Executables)
2396 );
2397 }
2398
2399 #[test]
2400 fn builtin_id_from_u16_out_of_range() {
2401 assert_eq!(BuiltinId::from_u16(BuiltinId::Executables as u16 + 1), None);
2402 assert_eq!(BuiltinId::from_u16(u16::MAX), None);
2403 }
2404
2405 #[test]
2406 fn op_enum_clone_roundtrip() {
2407 let o = Op::Call(42, 3, 0);
2408 assert!(matches!(o.clone(), Op::Call(42, 3, 0)));
2409 }
2410
2411 #[test]
2412 fn chunk_clone_independent_ops() {
2413 let mut c = Chunk::new();
2414 c.emit(Op::Negate, 1);
2415 let mut d = c.clone();
2416 d.emit(Op::Pop, 2);
2417 assert_eq!(c.len(), 1);
2418 assert_eq!(d.len(), 2);
2419 }
2420
2421 #[test]
2422 fn chunk_disassemble_includes_ops() {
2423 let mut c = Chunk::new();
2424 c.emit(Op::LoadInt(7), 1);
2425 let s = c.disassemble();
2426 assert!(s.contains("0000"));
2427 assert!(s.contains("LoadInt(7)"));
2428 assert!(s.contains(" -")); // no ast ref column
2429 }
2430
2431 #[test]
2432 fn ast_expr_at_roundtrips_pooled_expr() {
2433 let mut c = Chunk::new();
2434 let e = ast::Expr {
2435 kind: ast::ExprKind::Integer(99),
2436 line: 3,
2437 };
2438 c.ast_expr_pool.push(e);
2439 c.emit_with_ast_idx(Op::LoadInt(99), 3, Some(0));
2440 let got = c.ast_expr_at(0).expect("ast ref");
2441 assert!(matches!(&got.kind, ast::ExprKind::Integer(99)));
2442 assert_eq!(got.line, 3);
2443 }
2444}