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