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