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