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