logicaffeine_forge/jit.rs
1//! J1: the straight-line micro-op compiler — bytecode in, native code out.
2//!
3//! [`MicroOp`] is the JIT's input IR: the integer subset of the bytecode VM's
4//! register operations. [`compile_straightline`] lowers each op to a fixed
5//! stencil micro-sequence over the frame/operand-stack machine model and glues
6//! the result into one executable chain:
7//!
8//! ```text
9//! LoadConst{dst,v} → const(v); slot_set(dst)
10//! Move{dst,src} → slot_get(src); slot_set(dst)
11//! Add{dst,l,r} → slot_get(l); slot_get(r); addi; slot_set(dst)
12//! … (Sub/Mul/Lt/Eq identical shape)
13//! Gt{dst,l,r} → slot_get(r); slot_get(l); lti; slot_set(dst) (swap)
14//! Return{src} → slot_get(src); return
15//! ```
16//!
17//! Anything outside this subset is the caller's tier-up bail (`None` from the
18//! adapter) — the VM keeps running it.
19
20use std::sync::atomic::{AtomicI64, Ordering};
21
22use crate::buffer::{HoleValue, JitBuffer, JitChain};
23use crate::{
24 V_BINOP, V_BRANCH, V_CONST, V_FBINOP, V_FMOV, V_MOV, V_RET, V_SQRTF, V_DIVF, V_I2F, V_BRANCHF,
25 V_ARRLD_RPTR, V_ARRLD_RPTR_C, V_ARRST_RPTR, V_ARRST_RPTR_C,
26 ST_ADD3, ST_ADDF3, ST_AND3, ST_ARRLD, ST_ARRLD2_ADDF, ST_ARRLD2_MULF, ST_ARRLD2_SUBF, ST_ARRLDB, ST_ARRLDB_U, ST_ARRLD_U, ST_ARRST, ST_ARRSTB, ST_ARRSTB_U, ST_ARRST_U, ST_BREQ, ST_BREQF, ST_BRLEF, ST_BRLT, ST_CALL, ST_PUSH, ST_LIST_CLEAR,
27 ST_ARRLD_I32, ST_ARRLD_I32_U, ST_ARRST_I32, ST_ARRST_I32_U,
28 ST_ARRLD2_ADD, ST_ARRLD2_ADD_U, ST_ARRLD2_SUB, ST_ARRLD2_SUB_U, ST_ARRLD2_MUL, ST_ARRLD2_MUL_U,
29 ST_ARRLDAFF_NONE, ST_ARRLDAFF_NONE_U, ST_ARRLDAFF_ADD, ST_ARRLDAFF_ADD_U,
30 ST_ARRLDAFF_SUB, ST_ARRLDAFF_SUB_U, ST_ARRLDAFF_MUL, ST_ARRLDAFF_MUL_U,
31 ST_ARRRMW_ADD, ST_ARRRMW_ADD_U, ST_ARRRMW_SUB, ST_ARRRMW_SUB_U, ST_ARRRMW_MUL, ST_ARRRMW_MUL_U,
32 ST_ARRRMW_AND, ST_ARRRMW_AND_U, ST_ARRRMW_OR, ST_ARRRMW_OR_U, ST_ARRRMW_XOR, ST_ARRRMW_XOR_U,
33 ST_ARRRMW_ADDF, ST_ARRRMW_ADDF_U, ST_ARRRMW_SUBF, ST_ARRRMW_SUBF_U, ST_ARRRMW_MULF, ST_ARRRMW_MULF_U,
34 ST_FMAF,
35 ST_ARRCONDSWAP_GT, ST_ARRCONDSWAP_GT_U, ST_ARRCONDSWAP_LT, ST_ARRCONDSWAP_LT_U,
36 ST_ARRCONDSWAP_GE, ST_ARRCONDSWAP_GE_U, ST_ARRCONDSWAP_LE, ST_ARRCONDSWAP_LE_U,
37 ST_ARRSWAP, ST_ARRSWAP_U,
38 ST_BRLTF, ST_BRZ, ST_CALL_PRECISE, ST_CONSTST, ST_DEOPT, ST_DEOPT_AT, ST_DIV3C,
39 ST_DIVPOW2, ST_MAGICDIV,
40 ST_DIVF3C, ST_EQ3, ST_EQF3, ST_I2F2,
41 ST_JUMP, ST_LE3, ST_LEF3, ST_LT3, ST_LTF3, ST_MOD3C, ST_MOVSS, ST_MUL3, ST_MULF3, ST_NE3,
42 ST_ALLOCLIST, ST_CALL_SELF, ST_CALL_SELF_COPY, ST_LISTTRIPLE, ST_MAPHGET, ST_MAPHHAS, ST_MAPHSET, ST_MEMMEM, ST_NEF3,
43 ST_NOTB2,
44 ST_NOTI2, ST_OR3, ST_RET2,
45 ST_SHL3, ST_SHR3, ST_SQRTF2, ST_SUB3,
46 ST_SUBF3, ST_XOR3,
47};
48
49/// Frame slot index (a VM register number).
50pub type Slot = u16;
51
52/// The kernel's locked maximum LOGOS call depth, BAKED into the precise and
53/// self call stencils (their holes are all spoken for, so the limit cannot be
54/// a runtime hole). It mirrors `logicaffeine_compile::semantics::MAX_CALL_DEPTH`
55/// and the matching constant in `stencils/int_stencils.rs`; the callers in the
56/// JIT crate pass that same value, asserted here at stencil selection so a
57/// drift fails loudly instead of silently capping native recursion at the wrong
58/// depth (which would diverge from the kernel's depth-exceeded error).
59pub const BAKED_CALL_DEPTH: i64 = 2_500;
60
61/// Comparison kinds for [`MicroOp::Branch`].
62#[derive(Clone, Copy, Debug, PartialEq, Eq)]
63pub enum Cmp {
64 /// `lhs < rhs`
65 Lt,
66 /// `lhs > rhs`
67 Gt,
68 /// `lhs <= rhs`
69 LtEq,
70 /// `lhs >= rhs`
71 GtEq,
72 /// `lhs == rhs`
73 Eq,
74 /// `lhs != rhs`
75 NotEq,
76}
77
78impl Cmp {
79 /// Evaluate over two i64s.
80 pub fn eval(self, a: i64, b: i64) -> bool {
81 match self {
82 Cmp::Lt => a < b,
83 Cmp::Gt => a > b,
84 Cmp::LtEq => a <= b,
85 Cmp::GtEq => a >= b,
86 Cmp::Eq => a == b,
87 Cmp::NotEq => a != b,
88 }
89 }
90
91 /// The comparison with the opposite truth value.
92 pub fn negated(self) -> Cmp {
93 match self {
94 Cmp::Lt => Cmp::GtEq,
95 Cmp::Gt => Cmp::LtEq,
96 Cmp::LtEq => Cmp::Gt,
97 Cmp::GtEq => Cmp::Lt,
98 Cmp::Eq => Cmp::NotEq,
99 Cmp::NotEq => Cmp::Eq,
100 }
101 }
102}
103
104/// The float binary operation of a fused [`MicroOp::ArrLoad2F`].
105#[derive(Clone, Copy, Debug, PartialEq, Eq)]
106pub enum FOp {
107 /// IEEE addition.
108 Add,
109 /// IEEE subtraction.
110 Sub,
111 /// IEEE multiplication.
112 Mul,
113}
114
115impl FOp {
116 /// Apply over two f64s.
117 pub fn eval(self, a: f64, b: f64) -> f64 {
118 match self {
119 FOp::Add => a + b,
120 FOp::Sub => a - b,
121 FOp::Mul => a * b,
122 }
123 }
124}
125
126/// The integer binary operation of a fused two-buffer [`MicroOp::ArrLoad2`]:
127/// `frame[dst] = a[i-1] <op> b[j-1]`, the two elements loaded from two
128/// (possibly distinct) pinned 8-byte int buffers. Add/Mul commute; Sub does
129/// not (it only fuses `a[i] - b[j]`).
130#[derive(Clone, Copy, Debug, PartialEq, Eq)]
131pub enum IOp {
132 /// Wrapping i64 addition.
133 Add,
134 /// Wrapping i64 subtraction.
135 Sub,
136 /// Wrapping i64 multiplication.
137 Mul,
138}
139
140impl IOp {
141 /// Apply over two i64s with the kernel's wrapping semantics.
142 pub fn eval(self, a: i64, b: i64) -> i64 {
143 match self {
144 IOp::Add => a.wrapping_add(b),
145 IOp::Sub => a.wrapping_sub(b),
146 IOp::Mul => a.wrapping_mul(b),
147 }
148 }
149}
150
151/// The index-arithmetic shape folded into a fused [`MicroOp::ArrLoadAffine`].
152/// The computed 1-based index is `(frame[a] OP frame[b]).wrapping_add(c)` for a
153/// two-slot op, or `frame[a].wrapping_add(c)` for [`AffOp::None`] (a single slot
154/// plus a constant). The op wraps with the kernel's exact i64 semantics so the
155/// load is bit-identical to the un-fused index arithmetic + `ArrLoad`.
156#[derive(Clone, Copy, Debug, PartialEq, Eq)]
157pub enum AffOp {
158 /// No second slot: `idx = frame[a] + c` (the `w + 1` shape).
159 None,
160 /// `idx = (frame[a] + frame[b]) + c` (the `i*n + j + 1` tail).
161 Add,
162 /// `idx = (frame[a] - frame[b]) + c` (the `w - wi + 1` shape).
163 Sub,
164 /// `idx = (frame[a] * frame[b]) + c`.
165 Mul,
166}
167
168impl AffOp {
169 /// Compute the 1-based index with the kernel's wrapping i64 semantics.
170 pub fn eval(self, a: i64, b: i64, c: i64) -> i64 {
171 match self {
172 AffOp::None => a.wrapping_add(c),
173 AffOp::Add => a.wrapping_add(b).wrapping_add(c),
174 AffOp::Sub => a.wrapping_sub(b).wrapping_add(c),
175 AffOp::Mul => a.wrapping_mul(b).wrapping_add(c),
176 }
177 }
178}
179
180/// The operation of a fused read-modify-write [`MicroOp::ArrRMW`]:
181/// `buf[idx-1] = buf[idx-1] <op> operand`. The INT ops (Add/Sub/Mul wrap as the
182/// kernel's i64 arithmetic; And/Or/Xor bitwise) treat the element and operand as
183/// raw i64; the FLOAT ops (AddF/SubF/MulF) reinterpret both as f64 (the nbody
184/// velocity/position-update idiom). Sub/SubF are the only non-commutative
185/// members — the peephole fuses them only when the loaded element is the LEFT
186/// operand.
187#[derive(Clone, Copy, Debug, PartialEq, Eq)]
188pub enum RmwOp {
189 /// Wrapping addition.
190 Add,
191 /// Wrapping subtraction (`buf[i] - operand`).
192 Sub,
193 /// Wrapping multiplication.
194 Mul,
195 /// Bitwise AND.
196 And,
197 /// Bitwise OR.
198 Or,
199 /// Bitwise XOR.
200 Xor,
201 /// IEEE addition (operands reinterpreted as f64).
202 AddF,
203 /// IEEE subtraction (`buf[i] - operand`, as f64).
204 SubF,
205 /// IEEE multiplication (as f64).
206 MulF,
207}
208
209impl RmwOp {
210 /// Whether the op reinterprets its operands as f64.
211 pub fn is_float(self) -> bool {
212 matches!(self, RmwOp::AddF | RmwOp::SubF | RmwOp::MulF)
213 }
214
215 /// Apply over the raw i64 bits of the element and operand — the bit-exact
216 /// spec every stencil must match. Float ops reinterpret the bits as f64,
217 /// compute, and re-encode (copy-and-patch never reassociates, so ordering is
218 /// exact).
219 pub fn eval(self, a: i64, b: i64) -> i64 {
220 match self {
221 RmwOp::Add => a.wrapping_add(b),
222 RmwOp::Sub => a.wrapping_sub(b),
223 RmwOp::Mul => a.wrapping_mul(b),
224 RmwOp::And => a & b,
225 RmwOp::Or => a | b,
226 RmwOp::Xor => a ^ b,
227 RmwOp::AddF => (f64::from_bits(a as u64) + f64::from_bits(b as u64)).to_bits() as i64,
228 RmwOp::SubF => (f64::from_bits(a as u64) - f64::from_bits(b as u64)).to_bits() as i64,
229 RmwOp::MulF => (f64::from_bits(a as u64) * f64::from_bits(b as u64)).to_bits() as i64,
230 }
231 }
232}
233
234/// The source operand appended by a [`MicroOp::StrAppend`] — the right-hand side
235/// of a `Set text to text + <s>` in a pinned mutable-Text build loop.
236#[derive(Clone, Copy, Debug, PartialEq, Eq)]
237pub enum StrSrc {
238 /// A single ASCII byte VALUE living in a frame slot (the `Kind::TextByte`
239 /// lane: `text + ch` where `ch` is a 1-char ASCII text). The helper appends
240 /// `byte as char` — bit-identical to the VM concatenating the 1-char `Text`.
241 Byte(Slot),
242 /// A constant ASCII byte slice baked into the chain (`text + "XXXXX"`): a
243 /// `'static` pointer + length. The helper appends the whole slice — identical
244 /// to the VM's `Text + Text` concatenation of the literal.
245 Const {
246 /// `'static` pointer to the constant's bytes.
247 ptr: i64,
248 /// The constant's byte length.
249 len: i64,
250 },
251}
252
253/// The integer straight-line subset the J1 compiler accepts.
254#[derive(Clone, Copy, Debug, PartialEq, Eq)]
255pub enum MicroOp {
256 /// `frame[dst] = value`.
257 LoadConst {
258 /// Destination slot.
259 dst: Slot,
260 /// Immediate value.
261 value: i64,
262 },
263 /// `frame[dst] = frame[src]`.
264 Move {
265 /// Destination slot.
266 dst: Slot,
267 /// Source slot.
268 src: Slot,
269 },
270 /// `frame[dst] = frame[lhs] + frame[rhs]` (wrapping).
271 Add {
272 /// Destination slot.
273 dst: Slot,
274 /// Left operand slot.
275 lhs: Slot,
276 /// Right operand slot.
277 rhs: Slot,
278 },
279 /// `frame[dst] = frame[lhs] - frame[rhs]` (wrapping).
280 Sub {
281 /// Destination slot.
282 dst: Slot,
283 /// Left operand slot.
284 lhs: Slot,
285 /// Right operand slot.
286 rhs: Slot,
287 },
288 /// `frame[dst] = frame[lhs] * frame[rhs]` (wrapping).
289 Mul {
290 /// Destination slot.
291 dst: Slot,
292 /// Left operand slot.
293 lhs: Slot,
294 /// Right operand slot.
295 rhs: Slot,
296 },
297 /// `frame[dst] = (frame[lhs] < frame[rhs]) as i64`.
298 Lt {
299 /// Destination slot.
300 dst: Slot,
301 /// Left operand slot.
302 lhs: Slot,
303 /// Right operand slot.
304 rhs: Slot,
305 },
306 /// `frame[dst] = (frame[lhs] > frame[rhs]) as i64`.
307 Gt {
308 /// Destination slot.
309 dst: Slot,
310 /// Left operand slot.
311 lhs: Slot,
312 /// Right operand slot.
313 rhs: Slot,
314 },
315 /// `frame[dst] = (frame[lhs] == frame[rhs]) as i64`.
316 Eq {
317 /// Destination slot.
318 dst: Slot,
319 /// Left operand slot.
320 lhs: Slot,
321 /// Right operand slot.
322 rhs: Slot,
323 },
324 /// `frame[dst] = frame[lhs] / frame[rhs]` (wrapping; `MIN / -1 = MIN`).
325 /// A zero divisor SIDE-EXITS the chain ([`ChainOutcome::Deopt`]) before
326 /// any effect — the caller replays on bytecode, where the kernel raises
327 /// the exact "Division by zero" error.
328 Div {
329 /// Destination slot.
330 dst: Slot,
331 /// Left operand slot.
332 lhs: Slot,
333 /// Right operand slot.
334 rhs: Slot,
335 },
336 /// `frame[dst] = frame[lhs] / 2^k` (signed, round toward zero) via a
337 /// sign-correcting shift — bit-exact with [`MicroOp::Div`] but with NO
338 /// side-exit (a power-of-two divisor is never `0` or `-1`). Emitted in
339 /// place of `Div` when the divisor is a region-constant power of two and
340 /// the dividend is integer-typed.
341 DivPow2 {
342 /// Destination slot.
343 dst: Slot,
344 /// Dividend slot.
345 lhs: Slot,
346 /// Shift amount `k` (divisor = `2^k`, `1 <= k <= 62`).
347 k: u32,
348 },
349 /// `frame[dst] = frame[lhs] / c` (`mul_back == 0`) or `frame[lhs] % c`
350 /// (`mul_back == c`) by the Granlund–Montgomery / libdivide UNSIGNED
351 /// magic-reciprocal sequence (`mulhi(magic, x)` + add-fixup + shift, then
352 /// `x - q*c` for the remainder) — a `mul`+`shr` (~3 cycles) instead of
353 /// `idiv` (~25). NO side-exit (a literal `c > 0` is never `0` or `-1`).
354 /// Emitted in place of `Div`/`Mod` when the divisor is a compile-time
355 /// constant non-power-of-two and the dividend is proven Int and NON-NEGATIVE
356 /// (the unsigned magic equals the signed truncating result only there). The
357 /// `more` byte is the `logicaffeine_data::LogosDivU64` encoding (low 6 bits
358 /// = shift, `0x40` = the 65-bit add-marker path, `0x80` = pure-shift pow2).
359 MagicDivU {
360 /// Destination slot.
361 dst: Slot,
362 /// Dividend slot.
363 lhs: Slot,
364 /// Precomputed magic multiplier `M`.
365 magic: u64,
366 /// Shift / path encoding (see `logicaffeine_data::LogosDivU64`).
367 more: u8,
368 /// `0` selects the quotient; otherwise the divisor `c` selects the
369 /// remainder `x - (x/c)*c`.
370 mul_back: i64,
371 },
372 /// `frame[dst] = frame[lhs] % frame[rhs]` (wrapping; `MIN % -1 = 0`).
373 /// Zero divisor side-exits exactly like [`MicroOp::Div`].
374 Mod {
375 /// Destination slot.
376 dst: Slot,
377 /// Left operand slot.
378 lhs: Slot,
379 /// Right operand slot.
380 rhs: Slot,
381 },
382 /// `frame[dst] = (frame[lhs] <= frame[rhs]) as i64`.
383 LtEq {
384 /// Destination slot.
385 dst: Slot,
386 /// Left operand slot.
387 lhs: Slot,
388 /// Right operand slot.
389 rhs: Slot,
390 },
391 /// `frame[dst] = (frame[lhs] >= frame[rhs]) as i64`.
392 GtEq {
393 /// Destination slot.
394 dst: Slot,
395 /// Left operand slot.
396 lhs: Slot,
397 /// Right operand slot.
398 rhs: Slot,
399 },
400 /// `frame[dst] = (frame[lhs] != frame[rhs]) as i64`.
401 Neq {
402 /// Destination slot.
403 dst: Slot,
404 /// Left operand slot.
405 lhs: Slot,
406 /// Right operand slot.
407 rhs: Slot,
408 },
409 /// Fused compare-and-branch: transfer to `target` when `cmp(lhs, rhs)` is
410 /// FALSE (the `JumpIfFalse`-on-a-fresh-comparison shape); fall through
411 /// when TRUE. No comparison value is materialized — the adapter proves
412 /// the scratch dead before fusing.
413 Branch {
414 /// The comparison.
415 cmp: Cmp,
416 /// Left operand slot.
417 lhs: Slot,
418 /// Right operand slot.
419 rhs: Slot,
420 /// Micro-op index to transfer to when the comparison is FALSE.
421 target: usize,
422 },
423 /// `frame[dst] = frame[lhs] & frame[rhs]` (bitwise; logical for 0/1 Bools).
424 BitAnd {
425 /// Destination slot.
426 dst: Slot,
427 /// Left operand slot.
428 lhs: Slot,
429 /// Right operand slot.
430 rhs: Slot,
431 },
432 /// `frame[dst] = frame[lhs] | frame[rhs]` (bitwise; logical for 0/1 Bools).
433 BitOr {
434 /// Destination slot.
435 dst: Slot,
436 /// Left operand slot.
437 lhs: Slot,
438 /// Right operand slot.
439 rhs: Slot,
440 },
441 /// `frame[dst] = frame[lhs] ^ frame[rhs]`.
442 BitXor {
443 /// Destination slot.
444 dst: Slot,
445 /// Left operand slot.
446 lhs: Slot,
447 /// Right operand slot.
448 rhs: Slot,
449 },
450 /// `frame[dst] = frame[lhs].wrapping_shl(frame[rhs] as u32)` — the
451 /// kernel's locked shift spec (count truncates to u32, masks mod 64).
452 Shl {
453 /// Destination slot.
454 dst: Slot,
455 /// Left operand slot.
456 lhs: Slot,
457 /// Right operand slot.
458 rhs: Slot,
459 },
460 /// `frame[dst] = frame[lhs].wrapping_shr(frame[rhs] as u32)` (arithmetic).
461 Shr {
462 /// Destination slot.
463 dst: Slot,
464 /// Left operand slot.
465 lhs: Slot,
466 /// Right operand slot.
467 rhs: Slot,
468 },
469 /// `frame[dst] = !frame[src]` — bitwise NOT (the kernel's Int `not`).
470 NotInt {
471 /// Destination slot.
472 dst: Slot,
473 /// Source slot.
474 src: Slot,
475 },
476 /// `frame[dst] = frame[src] ^ 1` — logical NOT over 0/1 Bools.
477 NotBool {
478 /// Destination slot.
479 dst: Slot,
480 /// Source slot.
481 src: Slot,
482 },
483 /// Float ops: f64 values as raw bits in the i64 slots. Arithmetic is
484 /// IEEE; equality is the kernel's EPSILON rule.
485 AddF {
486 /// Destination slot.
487 dst: Slot,
488 /// Left operand slot.
489 lhs: Slot,
490 /// Right operand slot.
491 rhs: Slot,
492 },
493 /// `bits(f(lhs) - f(rhs))`.
494 SubF {
495 /// Destination slot.
496 dst: Slot,
497 /// Left operand slot.
498 lhs: Slot,
499 /// Right operand slot.
500 rhs: Slot,
501 },
502 /// `bits(f(lhs) * f(rhs))`.
503 MulF {
504 /// Destination slot.
505 dst: Slot,
506 /// Left operand slot.
507 lhs: Slot,
508 /// Right operand slot.
509 rhs: Slot,
510 },
511 /// `bits(f(lhs) / f(rhs))`; divisor `== 0.0` side-exits like [`MicroOp::Div`].
512 DivF {
513 /// Destination slot.
514 dst: Slot,
515 /// Left operand slot.
516 lhs: Slot,
517 /// Right operand slot.
518 rhs: Slot,
519 },
520 /// `(f(lhs) < f(rhs)) as i64` (IEEE, NaN → 0).
521 LtF {
522 /// Destination slot.
523 dst: Slot,
524 /// Left operand slot.
525 lhs: Slot,
526 /// Right operand slot.
527 rhs: Slot,
528 },
529 /// `(f(lhs) > f(rhs)) as i64`.
530 GtF {
531 /// Destination slot.
532 dst: Slot,
533 /// Left operand slot.
534 lhs: Slot,
535 /// Right operand slot.
536 rhs: Slot,
537 },
538 /// `(f(lhs) <= f(rhs)) as i64`.
539 LtEqF {
540 /// Destination slot.
541 dst: Slot,
542 /// Left operand slot.
543 lhs: Slot,
544 /// Right operand slot.
545 rhs: Slot,
546 },
547 /// `(f(lhs) >= f(rhs)) as i64`.
548 GtEqF {
549 /// Destination slot.
550 dst: Slot,
551 /// Left operand slot.
552 lhs: Slot,
553 /// Right operand slot.
554 rhs: Slot,
555 },
556 /// The kernel's epsilon equality as a value.
557 EqF {
558 /// Destination slot.
559 dst: Slot,
560 /// Left operand slot.
561 lhs: Slot,
562 /// Right operand slot.
563 rhs: Slot,
564 },
565 /// Negated epsilon equality as a value.
566 NeqF {
567 /// Destination slot.
568 dst: Slot,
569 /// Left operand slot.
570 lhs: Slot,
571 /// Right operand slot.
572 rhs: Slot,
573 },
574 /// Fused FLOAT compare-and-branch: transfer to `target` when
575 /// `cmp(f(lhs), f(rhs))` is FALSE — which, under IEEE, includes every
576 /// NaN-unordered comparison (matching the kernel's relations exactly).
577 BranchF {
578 /// The comparison.
579 cmp: Cmp,
580 /// Left operand slot.
581 lhs: Slot,
582 /// Right operand slot.
583 rhs: Slot,
584 /// Micro-op index to transfer to when the comparison is FALSE.
585 target: usize,
586 },
587 /// `frame[dst] = bits(frame[src] as f64)` — the kernel's Int→Float
588 /// promotion.
589 IntToFloat {
590 /// Destination slot.
591 dst: Slot,
592 /// Source slot.
593 src: Slot,
594 },
595 /// `frame[dst] = bits(sqrt(f(frame[src])))` — the kernel's Float sqrt
596 /// builtin (IEEE: negative → NaN, no error path).
597 SqrtF {
598 /// Destination slot.
599 dst: Slot,
600 /// Source slot.
601 src: Slot,
602 },
603 /// Map get through the pinned storage: an Int hit lands in `dst`; a
604 /// miss or non-Int value side-exits (the replay raises the kernel's
605 /// exact error or re-runs the boxed path).
606 MapGet {
607 /// Destination slot.
608 dst: Slot,
609 /// Key slot.
610 key: Slot,
611 /// The pinned `*mut MapStorage` slot.
612 map_slot: Slot,
613 /// Runtime helper address (indirect call, no relocation).
614 helper_addr: i64,
615 },
616 /// Map insert (always succeeds; same storage, same iteration order).
617 MapSet {
618 /// Value slot.
619 src: Slot,
620 /// Key slot.
621 key: Slot,
622 /// The pinned `*mut MapStorage` slot.
623 map_slot: Slot,
624 /// Runtime helper address.
625 helper_addr: i64,
626 },
627 /// DIRECT self-call (mode A): entry patched post-layout into the
628 /// literal pool; static frame-size bound; plain replay deopt.
629 CallSelf {
630 /// Caller register receiving the result.
631 dst: Slot,
632 /// First slot of the staged argument window.
633 args_start: Slot,
634 /// Live-depth cell address.
635 depth_addr: i64,
636 /// Shared status cell address.
637 status_addr: i64,
638 /// MY arena-limit slot (the bound source).
639 limit_slot: Slot,
640 /// The callee's (own) full frame size in slots.
641 frame_size: i64,
642 },
643 /// DIRECT self-call with a FUSED argument copy (Lever A: the pinned-arg
644 /// self-call ABI). Same model as [`MicroOp::CallSelf`], but it stages the
645 /// `arg_count` contiguous scalar arguments from `src_start..` into the
646 /// callee window itself rather than relying on `arg_count` separate
647 /// `Move` pieces before the call — one piece replaces `arg_count + 1`.
648 /// Emitted only for all-scalar (Int/Bool/Float) self-calls; list-param
649 /// and pin-triple staging stay on the per-`Move` path.
650 CallSelfCopy {
651 /// Caller register receiving the result.
652 dst: Slot,
653 /// First slot of the staged argument window (the callee frame base).
654 args_start: Slot,
655 /// First slot of the SOURCE argument block in this frame.
656 src_start: Slot,
657 /// Number of contiguous scalar arguments to copy.
658 arg_count: u16,
659 /// Live-depth cell address.
660 depth_addr: i64,
661 /// Shared status cell address.
662 status_addr: i64,
663 /// MY arena-limit slot (the bound source).
664 limit_slot: Slot,
665 /// The callee's (own) full frame size in slots.
666 frame_size: i64,
667 },
668 /// Allocate a registry-owned fresh list and plant its pin triple.
669 NewList {
670 /// Pin handle slot.
671 vec_slot: Slot,
672 /// Pin buffer-pointer slot.
673 ptr_slot: Slot,
674 /// Pin length slot.
675 len_slot: Slot,
676 /// Runtime allocator address.
677 helper_addr: i64,
678 },
679 /// Plant a pin triple from a list handle already in `handle_slot`
680 /// (a self-call's returned list, seen from the caller).
681 ListTriple {
682 /// Slot holding the `*mut Vec<i64>` handle.
683 handle_slot: Slot,
684 /// Pin handle slot.
685 vec_slot: Slot,
686 /// Pin buffer-pointer slot.
687 ptr_slot: Slot,
688 /// Pin length slot.
689 len_slot: Slot,
690 /// Runtime helper address.
691 helper_addr: i64,
692 },
693 /// Map membership into `dst` (0/1).
694 MapHas {
695 /// Destination slot.
696 dst: Slot,
697 /// Key slot.
698 key: Slot,
699 /// The pinned `*mut MapStorage` slot.
700 map_slot: Slot,
701 /// Runtime helper address.
702 helper_addr: i64,
703 },
704 /// Native SELF-CALL through the program's entry table (callee frame
705 /// windowed at `base + args_start` like the VM). Missing entry, a
706 /// MAX_CALL_DEPTH crossing, or arena overflow side-exits the WHOLE
707 /// native stack via the shared status cell.
708 Call {
709 /// Result slot.
710 dst: Slot,
711 /// Callee frame offset within the caller's frame.
712 args_start: Slot,
713 /// Address of the [entry, regcount] table slot pair.
714 table_addr: i64,
715 /// Address of the shared live-depth cell.
716 depth_addr: i64,
717 /// Address of the shared deopt-status cell.
718 status_addr: i64,
719 /// Frame slot holding the arena END address.
720 limit_slot: Slot,
721 /// MAX_CALL_DEPTH.
722 depth_limit: i64,
723 },
724 /// Pinned-array push. The contiguous regalloc backend lowers this to an
725 /// INLINE fast path — `len < cap ? buffer[len++] = v` straight in registers,
726 /// reading the capacity from the live `*mut Vec` (a codegen-probed field
727 /// offset, never a hardcoded layout) — and calls the runtime helper ONLY on
728 /// the cold realloc boundary (`len == cap`), where it reallocates and
729 /// refreshes the pinned pointer/length slots. The per-piece stencil tier
730 /// always calls the helper.
731 ArrPush {
732 /// Value slot.
733 src: Slot,
734 /// Frame slot holding the `*mut Vec` handle.
735 vec_slot: Slot,
736 /// Pinned pointer slot to refresh.
737 ptr_slot: Slot,
738 /// Pinned length slot to refresh.
739 len_slot: Slot,
740 /// The runtime helper's address.
741 helper_addr: i64,
742 /// 1-BYTE (`Seq of Bool`) element: the inline fast-path store writes the
743 /// boolean normalization `(v != 0) as u8` (matching `logos_rt_push_bool`),
744 /// not 8 raw bytes. `false` for Int/Float (8-byte raw bits).
745 byte: bool,
746 /// 4-byte Int element (`ListRepr::IntsI32`): the inline fast-path store
747 /// TRUNCATES the value to its low 4 bytes (lossless under the narrowing
748 /// proof), matching `logos_rt_push_i32`. Mutually exclusive with `byte`.
749 narrow32: bool,
750 },
751 /// Pinned-array in-place clear through a runtime helper: truncate the buffer
752 /// to empty (keep capacity) and refresh the pinned pointer/length slots
753 /// (length → 0). Lowers an in-region `NewEmptyList` on a pinned array; an
754 /// in-place mutation, so a PRECISE region resumes over it soundly.
755 ListClear {
756 /// Frame slot holding the `*mut Vec` handle.
757 vec_slot: Slot,
758 /// Pinned pointer slot to refresh.
759 ptr_slot: Slot,
760 /// Pinned length slot to refresh (set to 0).
761 len_slot: Slot,
762 /// The runtime helper's address.
763 helper_addr: i64,
764 },
765 /// Pinned MUTABLE-Text append (`Set text to text + <s>`) through a runtime
766 /// helper. `text_handle_slot` holds a `*mut Value` to the VM register cell
767 /// holding the accumulator (planted at region entry); the helper grows the
768 /// accumulator THROUGH that cell with EXACTLY the VM's `add_assign`
769 /// semantics — in place when the `Rc<String>` is sole-owned, copy-on-write
770 /// (a fresh `Rc` written back into the cell, the alias untouched) otherwise.
771 /// `src` is the appended operand (a 1-char frame byte, or a baked constant
772 /// slice). Bit-identical to the tree-walker for every alias case. The buffer
773 /// reaches into the VM register file, so — like the other helper calls — it
774 /// clobbers the caller-saved registers (the residents are spilled/reloaded).
775 StrAppend {
776 /// Frame slot holding the `*mut Value` accumulator-cell handle.
777 text_handle_slot: Slot,
778 /// The appended source operand.
779 src: StrSrc,
780 /// The runtime helper's address (`logos_rt_str_append`).
781 helper_addr: i64,
782 },
783 /// COUNT OVERLAPPING SUBSTRING MATCHES through a runtime helper — the whole
784 /// naive-search nest (string_search) collapsed to one piece. The helper
785 /// reads the pinned haystack/needle byte buffers, counts overlapping needle
786 /// occurrences over the outer range `[frame[i_slot], textLen - needleLen + 1]`
787 /// (1-based, matching the nest), ADDS the count into `frame[count_slot]`, and
788 /// advances `frame[i_slot]` to the loop's exit value. On a recoverable
789 /// disagreement (a checked needle index past the needle buffer) it touches
790 /// nothing and takes the deopt continuation, so the VM replays the exact
791 /// nest on bytecode and raises the same error. Holes: 0 = haystack ptr slot,
792 /// 1 = haystack len slot, 2 = needle ptr slot, 3 = needle len slot, 4 =
793 /// needleLen value slot, 5 = i (start) slot, 6 = count accumulator slot,
794 /// 7 = helper address.
795 MemMem {
796 /// Frame slot holding the pinned haystack buffer pointer.
797 h_ptr_slot: Slot,
798 /// Frame slot holding the pinned haystack length.
799 h_len_slot: Slot,
800 /// Frame slot holding the pinned needle buffer pointer.
801 n_ptr_slot: Slot,
802 /// Frame slot holding the pinned needle length.
803 n_len_slot: Slot,
804 /// Frame slot holding the program's `needleLen` value (inner bound).
805 needle_len_slot: Slot,
806 /// Frame slot holding the 1-based outer index `i` (start; written to the
807 /// exit value on success).
808 i_slot: Slot,
809 /// Frame slot holding the running `count` accumulator (added into).
810 count_slot: Slot,
811 /// The runtime helper's address.
812 helper_addr: i64,
813 },
814 /// Pinned-array load: `frame[dst] = buffer[frame[idx] - 1]` (1-based;
815 /// bits regardless of element kind). Out-of-bounds (incl. 0/negative)
816 /// SIDE-EXITS before any effect.
817 ArrLoad {
818 /// Destination slot.
819 dst: Slot,
820 /// Index slot (1-based value).
821 idx: Slot,
822 /// Frame slot holding the pinned buffer pointer.
823 ptr_slot: Slot,
824 /// Frame slot holding the pinned length.
825 len_slot: Slot,
826 /// 1-byte elements (Bool buffers) instead of 8-byte.
827 byte: bool,
828 /// 4-byte SIGN-EXTENDED Int elements (`ListRepr::IntsI32`): the load is a
829 /// `movsxd` widening the stored `i32` to a full i64 (lossless). Mutually
830 /// exclusive with `byte`; both `false` ⇒ the default 8-byte element.
831 narrow32: bool,
832 /// Bounds-checked (`true`) or elided (`false`, the Oracle proved
833 /// the index in range — V8/LLVM bounds-check elimination). Unchecked
834 /// loads have no out-of-bounds continuation.
835 checked: bool,
836 },
837 /// FUSED affine-index load: `frame[dst] = buffer[idx - 1]` where the 1-based
838 /// `idx = (frame[a] OP frame[b]).wrapping_add(const_offset)` (or
839 /// `frame[a] + const_offset` for [`AffOp::None`]) is computed INSIDE the
840 /// stencil — the peephole collapses the index-arithmetic chain
841 /// (`<binop>(t = a OP b); [Add(t2 = t + Kc);] ArrLoad(t2)`, or
842 /// `Add(t = a + Kc); ArrLoad(t)`) plus the load into ONE piece when the
843 /// intermediate index temps are single-use scratch. The dominant indexed
844 /// inner loops (matrix_mult `i*n+k+1`, knapsack `w - wi + 1`, the `w + 1`
845 /// row read) lose worst to V8 precisely on this per-op dispatch; folding the
846 /// 2-3 index ops into the load is a direct dispatch-reduction win. Out of
847 /// bounds (incl. the 0/negative index the wrapping-sub trick catches)
848 /// SIDE-EXITS before any effect, exactly like [`MicroOp::ArrLoad`].
849 ArrLoadAffine {
850 /// Destination slot.
851 dst: Slot,
852 /// First index operand slot.
853 a: Slot,
854 /// The index op (`None` = single slot + const; else two-slot binop).
855 op: AffOp,
856 /// Second index operand slot (ignored for [`AffOp::None`]).
857 b: Slot,
858 /// The constant folded into the index (the trailing `+ Kc`; `0` if none).
859 const_offset: i64,
860 /// Frame slot holding the pinned buffer pointer.
861 ptr_slot: Slot,
862 /// Frame slot holding the pinned length.
863 len_slot: Slot,
864 /// Bounds-checked (`true`) or elided (`false`, the Oracle proved the
865 /// computed index in range). Unchecked has no out-of-bounds continuation.
866 checked: bool,
867 },
868 /// FUSED two-load float binop: `frame[dst] = bits(f(buf[i-1]) <op>
869 /// f(buf[j-1]))`, BOTH elements from the SAME pinned 8-byte buffer (one
870 /// pointer slot, one length slot). The peephole collapses `ArrLoad(t1 =
871 /// arr[i]); ArrLoad(t2 = arr[j]); {Add,Sub,Mul}F(dst = t1 OP t2)` when both
872 /// loads hit the same array and the two scratch loads are single-use — so
873 /// the two f64s never round-trip through the frame. EITHER index out of
874 /// bounds SIDE-EXITS before any effect, exactly like [`MicroOp::ArrLoad`].
875 ArrLoad2F {
876 /// Destination slot.
877 dst: Slot,
878 /// First index slot (1-based value).
879 i: Slot,
880 /// Second index slot (1-based value).
881 j: Slot,
882 /// Frame slot holding the pinned buffer pointer.
883 ptr_slot: Slot,
884 /// Frame slot holding the pinned length.
885 len_slot: Slot,
886 /// The float operation applied to the two loaded values.
887 op: FOp,
888 },
889 /// FUSED two-load INTEGER binop: `frame[dst] = a[i-1] <op> b[j-1]`, the two
890 /// 8-byte elements loaded from TWO (possibly distinct) pinned int buffers
891 /// (each with its own pointer + length slot). The peephole collapses
892 /// `ArrLoad(t1 = a[i]); ArrLoad(t2 = b[j]); {Add,Sub,Mul}(dst = t1 OP t2)`
893 /// — the matrix-multiply / dot-product idiom `c += a[..] * b[..]` — when the
894 /// two scratch loads are single-use, so the loaded i64s never round-trip the
895 /// frame and three dispatches collapse to one. EITHER index out of bounds
896 /// SIDE-EXITS before any effect, exactly like [`MicroOp::ArrLoad`]. The two
897 /// buffers may be the SAME (one self-referential dot product) or distinct.
898 ArrLoad2 {
899 /// Destination slot.
900 dst: Slot,
901 /// First index slot (1-based value), addressing buffer `a`.
902 i: Slot,
903 /// Second index slot (1-based value), addressing buffer `b`.
904 j: Slot,
905 /// Frame slot holding the pinned pointer of the first buffer (`a`).
906 ptr_a: Slot,
907 /// Frame slot holding the pinned length of the first buffer (`a`).
908 len_a: Slot,
909 /// Frame slot holding the pinned pointer of the second buffer (`b`).
910 ptr_b: Slot,
911 /// Frame slot holding the pinned length of the second buffer (`b`).
912 len_b: Slot,
913 /// The integer operation applied to the two loaded values.
914 op: IOp,
915 /// Bounds-checked (`true`) or elided (`false`, the Oracle proved both
916 /// indices in range). Unchecked has no out-of-bounds continuation.
917 checked: bool,
918 },
919 /// Pinned-array store: `buffer[frame[idx] - 1] = frame[src]`; bounds
920 /// side-exit BEFORE the store.
921 ArrStore {
922 /// Source slot.
923 src: Slot,
924 /// Index slot (1-based value).
925 idx: Slot,
926 /// Frame slot holding the pinned buffer pointer.
927 ptr_slot: Slot,
928 /// Frame slot holding the pinned length.
929 len_slot: Slot,
930 /// 1-byte elements (Bool buffers; stores normalize to 0/1).
931 byte: bool,
932 /// 4-byte SIGN-EXTENDED Int elements (`ListRepr::IntsI32`, the VM's
933 /// `LOGOS_NARROW_VM` half-width buffers): a store TRUNCATES the value to
934 /// its low 4 bytes (lossless under the narrowing proof). Mutually
935 /// exclusive with `byte`; both `false` ⇒ the default 8-byte element.
936 narrow32: bool,
937 /// Bounds-checked (`true`) or elided (`false`, the Oracle proved
938 /// the index in range). Unchecked stores have no side-exit.
939 checked: bool,
940 },
941 /// FUSED read-modify-write on a pinned 8-byte int array:
942 /// `buffer[frame[idx] - 1] = buffer[frame[idx] - 1] <op> frame[operand]`
943 /// in ONE stencil. The peephole collapses `ArrLoad(t = arr[idx]);
944 /// <int ALU>(t2 = t OP operand); ArrStore(arr[idx] = t2)` when both array
945 /// ops hit the SAME pinned buffer + index and the two scratch values
946 /// (`t`, `t2`) are single-use — so the element never round-trips the frame
947 /// and ONE bounds check covers the load+store. Out-of-bounds (incl.
948 /// 0/negative) SIDE-EXITS before any effect, exactly like [`MicroOp::ArrStore`].
949 ArrRMW {
950 /// Index slot (1-based value).
951 idx: Slot,
952 /// Operand slot (the RHS of the op; `frame[operand]`).
953 operand: Slot,
954 /// Frame slot holding the pinned buffer pointer.
955 ptr_slot: Slot,
956 /// Frame slot holding the pinned length.
957 len_slot: Slot,
958 /// The integer operation applied in place.
959 op: RmwOp,
960 /// Bounds-checked (`true`) or elided (`false`). Unchecked has no
961 /// out-of-bounds continuation.
962 checked: bool,
963 },
964 /// FUSED conditional adjacent swap on a pinned 8-byte int array:
965 /// `let a = buf[idx1-1]; let b = buf[idx2-1]; if cmp(a, b) { buf[idx1-1] = b;
966 /// buf[idx2-1] = a }` in ONE stencil. The peephole collapses the sort
967 /// inner-loop idiom `ArrLoad(a,i1); ArrLoad(b,i2); Branch(cmp,a,b,skip);
968 /// ArrStore(i1,b); ArrStore(i2,a)` (bubble/insertion/quick sort) — 5 ops → 1,
969 /// the two loaded values never round-trip the frame, the compare-branch is
970 /// gone. The swap is ATOMIC (both writes or neither); a checked variant
971 /// bounds-checks BOTH indices up front and SIDE-EXITS before any effect, the
972 /// unchecked variant (Oracle-proven indices) has no side-exit.
973 ArrCondSwap {
974 /// First index slot (1-based value).
975 idx1: Slot,
976 /// Second index slot (1-based value).
977 idx2: Slot,
978 /// Frame slot holding the pinned buffer pointer.
979 ptr_slot: Slot,
980 /// Frame slot holding the pinned length.
981 len_slot: Slot,
982 /// Swap when `cmp(buf[idx1-1], buf[idx2-1])` is TRUE.
983 cmp: Cmp,
984 /// Bounds-checked (`true`) or elided (`false`).
985 checked: bool,
986 },
987 /// FUSED unconditional adjacent swap on a pinned 8-byte int array:
988 /// `let a = buf[idx1-1]; let b = buf[idx2-1]; buf[idx1-1] = b; buf[idx2-1] =
989 /// a` in ONE stencil. The peephole collapses the `Let tmp = arr[i]; arr[i] =
990 /// arr[j]; arr[j] = tmp` exchange (quicksort/heap_sort/mergesort partition &
991 /// sift) — `ArrLoad(a,i); ArrLoad(b,j); ArrStore(i,b); ArrStore(j,a)` — 4 ops
992 /// → 1. The swap is ATOMIC; a checked variant bounds-checks BOTH indices up
993 /// front and SIDE-EXITS before any effect.
994 ArrSwap {
995 /// First index slot (1-based value).
996 idx1: Slot,
997 /// Second index slot (1-based value).
998 idx2: Slot,
999 /// Frame slot holding the pinned buffer pointer.
1000 ptr_slot: Slot,
1001 /// Frame slot holding the pinned length.
1002 len_slot: Slot,
1003 /// Bounds-checked (`true`) or elided (`false`).
1004 checked: bool,
1005 },
1006 /// FUSED float multiply-add: `frame[dst] = bits((f(a) * f(b)) + f(c))` — the
1007 /// peephole collapses `MulF(t = a*b); AddF(d = t + c)` (the product `t`
1008 /// single-use) into one stencil, cutting a dispatch on the float arithmetic
1009 /// chains that dominate the float cluster (nbody's `dz*dz + s`, dot
1010 /// products). The product and the add round SEPARATELY (`(a*b)+c`, two
1011 /// roundings) — NOT a hardware single-rounding FMA — so it stays bit-identical
1012 /// to the unfused `MulF`+`AddF`. Mem-form: every operand reads from the frame,
1013 /// so it preserves the threaded XMM pins (the caller only fuses when the
1014 /// operands are frame-resident, keeping it a spill-free win).
1015 FmaF {
1016 /// Destination slot.
1017 dst: Slot,
1018 /// First product operand slot.
1019 a: Slot,
1020 /// Second product operand slot.
1021 b: Slot,
1022 /// Addend slot.
1023 c: Slot,
1024 },
1025 /// Unconditional transfer to the micro-op at `target` (an index into the
1026 /// program; forward or backward).
1027 Jump {
1028 /// Micro-op index to transfer to.
1029 target: usize,
1030 },
1031 /// Transfer to `target` when frame\[cond\] is ZERO; fall through otherwise.
1032 JumpIfFalse {
1033 /// Condition slot (zero = jump).
1034 cond: Slot,
1035 /// Micro-op index to transfer to.
1036 target: usize,
1037 },
1038 /// Transfer to `target` when frame\[cond\] is NONZERO; fall through
1039 /// otherwise (the same brz stencil with swapped continuations).
1040 JumpIfTrue {
1041 /// Condition slot (nonzero = jump).
1042 cond: Slot,
1043 /// Micro-op index to transfer to.
1044 target: usize,
1045 },
1046 /// Terminate the chain, returning frame\[src\].
1047 Return {
1048 /// Slot whose value is returned.
1049 src: Slot,
1050 },
1051}
1052
1053/// Compile errors — structural, found before any code is emitted.
1054#[derive(Debug, PartialEq, Eq)]
1055pub enum JitCompileError {
1056 /// The program is empty.
1057 Empty,
1058 /// Execution can run off the end: the final op must be Return or Jump.
1059 FallsOffTheEnd,
1060 /// A jump target is outside the program.
1061 BadJumpTarget {
1062 /// Index of the offending op.
1063 op_index: usize,
1064 /// The out-of-range target.
1065 target: usize,
1066 },
1067 /// Assembly failed (missing hole/patch/map errors).
1068 Assembly(String),
1069 /// An op the per-piece stencil tier has no lowering for (e.g.
1070 /// [`MicroOp::StrAppend`], which is emitted ONLY for the contiguous regalloc
1071 /// backend). The caller declines (falls back to bytecode), so this is never a
1072 /// hard failure — it just routes around the stencil tier.
1073 Unsupported(&'static str),
1074}
1075
1076impl std::fmt::Display for JitCompileError {
1077 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1078 match self {
1079 JitCompileError::Empty => write!(f, "jit: empty program"),
1080 JitCompileError::FallsOffTheEnd => {
1081 write!(f, "jit: the final op must be Return or Jump")
1082 }
1083 JitCompileError::BadJumpTarget { op_index, target } => {
1084 write!(f, "jit: op {op_index} jumps to {target}, outside the program")
1085 }
1086 JitCompileError::Assembly(e) => write!(f, "jit: assembly failed: {e}"),
1087 JitCompileError::Unsupported(name) => {
1088 write!(f, "jit: the stencil tier has no lowering for {name}")
1089 }
1090 }
1091 }
1092}
1093
1094impl std::error::Error for JitCompileError {}
1095
1096/// What one chain run produced.
1097#[derive(Clone, Copy, Debug, PartialEq, Eq)]
1098pub enum ChainOutcome {
1099 /// The chain reached its Return; this is the returned value.
1100 Return(i64),
1101 /// A checked op side-exited; the payload is the RAW status-cell value.
1102 /// `1` is the plain marker (replay from the entry state — every effect
1103 /// confined to the private frame). `(pc << 2) | 3` is a PRECISE exit:
1104 /// effects already landed, resume at bytecode `pc`.
1105 Deopt(i64),
1106}
1107
1108impl ChainOutcome {
1109 /// The returned value; panics on [`ChainOutcome::Deopt`] — for tests and
1110 /// callers of chains that contain no checked ops.
1111 pub fn expect_return(self) -> i64 {
1112 match self {
1113 ChainOutcome::Return(v) => v,
1114 ChainOutcome::Deopt(_) => panic!("chain side-exited (deopt) — no return value"),
1115 }
1116 }
1117
1118 /// True when the run side-exited.
1119 pub fn is_deopt(&self) -> bool {
1120 matches!(self, ChainOutcome::Deopt(_))
1121 }
1122}
1123
1124/// A compiled chain plus its side-exit status cell (present only when the
1125/// program contains checked ops). The deopt stencil stores 1 through the
1126/// patched cell address; [`CompiledChain::run_with_frame`] reads-and-resets
1127/// it after every run.
1128#[derive(Debug)]
1129pub struct CompiledChain {
1130 chain: JitChain,
1131 status: Option<std::sync::Arc<AtomicI64>>,
1132 /// Extra cells the chain's machine code reads by baked address and that must
1133 /// therefore outlive the chain (the contiguous FUNCTION backend's self-call
1134 /// ENTRY cell: an `Arc<AtomicI64>` holding this chain's own entry address,
1135 /// loaded by every `CallSelf`/`CallSelfCopy` and written after mapping). The
1136 /// run/deopt contract is unchanged — these are pure keep-alives.
1137 #[allow(dead_code)]
1138 keepalive: Vec<std::sync::Arc<AtomicI64>>,
1139}
1140
1141impl CompiledChain {
1142 /// Wrap a raw `JitChain` (from the contiguous regalloc backend) and its
1143 /// shared status cell as a runnable `CompiledChain` — same run/deopt
1144 /// contract as a stencil chain.
1145 pub(crate) fn from_chain(
1146 chain: JitChain,
1147 status: Option<std::sync::Arc<AtomicI64>>,
1148 ) -> Self {
1149 CompiledChain { chain, status, keepalive: Vec::new() }
1150 }
1151
1152 /// Wrap a `JitChain` plus its status cell AND any extra cells the code reads
1153 /// by baked address (the self-call entry cell), which the `CompiledChain`
1154 /// then keeps alive for the chain's whole lifetime.
1155 pub(crate) fn from_chain_keepalive(
1156 chain: JitChain,
1157 status: Option<std::sync::Arc<AtomicI64>>,
1158 keepalive: Vec<std::sync::Arc<AtomicI64>>,
1159 ) -> Self {
1160 CompiledChain { chain, status, keepalive }
1161 }
1162
1163 /// Run over the given frame (slot 0 = VM register 0, …) with a fresh
1164 /// operand stack.
1165 pub fn run_with_frame(&self, frame: &mut [i64]) -> ChainOutcome {
1166 let v = self.chain.run_with_frame(frame);
1167 if let Some(cell) = &self.status {
1168 let raw = cell.swap(0, Ordering::Relaxed);
1169 if raw != 0 {
1170 return ChainOutcome::Deopt(raw);
1171 }
1172 }
1173 ChainOutcome::Return(v)
1174 }
1175
1176 /// The mapped code+pool bytes (diagnostics/tests).
1177 pub fn bytes(&self) -> &[u8] {
1178 self.chain.bytes()
1179 }
1180
1181 /// See [`crate::buffer::JitChain::patch_marked`].
1182 pub fn patch_marked(&self, value: u64) -> Result<(), crate::JitError> {
1183 self.chain.patch_marked(value)
1184 }
1185
1186 /// See [`crate::buffer::JitChain::has_patch_marks`].
1187 pub fn has_patch_marks(&self) -> bool {
1188 self.chain.has_patch_marks()
1189 }
1190
1191 /// The runtime base address (diagnostics/tests).
1192 pub fn base(&self) -> u64 {
1193 self.chain.base()
1194 }
1195
1196 /// Stencil pieces in the chain (diagnostics/tests).
1197 pub fn piece_count(&self) -> usize {
1198 self.chain.piece_count()
1199 }
1200}
1201
1202/// Lower a straight-line micro-op program into one executable stencil chain.
1203///
1204/// Emit ONE memory-form piece (an op outside the variant families) with
1205/// explicit continuation and deopt labels — the pinned compiler's bridge
1206/// to the classic stencils. Pinned operands were spilled before this
1207/// piece and pinned destinations reload after it.
1208/// The fused read-modify-write stencil for an op + bounds-check mode. The
1209/// checked twins side-exit through cont 1 on out-of-bounds; the unchecked `_u`
1210/// twins have no length hole and no out-of-bounds continuation.
1211fn rmw_stencil(op: RmwOp, checked: bool) -> &'static crate::Stencil {
1212 match (op, checked) {
1213 (RmwOp::Add, true) => &ST_ARRRMW_ADD,
1214 (RmwOp::Add, false) => &ST_ARRRMW_ADD_U,
1215 (RmwOp::Sub, true) => &ST_ARRRMW_SUB,
1216 (RmwOp::Sub, false) => &ST_ARRRMW_SUB_U,
1217 (RmwOp::Mul, true) => &ST_ARRRMW_MUL,
1218 (RmwOp::Mul, false) => &ST_ARRRMW_MUL_U,
1219 (RmwOp::And, true) => &ST_ARRRMW_AND,
1220 (RmwOp::And, false) => &ST_ARRRMW_AND_U,
1221 (RmwOp::Or, true) => &ST_ARRRMW_OR,
1222 (RmwOp::Or, false) => &ST_ARRRMW_OR_U,
1223 (RmwOp::Xor, true) => &ST_ARRRMW_XOR,
1224 (RmwOp::Xor, false) => &ST_ARRRMW_XOR_U,
1225 (RmwOp::AddF, true) => &ST_ARRRMW_ADDF,
1226 (RmwOp::AddF, false) => &ST_ARRRMW_ADDF_U,
1227 (RmwOp::SubF, true) => &ST_ARRRMW_SUBF,
1228 (RmwOp::SubF, false) => &ST_ARRRMW_SUBF_U,
1229 (RmwOp::MulF, true) => &ST_ARRRMW_MULF,
1230 (RmwOp::MulF, false) => &ST_ARRRMW_MULF_U,
1231 }
1232}
1233
1234/// The fused two-buffer integer-load binop stencil for an op + bounds-check
1235/// mode. The checked twins side-exit through cont 1 when EITHER index is out of
1236/// bounds; the unchecked `_u` twins have no length holes and no continuation.
1237fn ld2_int_stencil(op: IOp, checked: bool) -> &'static crate::Stencil {
1238 match (op, checked) {
1239 (IOp::Add, true) => &ST_ARRLD2_ADD,
1240 (IOp::Add, false) => &ST_ARRLD2_ADD_U,
1241 (IOp::Sub, true) => &ST_ARRLD2_SUB,
1242 (IOp::Sub, false) => &ST_ARRLD2_SUB_U,
1243 (IOp::Mul, true) => &ST_ARRLD2_MUL,
1244 (IOp::Mul, false) => &ST_ARRLD2_MUL_U,
1245 }
1246}
1247
1248/// The fused affine-index load stencil for an index op + bounds-check mode. The
1249/// checked twins side-exit through cont 1 when the COMPUTED index is out of
1250/// bounds; the unchecked `_u` twins have no length hole and no continuation.
1251/// The pinned-array LOAD stencil for an element width: 1-byte (Bool, zero-ext),
1252/// 4-byte (`IntsI32`, sign-ext), or the default 8-byte (Int/Float raw bits).
1253/// `byte` and `narrow32` are mutually exclusive (the adapter never sets both).
1254fn arrld_stencil(byte: bool, narrow32: bool, checked: bool) -> &'static crate::Stencil {
1255 match (byte, narrow32, checked) {
1256 (true, _, true) => &ST_ARRLDB,
1257 (true, _, false) => &ST_ARRLDB_U,
1258 (_, true, true) => &ST_ARRLD_I32,
1259 (_, true, false) => &ST_ARRLD_I32_U,
1260 (false, false, true) => &ST_ARRLD,
1261 (false, false, false) => &ST_ARRLD_U,
1262 }
1263}
1264
1265/// The pinned-array STORE stencil for an element width (see [`arrld_stencil`]).
1266fn arrst_stencil(byte: bool, narrow32: bool, checked: bool) -> &'static crate::Stencil {
1267 match (byte, narrow32, checked) {
1268 (true, _, true) => &ST_ARRSTB,
1269 (true, _, false) => &ST_ARRSTB_U,
1270 (_, true, true) => &ST_ARRST_I32,
1271 (_, true, false) => &ST_ARRST_I32_U,
1272 (false, false, true) => &ST_ARRST,
1273 (false, false, false) => &ST_ARRST_U,
1274 }
1275}
1276
1277fn affine_stencil(op: AffOp, checked: bool) -> &'static crate::Stencil {
1278 match (op, checked) {
1279 (AffOp::None, true) => &ST_ARRLDAFF_NONE,
1280 (AffOp::None, false) => &ST_ARRLDAFF_NONE_U,
1281 (AffOp::Add, true) => &ST_ARRLDAFF_ADD,
1282 (AffOp::Add, false) => &ST_ARRLDAFF_ADD_U,
1283 (AffOp::Sub, true) => &ST_ARRLDAFF_SUB,
1284 (AffOp::Sub, false) => &ST_ARRLDAFF_SUB_U,
1285 (AffOp::Mul, true) => &ST_ARRLDAFF_MUL,
1286 (AffOp::Mul, false) => &ST_ARRLDAFF_MUL_U,
1287 }
1288}
1289
1290/// The conditional-swap stencil for a comparison + bounds-check mode. The
1291/// peephole only ever emits the four orderings (Eq/NotEq swaps are nonsensical).
1292fn condswap_stencil(cmp: Cmp, checked: bool) -> &'static crate::Stencil {
1293 match (cmp, checked) {
1294 (Cmp::Gt, true) => &ST_ARRCONDSWAP_GT,
1295 (Cmp::Gt, false) => &ST_ARRCONDSWAP_GT_U,
1296 (Cmp::Lt, true) => &ST_ARRCONDSWAP_LT,
1297 (Cmp::Lt, false) => &ST_ARRCONDSWAP_LT_U,
1298 (Cmp::GtEq, true) => &ST_ARRCONDSWAP_GE,
1299 (Cmp::GtEq, false) => &ST_ARRCONDSWAP_GE_U,
1300 (Cmp::LtEq, true) => &ST_ARRCONDSWAP_LE,
1301 (Cmp::LtEq, false) => &ST_ARRCONDSWAP_LE_U,
1302 (Cmp::Eq | Cmp::NotEq, _) => unreachable!("cond-swap peephole never emits Eq/NotEq"),
1303 }
1304}
1305
1306fn emit_mem_form(
1307 buf: &mut JitBuffer,
1308 op: &MicroOp,
1309 next: crate::buffer::Label,
1310 deopt_piece: usize,
1311 status: &Option<std::sync::Arc<AtomicI64>>,
1312) {
1313 match *op {
1314 MicroOp::Div { dst, lhs, rhs } | MicroOp::Mod { dst, lhs, rhs } => {
1315 let stencil = if matches!(op, MicroOp::Div { .. }) { &ST_DIV3C } else { &ST_MOD3C };
1316 buf.push_stencil(
1317 stencil,
1318 &[
1319 HoleValue::Const(0, lhs as i64),
1320 HoleValue::Const(1, rhs as i64),
1321 HoleValue::Const(2, dst as i64),
1322 HoleValue::Cont(0, next),
1323 HoleValue::Cont(1, buf.label(deopt_piece)),
1324 ],
1325 );
1326 }
1327 MicroOp::DivPow2 { dst, lhs, k } => {
1328 buf.push_stencil(
1329 &ST_DIVPOW2,
1330 &[
1331 HoleValue::Const(0, lhs as i64),
1332 HoleValue::Const(1, k as i64),
1333 HoleValue::Const(2, (1i64 << k) - 1),
1334 HoleValue::Const(3, dst as i64),
1335 HoleValue::Cont(0, next),
1336 ],
1337 );
1338 }
1339 MicroOp::MagicDivU { dst, lhs, magic, more, mul_back } => {
1340 buf.push_stencil(
1341 &ST_MAGICDIV,
1342 &[
1343 HoleValue::Const(0, lhs as i64),
1344 HoleValue::Const(1, magic as i64),
1345 HoleValue::Const(2, more as i64),
1346 HoleValue::Const(3, mul_back),
1347 HoleValue::Const(4, dst as i64),
1348 HoleValue::Cont(0, next),
1349 ],
1350 );
1351 }
1352 MicroOp::AddF { dst, lhs, rhs }
1353 | MicroOp::SubF { dst, lhs, rhs }
1354 | MicroOp::MulF { dst, lhs, rhs } => {
1355 let stencil = match op {
1356 MicroOp::AddF { .. } => &ST_ADDF3,
1357 MicroOp::SubF { .. } => &ST_SUBF3,
1358 _ => &ST_MULF3,
1359 };
1360 buf.push_stencil(
1361 stencil,
1362 &[
1363 HoleValue::Const(0, lhs as i64),
1364 HoleValue::Const(1, rhs as i64),
1365 HoleValue::Const(2, dst as i64),
1366 HoleValue::Cont(0, next),
1367 ],
1368 );
1369 }
1370 MicroOp::DivF { dst, lhs, rhs } => {
1371 buf.push_stencil(
1372 &ST_DIVF3C,
1373 &[
1374 HoleValue::Const(0, lhs as i64),
1375 HoleValue::Const(1, rhs as i64),
1376 HoleValue::Const(2, dst as i64),
1377 HoleValue::Cont(0, next),
1378 HoleValue::Cont(1, buf.label(deopt_piece)),
1379 ],
1380 );
1381 }
1382 MicroOp::LtF { dst, lhs, rhs }
1383 | MicroOp::LtEqF { dst, lhs, rhs }
1384 | MicroOp::EqF { dst, lhs, rhs }
1385 | MicroOp::NeqF { dst, lhs, rhs } => {
1386 let stencil = match op {
1387 MicroOp::LtF { .. } => &ST_LTF3,
1388 MicroOp::LtEqF { .. } => &ST_LEF3,
1389 MicroOp::EqF { .. } => &ST_EQF3,
1390 _ => &ST_NEF3,
1391 };
1392 buf.push_stencil(
1393 stencil,
1394 &[
1395 HoleValue::Const(0, lhs as i64),
1396 HoleValue::Const(1, rhs as i64),
1397 HoleValue::Const(2, dst as i64),
1398 HoleValue::Cont(0, next),
1399 ],
1400 );
1401 }
1402 // a > b ⇔ b < a; a >= b ⇔ b <= a (Lever 2a): a VALUE-form float ordering
1403 // compare lowers mem-form by SWAPPING operands and reusing the LtF/LeF
1404 // stencil — IEEE-exact, NaN-false on both sides of the swap, bit-identical
1405 // to the tree-walker. (Float EQUALITY is epsilon-fuzzy and stays on its
1406 // own EqF/NeqF path — never swapped here.) This gives the pinned compiler
1407 // a lowering for `GtF`/`GtEqF`, so a region carrying a value-form float
1408 // `>`/`>=` no longer has to drop ALL its XMM pins.
1409 MicroOp::GtF { dst, lhs, rhs } | MicroOp::GtEqF { dst, lhs, rhs } => {
1410 let stencil = match op {
1411 MicroOp::GtF { .. } => &ST_LTF3,
1412 _ => &ST_LEF3,
1413 };
1414 buf.push_stencil(
1415 stencil,
1416 &[
1417 HoleValue::Const(0, rhs as i64),
1418 HoleValue::Const(1, lhs as i64),
1419 HoleValue::Const(2, dst as i64),
1420 HoleValue::Cont(0, next),
1421 ],
1422 );
1423 }
1424 MicroOp::NotInt { dst, src } | MicroOp::NotBool { dst, src }
1425 | MicroOp::IntToFloat { dst, src } | MicroOp::SqrtF { dst, src } => {
1426 let stencil = match op {
1427 MicroOp::NotInt { .. } => &ST_NOTI2,
1428 MicroOp::NotBool { .. } => &ST_NOTB2,
1429 MicroOp::SqrtF { .. } => &ST_SQRTF2,
1430 _ => &ST_I2F2,
1431 };
1432 buf.push_stencil(
1433 stencil,
1434 &[
1435 HoleValue::Const(0, src as i64),
1436 HoleValue::Const(1, dst as i64),
1437 HoleValue::Cont(0, next),
1438 ],
1439 );
1440 }
1441 MicroOp::Call {
1442 dst,
1443 args_start,
1444 table_addr,
1445 depth_addr,
1446 status_addr,
1447 limit_slot,
1448 depth_limit,
1449 } => {
1450 debug_assert!(status.is_some(), "calls require the status cell");
1451 buf.push_stencil(
1452 &ST_CALL,
1453 &[
1454 HoleValue::Const(0, table_addr),
1455 HoleValue::Const(1, args_start as i64),
1456 HoleValue::Const(2, dst as i64),
1457 HoleValue::Const(3, depth_addr),
1458 HoleValue::Const(4, status_addr),
1459 HoleValue::Const(5, limit_slot as i64),
1460 HoleValue::Const(6, depth_limit),
1461 HoleValue::Cont(0, next),
1462 ],
1463 );
1464 }
1465 MicroOp::MapGet { dst, key, map_slot, helper_addr } => {
1466 buf.push_stencil(
1467 &ST_MAPHGET,
1468 &[
1469 HoleValue::Const(0, map_slot as i64),
1470 HoleValue::Const(1, key as i64),
1471 HoleValue::Const(2, dst as i64),
1472 HoleValue::Const(3, helper_addr),
1473 HoleValue::Cont(0, next),
1474 HoleValue::Cont(1, buf.label(deopt_piece)),
1475 ],
1476 );
1477 }
1478 MicroOp::MapSet { src, key, map_slot, helper_addr } => {
1479 buf.push_stencil(
1480 &ST_MAPHSET,
1481 &[
1482 HoleValue::Const(0, map_slot as i64),
1483 HoleValue::Const(1, key as i64),
1484 HoleValue::Const(2, src as i64),
1485 HoleValue::Const(3, helper_addr),
1486 HoleValue::Cont(0, next),
1487 ],
1488 );
1489 }
1490 MicroOp::MemMem {
1491 h_ptr_slot,
1492 h_len_slot,
1493 n_ptr_slot,
1494 n_len_slot,
1495 needle_len_slot,
1496 i_slot,
1497 count_slot,
1498 helper_addr,
1499 } => {
1500 debug_assert!(status.is_some(), "memmem requires the status cell for its deopt exit");
1501 buf.push_stencil(
1502 &ST_MEMMEM,
1503 &[
1504 HoleValue::Const(0, h_ptr_slot as i64),
1505 HoleValue::Const(1, h_len_slot as i64),
1506 HoleValue::Const(2, n_ptr_slot as i64),
1507 HoleValue::Const(3, n_len_slot as i64),
1508 HoleValue::Const(4, needle_len_slot as i64),
1509 HoleValue::Const(5, i_slot as i64),
1510 HoleValue::Const(6, count_slot as i64),
1511 HoleValue::Const(7, helper_addr),
1512 HoleValue::Cont(0, next),
1513 HoleValue::Cont(1, buf.label(deopt_piece)),
1514 ],
1515 );
1516 }
1517 MicroOp::CallSelf { dst, args_start, depth_addr, status_addr, limit_slot, frame_size } => {
1518 buf.push_stencil(
1519 &ST_CALL_SELF,
1520 &[
1521 HoleValue::Const(0, 0),
1522 HoleValue::Const(1, args_start as i64),
1523 HoleValue::Const(2, dst as i64),
1524 HoleValue::Const(3, depth_addr),
1525 HoleValue::Const(4, status_addr),
1526 HoleValue::Const(5, limit_slot as i64),
1527 HoleValue::Const(6, frame_size),
1528 HoleValue::Cont(0, next),
1529 ],
1530 );
1531 buf.mark_patch_hole(0);
1532 }
1533 MicroOp::CallSelfCopy {
1534 dst,
1535 args_start,
1536 src_start,
1537 arg_count,
1538 depth_addr,
1539 status_addr,
1540 limit_slot,
1541 frame_size,
1542 } => {
1543 buf.push_stencil(
1544 &ST_CALL_SELF_COPY,
1545 &[
1546 HoleValue::Const(0, 0),
1547 HoleValue::Const(1, args_start as i64),
1548 HoleValue::Const(2, dst as i64),
1549 HoleValue::Const(3, depth_addr),
1550 HoleValue::Const(4, status_addr),
1551 HoleValue::Const(5, limit_slot as i64),
1552 HoleValue::Const(6, frame_size),
1553 HoleValue::Const(7, src_start as i64),
1554 HoleValue::Const(8, arg_count as i64),
1555 HoleValue::Cont(0, next),
1556 ],
1557 );
1558 buf.mark_patch_hole(0);
1559 }
1560 MicroOp::NewList { vec_slot, ptr_slot, len_slot, helper_addr } => {
1561 buf.push_stencil(
1562 &ST_ALLOCLIST,
1563 &[
1564 HoleValue::Const(0, vec_slot as i64),
1565 HoleValue::Const(1, ptr_slot as i64),
1566 HoleValue::Const(2, len_slot as i64),
1567 HoleValue::Const(3, helper_addr),
1568 HoleValue::Cont(0, next),
1569 ],
1570 );
1571 }
1572 MicroOp::ListTriple { handle_slot, vec_slot, ptr_slot, len_slot, helper_addr } => {
1573 buf.push_stencil(
1574 &ST_LISTTRIPLE,
1575 &[
1576 HoleValue::Const(0, handle_slot as i64),
1577 HoleValue::Const(1, vec_slot as i64),
1578 HoleValue::Const(2, ptr_slot as i64),
1579 HoleValue::Const(3, len_slot as i64),
1580 HoleValue::Const(4, helper_addr),
1581 HoleValue::Cont(0, next),
1582 ],
1583 );
1584 }
1585 MicroOp::MapHas { dst, key, map_slot, helper_addr } => {
1586 buf.push_stencil(
1587 &ST_MAPHHAS,
1588 &[
1589 HoleValue::Const(0, map_slot as i64),
1590 HoleValue::Const(1, key as i64),
1591 HoleValue::Const(2, dst as i64),
1592 HoleValue::Const(3, helper_addr),
1593 HoleValue::Cont(0, next),
1594 ],
1595 );
1596 }
1597 // Array load/store: 1-based, bounds-checked. A failed bound jumps to
1598 // the shared deopt terminal (region replay / function side-exit).
1599 // The pinned mem-form path has already spilled the pinned operands
1600 // (idx/ptr/len/src) to the frame, so these read frame slots exactly
1601 // like the unpinned chain.
1602 MicroOp::ArrLoad { dst, idx, ptr_slot, len_slot, byte, narrow32, checked } => {
1603 if checked {
1604 buf.push_stencil(
1605 arrld_stencil(byte, narrow32, true),
1606 &[
1607 HoleValue::Const(0, idx as i64),
1608 HoleValue::Const(1, ptr_slot as i64),
1609 HoleValue::Const(2, len_slot as i64),
1610 HoleValue::Const(3, dst as i64),
1611 HoleValue::Cont(0, next),
1612 HoleValue::Cont(1, buf.label(deopt_piece)),
1613 ],
1614 );
1615 } else {
1616 buf.push_stencil(
1617 arrld_stencil(byte, narrow32, false),
1618 &[
1619 HoleValue::Const(0, idx as i64),
1620 HoleValue::Const(1, ptr_slot as i64),
1621 HoleValue::Const(3, dst as i64),
1622 HoleValue::Cont(0, next),
1623 ],
1624 );
1625 }
1626 }
1627 MicroOp::ArrLoadAffine { dst, a, op, b, const_offset, ptr_slot, len_slot, checked } => {
1628 if checked {
1629 buf.push_stencil(
1630 affine_stencil(op, true),
1631 &[
1632 HoleValue::Const(0, a as i64),
1633 HoleValue::Const(1, b as i64),
1634 HoleValue::Const(2, const_offset),
1635 HoleValue::Const(3, ptr_slot as i64),
1636 HoleValue::Const(4, len_slot as i64),
1637 HoleValue::Const(5, dst as i64),
1638 HoleValue::Cont(0, next),
1639 HoleValue::Cont(1, buf.label(deopt_piece)),
1640 ],
1641 );
1642 } else {
1643 buf.push_stencil(
1644 affine_stencil(op, false),
1645 &[
1646 HoleValue::Const(0, a as i64),
1647 HoleValue::Const(1, b as i64),
1648 HoleValue::Const(2, const_offset),
1649 HoleValue::Const(3, ptr_slot as i64),
1650 HoleValue::Const(5, dst as i64),
1651 HoleValue::Cont(0, next),
1652 ],
1653 );
1654 }
1655 }
1656 MicroOp::ArrLoad2F { dst, i: ix, j: jx, ptr_slot, len_slot, op } => {
1657 let stencil = match op {
1658 FOp::Add => &ST_ARRLD2_ADDF,
1659 FOp::Sub => &ST_ARRLD2_SUBF,
1660 FOp::Mul => &ST_ARRLD2_MULF,
1661 };
1662 buf.push_stencil(
1663 stencil,
1664 &[
1665 HoleValue::Const(0, ix as i64),
1666 HoleValue::Const(1, jx as i64),
1667 HoleValue::Const(2, ptr_slot as i64),
1668 HoleValue::Const(3, len_slot as i64),
1669 HoleValue::Const(4, dst as i64),
1670 HoleValue::Cont(0, next),
1671 HoleValue::Cont(1, buf.label(deopt_piece)),
1672 ],
1673 );
1674 }
1675 MicroOp::ArrLoad2 { dst, i: ix, j: jx, ptr_a, len_a, ptr_b, len_b, op, checked } => {
1676 if checked {
1677 buf.push_stencil(
1678 ld2_int_stencil(op, true),
1679 &[
1680 HoleValue::Const(0, ix as i64),
1681 HoleValue::Const(1, jx as i64),
1682 HoleValue::Const(2, ptr_a as i64),
1683 HoleValue::Const(3, len_a as i64),
1684 HoleValue::Const(4, ptr_b as i64),
1685 HoleValue::Const(5, len_b as i64),
1686 HoleValue::Const(6, dst as i64),
1687 HoleValue::Cont(0, next),
1688 HoleValue::Cont(1, buf.label(deopt_piece)),
1689 ],
1690 );
1691 } else {
1692 buf.push_stencil(
1693 ld2_int_stencil(op, false),
1694 &[
1695 HoleValue::Const(0, ix as i64),
1696 HoleValue::Const(1, jx as i64),
1697 HoleValue::Const(2, ptr_a as i64),
1698 HoleValue::Const(3, ptr_b as i64),
1699 HoleValue::Const(4, dst as i64),
1700 HoleValue::Cont(0, next),
1701 ],
1702 );
1703 }
1704 }
1705 MicroOp::ArrStore { src, idx, ptr_slot, len_slot, byte, narrow32, checked } => {
1706 if checked {
1707 buf.push_stencil(
1708 arrst_stencil(byte, narrow32, true),
1709 &[
1710 HoleValue::Const(0, idx as i64),
1711 HoleValue::Const(1, ptr_slot as i64),
1712 HoleValue::Const(2, len_slot as i64),
1713 HoleValue::Const(3, src as i64),
1714 HoleValue::Cont(0, next),
1715 HoleValue::Cont(1, buf.label(deopt_piece)),
1716 ],
1717 );
1718 } else {
1719 buf.push_stencil(
1720 arrst_stencil(byte, narrow32, false),
1721 &[
1722 HoleValue::Const(0, idx as i64),
1723 HoleValue::Const(1, ptr_slot as i64),
1724 HoleValue::Const(3, src as i64),
1725 HoleValue::Cont(0, next),
1726 ],
1727 );
1728 }
1729 }
1730 MicroOp::ArrRMW { idx, operand, ptr_slot, len_slot, op, checked } => {
1731 if checked {
1732 buf.push_stencil(
1733 rmw_stencil(op, true),
1734 &[
1735 HoleValue::Const(0, idx as i64),
1736 HoleValue::Const(1, ptr_slot as i64),
1737 HoleValue::Const(2, len_slot as i64),
1738 HoleValue::Const(3, operand as i64),
1739 HoleValue::Cont(0, next),
1740 HoleValue::Cont(1, buf.label(deopt_piece)),
1741 ],
1742 );
1743 } else {
1744 buf.push_stencil(
1745 rmw_stencil(op, false),
1746 &[
1747 HoleValue::Const(0, idx as i64),
1748 HoleValue::Const(1, ptr_slot as i64),
1749 HoleValue::Const(3, operand as i64),
1750 HoleValue::Cont(0, next),
1751 ],
1752 );
1753 }
1754 }
1755 MicroOp::ArrCondSwap { idx1, idx2, ptr_slot, len_slot, cmp, checked } => {
1756 if checked {
1757 buf.push_stencil(
1758 condswap_stencil(cmp, true),
1759 &[
1760 HoleValue::Const(0, idx1 as i64),
1761 HoleValue::Const(1, idx2 as i64),
1762 HoleValue::Const(2, ptr_slot as i64),
1763 HoleValue::Const(3, len_slot as i64),
1764 HoleValue::Cont(0, next),
1765 HoleValue::Cont(1, buf.label(deopt_piece)),
1766 ],
1767 );
1768 } else {
1769 buf.push_stencil(
1770 condswap_stencil(cmp, false),
1771 &[
1772 HoleValue::Const(0, idx1 as i64),
1773 HoleValue::Const(1, idx2 as i64),
1774 HoleValue::Const(2, ptr_slot as i64),
1775 HoleValue::Cont(0, next),
1776 ],
1777 );
1778 }
1779 }
1780 MicroOp::ArrSwap { idx1, idx2, ptr_slot, len_slot, checked } => {
1781 if checked {
1782 buf.push_stencil(
1783 &ST_ARRSWAP,
1784 &[
1785 HoleValue::Const(0, idx1 as i64),
1786 HoleValue::Const(1, idx2 as i64),
1787 HoleValue::Const(2, ptr_slot as i64),
1788 HoleValue::Const(3, len_slot as i64),
1789 HoleValue::Cont(0, next),
1790 HoleValue::Cont(1, buf.label(deopt_piece)),
1791 ],
1792 );
1793 } else {
1794 buf.push_stencil(
1795 &ST_ARRSWAP_U,
1796 &[
1797 HoleValue::Const(0, idx1 as i64),
1798 HoleValue::Const(1, idx2 as i64),
1799 HoleValue::Const(2, ptr_slot as i64),
1800 HoleValue::Cont(0, next),
1801 ],
1802 );
1803 }
1804 }
1805 MicroOp::FmaF { dst, a, b, c } => {
1806 buf.push_stencil(
1807 &ST_FMAF,
1808 &[
1809 HoleValue::Const(0, a as i64),
1810 HoleValue::Const(1, b as i64),
1811 HoleValue::Const(2, c as i64),
1812 HoleValue::Const(3, dst as i64),
1813 HoleValue::Cont(0, next),
1814 ],
1815 );
1816 }
1817 // Helper-call push; refreshes the pinned ptr/len after a possible
1818 // realloc (the mem-form path reloads them from the frame after).
1819 MicroOp::ArrPush { src, vec_slot, ptr_slot, len_slot, helper_addr, byte: _, narrow32: _ } => {
1820 buf.push_stencil(
1821 &ST_PUSH,
1822 &[
1823 HoleValue::Const(0, src as i64),
1824 HoleValue::Const(1, vec_slot as i64),
1825 HoleValue::Const(2, ptr_slot as i64),
1826 HoleValue::Const(3, len_slot as i64),
1827 HoleValue::Const(4, helper_addr),
1828 HoleValue::Cont(0, next),
1829 ],
1830 );
1831 }
1832 ref other => unreachable!("emit_mem_form: unsupported op {other:?} (pinned chains exclude it)"),
1833 }
1834}
1835
1836/// REGISTER-THREADING compilation (EXODIA 3.1): up to four frame slots are
1837/// PINNED into the threaded registers r0..r3 for the whole chain. Pinned
1838/// operands ride registers through the generated location-variant
1839/// stencils; ops outside the variant families (Div, arrays, calls, …)
1840/// spill the pinned operands they read and reload the pinned slots they
1841/// write, so every memory-form piece still sees a coherent frame.
1842///
1843/// Frame-coherence contract: a pinned slot's FRAME cell is stale between
1844/// its spills — callers must only use pinning where nothing outside the
1845/// chain reads the frame mid-run (mode-A functions: replay re-enters from
1846/// the boundary arguments and the result returns by register).
1847pub fn compile_straightline_pinned(
1848 ops: &[MicroOp],
1849 pins: &[u16],
1850) -> Result<CompiledChain, JitCompileError> {
1851 compile_straightline_pinned_with(ops, pins, None)
1852}
1853
1854/// [`compile_straightline_pinned`] with a SHARED status cell (the function
1855/// tier's deopt/call seam). Integer/bool slots only — no XMM float pins.
1856pub fn compile_straightline_pinned_with(
1857 ops: &[MicroOp],
1858 pins: &[u16],
1859 shared_status: Option<std::sync::Arc<AtomicI64>>,
1860) -> Result<CompiledChain, JitCompileError> {
1861 compile_straightline_pinned_float(ops, pins, &[], shared_status)
1862}
1863
1864/// The full register-threading compiler with BOTH integer pins (`pins`,
1865/// threaded through the GP registers r0..r3) and FLOAT pins (`fpins`,
1866/// threaded through the XMM registers f0..f3). The two budgets are
1867/// independent — the threaded ABI carries `fn(base, sp, r0..r3, f0..f3)`, so
1868/// base/sp consume 2 of the 6 GP arg registers (leaving r0..r3) while all
1869/// four XMM pins sit in the 8 free XMM arg registers and never compete with
1870/// the integer pins.
1871///
1872/// A FLOAT slot pinned to `fN` keeps its f64 live in that XMM register across
1873/// every stencil — the pure float arith variants (V_FBINOP) read and write it
1874/// directly, and the mem-form float stencils (sqrt, divf, the float compares,
1875/// array load/store) THREAD f0..f3 unchanged, so any float pin they do not
1876/// themselves touch survives with no frame traffic. The ones a mem-form op
1877/// does read/write are spilled before / reloaded after, exactly like the GP
1878/// pins.
1879pub fn compile_straightline_pinned_float(
1880 ops: &[MicroOp],
1881 pins: &[u16],
1882 fpins: &[u16],
1883 shared_status: Option<std::sync::Arc<AtomicI64>>,
1884) -> Result<CompiledChain, JitCompileError> {
1885 if pins.is_empty() && fpins.is_empty() {
1886 return compile_straightline_coded(ops, shared_status, None, 0);
1887 }
1888 assert!(pins.len() <= 4, "at most four threaded GP registers");
1889 assert!(fpins.len() <= 6, "at most six threaded XMM registers (f0..f5)");
1890 debug_assert!(
1891 pins.iter().all(|p| !fpins.contains(p)),
1892 "a slot cannot be both GP- and float-pinned"
1893 );
1894 if ops.is_empty() {
1895 return Err(JitCompileError::Empty);
1896 }
1897 if !matches!(ops.last(), Some(MicroOp::Return { .. }) | Some(MicroOp::Jump { .. })) {
1898 return Err(JitCompileError::FallsOffTheEnd);
1899 }
1900 let loc = |slot: u16| -> usize {
1901 pins.iter().position(|&p| p == slot).map(|i| i + 1).unwrap_or(0)
1902 };
1903 // The XMM-pin location of a slot: 0 = frame, 1..4 = f0..f3.
1904 let floc = |slot: u16| -> usize {
1905 fpins.iter().position(|&p| p == slot).map(|i| i + 1).unwrap_or(0)
1906 };
1907 // Which pinned registers an op forces through MEMORY (spill before /
1908 // reload after) because it has no variant family.
1909 let mem_form_touch = |op: &MicroOp| -> (Vec<u16>, Vec<u16>) {
1910 let mut reads: Vec<u16> = Vec::new();
1911 let mut writes: Vec<u16> = Vec::new();
1912 match *op {
1913 MicroOp::Div { dst, lhs, rhs } | MicroOp::Mod { dst, lhs, rhs } => {
1914 reads.extend([lhs, rhs]);
1915 writes.push(dst);
1916 }
1917 MicroOp::DivPow2 { dst, lhs, .. } | MicroOp::MagicDivU { dst, lhs, .. } => {
1918 reads.push(lhs);
1919 writes.push(dst);
1920 }
1921 // Float add/sub/mul are register-threaded (V_FBINOP) — NOT
1922 // mem-form. DivF (div-by-zero side-exit) and the float comparisons
1923 // (no variant yet) stay mem-form.
1924 MicroOp::DivF { dst, lhs, rhs }
1925 | MicroOp::LtF { dst, lhs, rhs }
1926 | MicroOp::LtEqF { dst, lhs, rhs }
1927 | MicroOp::GtF { dst, lhs, rhs }
1928 | MicroOp::GtEqF { dst, lhs, rhs }
1929 | MicroOp::EqF { dst, lhs, rhs }
1930 | MicroOp::NeqF { dst, lhs, rhs } => {
1931 reads.extend([lhs, rhs]);
1932 writes.push(dst);
1933 }
1934 MicroOp::NotInt { dst, src }
1935 | MicroOp::NotBool { dst, src }
1936 | MicroOp::IntToFloat { dst, src }
1937 | MicroOp::SqrtF { dst, src } => {
1938 reads.push(src);
1939 writes.push(dst);
1940 }
1941 MicroOp::BranchF { lhs, rhs, .. } => reads.extend([lhs, rhs]),
1942 // ptr_slot/len_slot are read DIRECTLY from the frame by the array
1943 // stencil (or, when the array's base pointer is GP-pinned, from its
1944 // register via the RPTR variant) — never through the threaded-pin
1945 // spill path, so they are NOT listed as spill-reads here. (They are
1946 // loop-invariant; the array never moves unless pushed-to, and a
1947 // pushed array is not ptr-pinned.)
1948 MicroOp::ArrLoad { dst, idx, .. } => {
1949 reads.push(idx);
1950 writes.push(dst);
1951 }
1952 MicroOp::ArrLoadAffine { dst, a, op, b, .. } => {
1953 reads.push(a);
1954 if op != AffOp::None {
1955 reads.push(b);
1956 }
1957 writes.push(dst);
1958 }
1959 MicroOp::ArrLoad2F { dst, i: ix, j: jx, .. } => {
1960 reads.extend([ix, jx]);
1961 writes.push(dst);
1962 }
1963 MicroOp::ArrLoad2 { dst, i: ix, j: jx, .. } => {
1964 reads.extend([ix, jx]);
1965 writes.push(dst);
1966 }
1967 MicroOp::ArrStore { src, idx, .. } => {
1968 reads.extend([src, idx]);
1969 }
1970 MicroOp::ArrRMW { idx, operand, .. } => {
1971 reads.extend([idx, operand]);
1972 }
1973 MicroOp::ArrCondSwap { idx1, idx2, .. } => {
1974 reads.extend([idx1, idx2]);
1975 }
1976 MicroOp::ArrSwap { idx1, idx2, .. } => {
1977 reads.extend([idx1, idx2]);
1978 }
1979 MicroOp::FmaF { dst, a, b, c } => {
1980 reads.extend([a, b, c]);
1981 writes.push(dst);
1982 }
1983 MicroOp::ArrPush { src, vec_slot, ptr_slot, len_slot, .. } => {
1984 reads.extend([src, vec_slot, ptr_slot, len_slot]);
1985 writes.extend([ptr_slot, len_slot]);
1986 }
1987 MicroOp::MapGet { dst, key, map_slot, .. } => {
1988 reads.extend([key, map_slot]);
1989 writes.push(dst);
1990 }
1991 MicroOp::MapSet { src, key, map_slot, .. } => {
1992 reads.extend([src, key, map_slot]);
1993 }
1994 MicroOp::MapHas { dst, key, map_slot, .. } => {
1995 reads.extend([key, map_slot]);
1996 writes.push(dst);
1997 }
1998 MicroOp::MemMem {
1999 h_ptr_slot,
2000 h_len_slot,
2001 n_ptr_slot,
2002 n_len_slot,
2003 needle_len_slot,
2004 i_slot,
2005 count_slot,
2006 ..
2007 } => {
2008 // The helper reads every input slot from the frame and writes
2009 // `i_slot`/`count_slot` back — so a pinned operand must spill
2010 // first and a pinned destination reload after. (The recognizer
2011 // runs MemMem regions UNPINNED, so in practice these retain to
2012 // empty; kept for correctness if a pin ever survives.)
2013 reads.extend([
2014 h_ptr_slot,
2015 h_len_slot,
2016 n_ptr_slot,
2017 n_len_slot,
2018 needle_len_slot,
2019 i_slot,
2020 count_slot,
2021 ]);
2022 writes.extend([i_slot, count_slot]);
2023 }
2024 MicroOp::NewList { vec_slot, ptr_slot, len_slot, .. } => {
2025 writes.extend([vec_slot, ptr_slot, len_slot]);
2026 }
2027 MicroOp::ListTriple { handle_slot, vec_slot, ptr_slot, len_slot, .. } => {
2028 reads.push(handle_slot);
2029 writes.extend([vec_slot, ptr_slot, len_slot]);
2030 }
2031 MicroOp::Call { dst, .. } | MicroOp::CallSelf { dst, .. } => {
2032 // The callee window was staged through ordinary Moves; the
2033 // result lands in the frame.
2034 writes.push(dst);
2035 }
2036 MicroOp::CallSelfCopy { dst, src_start, arg_count, .. } => {
2037 // The fused stencil reads the SOURCE block from the FRAME, so
2038 // any source slot held in a pinned register must spill to its
2039 // frame cell before the copy; the result lands in the frame.
2040 reads.extend((src_start..src_start + arg_count).collect::<Vec<_>>());
2041 writes.push(dst);
2042 }
2043 _ => {}
2044 }
2045 // A touched slot needs spill/reload when it is pinned in EITHER
2046 // budget (GP r0..r3 or XMM f0..f3) — the spill/reload primitives
2047 // dispatch on which.
2048 let pinned = |s: u16| loc(s) != 0 || floc(s) != 0;
2049 reads.retain(|&r| pinned(r));
2050 reads.dedup();
2051 writes.retain(|&w| pinned(w));
2052 writes.dedup();
2053 (reads, writes)
2054 };
2055 let has_variant = |op: &MicroOp| -> bool {
2056 matches!(
2057 op,
2058 MicroOp::Add { .. }
2059 | MicroOp::Sub { .. }
2060 | MicroOp::Mul { .. }
2061 | MicroOp::BitAnd { .. }
2062 | MicroOp::BitOr { .. }
2063 | MicroOp::BitXor { .. }
2064 | MicroOp::Shl { .. }
2065 | MicroOp::Shr { .. }
2066 | MicroOp::Lt { .. }
2067 | MicroOp::LtEq { .. }
2068 | MicroOp::Eq { .. }
2069 | MicroOp::Neq { .. }
2070 | MicroOp::Gt { .. }
2071 | MicroOp::GtEq { .. }
2072 | MicroOp::Move { .. }
2073 | MicroOp::LoadConst { .. }
2074 | MicroOp::Return { .. }
2075 | MicroOp::Branch { .. }
2076 // Float add/sub/mul/div + sqrt — register-threaded through the
2077 // XMM-pin variant tables (V_FBINOP / V_DIVF / V_SQRTF), one
2078 // piece each. DivF still side-exits on a zero divisor; its
2079 // variant carries the deopt continuation.
2080 | MicroOp::AddF { .. }
2081 | MicroOp::SubF { .. }
2082 | MicroOp::MulF { .. }
2083 | MicroOp::DivF { .. }
2084 | MicroOp::SqrtF { .. }
2085 | MicroOp::IntToFloat { .. }
2086 | MicroOp::BranchF { .. }
2087 )
2088 };
2089
2090 let n_pin = pins.len() + fpins.len();
2091 // PASS 1: piece index per op (prologue reloads first).
2092 let mut op_piece: Vec<usize> = Vec::with_capacity(ops.len());
2093 let mut next_piece = n_pin;
2094 for op in ops {
2095 op_piece.push(next_piece);
2096 next_piece += match *op {
2097 // EPILOGUE (Phase 2 write-set pinning): a success exit spills every
2098 // pinned slot (GP and XMM) back to its frame cell so the VM's
2099 // write-back sees the loop-carried values — `n_pin` spill pieces +
2100 // the return.
2101 MicroOp::Return { .. } => n_pin + 1,
2102 _ if has_variant(op) => 1,
2103 MicroOp::Jump { .. } => 1,
2104 MicroOp::JumpIfFalse { cond, .. } | MicroOp::JumpIfTrue { cond, .. } => {
2105 if loc(cond) != 0 { 2 } else { 1 }
2106 }
2107 _ => {
2108 let (r, w) = mem_form_touch(op);
2109 r.len() + 1 + w.len()
2110 }
2111 };
2112 }
2113 let total_op_pieces = next_piece;
2114 let has_checked = ops.iter().any(|op| {
2115 matches!(
2116 op,
2117 // Integer add/sub/mul side-exit (`cont_1`) on signed overflow so the exact
2118 // tier promotes to BigInt — they need the status cell + deopt terminal.
2119 MicroOp::Add { .. }
2120 | MicroOp::Sub { .. }
2121 | MicroOp::Mul { .. }
2122 | MicroOp::Div { .. }
2123 | MicroOp::Mod { .. }
2124 | MicroOp::DivF { .. }
2125 | MicroOp::ArrLoad { .. }
2126 | MicroOp::ArrLoadAffine { checked: true, .. }
2127 | MicroOp::ArrLoad2F { .. }
2128 | MicroOp::ArrLoad2 { checked: true, .. }
2129 | MicroOp::ArrStore { .. }
2130 | MicroOp::ArrRMW { checked: true, .. }
2131 | MicroOp::ArrCondSwap { checked: true, .. }
2132 | MicroOp::ArrSwap { checked: true, .. }
2133 | MicroOp::Call { .. }
2134 | MicroOp::CallSelf { .. }
2135 | MicroOp::CallSelfCopy { .. }
2136 | MicroOp::MapGet { .. }
2137 | MicroOp::MemMem { .. }
2138 )
2139 });
2140 let status: Option<std::sync::Arc<AtomicI64>> = if has_checked {
2141 Some(shared_status.unwrap_or_else(|| std::sync::Arc::new(AtomicI64::new(0))))
2142 } else {
2143 None
2144 };
2145 let deopt_piece = total_op_pieces;
2146
2147 // PASS 2: emit.
2148 let mut buf = JitBuffer::new();
2149 // Prologue: reload each pinned slot from the frame — the GP pins into
2150 // r0..r3 (V_MOV), then the float pins into f0..f3 (V_FMOV). The pieces
2151 // are laid out GP-first, so piece index `i` for `i < pins.len()` reloads
2152 // GP pin `i`, and `pins.len() + j` reloads float pin `j`.
2153 for (i, &slot) in pins.iter().enumerate() {
2154 buf.push_stencil(
2155 V_MOV[i + 1][0],
2156 &[HoleValue::Const(0, slot as i64), HoleValue::Cont(0, buf.label(i + 1))],
2157 );
2158 }
2159 for (j, &slot) in fpins.iter().enumerate() {
2160 let here = pins.len() + j;
2161 buf.push_stencil(
2162 V_FMOV[j + 1][0],
2163 &[HoleValue::Const(0, slot as i64), HoleValue::Cont(0, buf.label(here + 1))],
2164 );
2165 }
2166 // Spill/reload a pinned slot — dispatching on its budget. A GP pin moves
2167 // through V_MOV (r0..r3 ↔ frame i64); a float pin through V_FMOV (f0..f3
2168 // ↔ frame f64 bits). A slot is in exactly one budget.
2169 let spill = |buf: &mut JitBuffer, slot: u16, after: usize| {
2170 if floc(slot) != 0 {
2171 buf.push_stencil(
2172 V_FMOV[0][floc(slot)],
2173 &[HoleValue::Const(1, slot as i64), HoleValue::Cont(0, buf.label(after))],
2174 );
2175 } else {
2176 buf.push_stencil(
2177 V_MOV[0][loc(slot)],
2178 &[HoleValue::Const(1, slot as i64), HoleValue::Cont(0, buf.label(after))],
2179 );
2180 }
2181 };
2182 let reload = |buf: &mut JitBuffer, slot: u16, after: usize| {
2183 if floc(slot) != 0 {
2184 buf.push_stencil(
2185 V_FMOV[floc(slot)][0],
2186 &[HoleValue::Const(0, slot as i64), HoleValue::Cont(0, buf.label(after))],
2187 );
2188 } else {
2189 buf.push_stencil(
2190 V_MOV[loc(slot)][0],
2191 &[HoleValue::Const(0, slot as i64), HoleValue::Cont(0, buf.label(after))],
2192 );
2193 }
2194 };
2195 // Jump threading: a continuation that lands on an unconditional `Jump`
2196 // follows it straight to the target, so the predecessor branches directly
2197 // and the per-iteration back-edge jmp every loop pays (`…; Jump(top)`) is
2198 // skipped. The Jump piece is still EMITTED (left as unreferenced dead code)
2199 // so piece indices — and every `op_piece[..]` label — stay valid.
2200 let no_thread = std::env::var_os("LOGOS_NOTHREAD").is_some();
2201 let thread = |start: usize| -> usize {
2202 if no_thread {
2203 return start;
2204 }
2205 let mut idx = start;
2206 for _ in 0..=ops.len() {
2207 match ops.get(idx) {
2208 Some(MicroOp::Jump { target }) => idx = *target,
2209 _ => break,
2210 }
2211 }
2212 idx
2213 };
2214 for (i, op) in ops.iter().enumerate() {
2215 let here = op_piece[i];
2216 debug_assert_eq!(
2217 buf.pieces_pushed(),
2218 here,
2219 "PASS1/PASS2 piece divergence before op {i} ({op:?}); pins={pins:?} fpins={fpins:?}"
2220 );
2221 let next_op = op_piece.get(thread(i + 1)).copied().unwrap_or(total_op_pieces);
2222 let lbl = |t: usize| -> usize { op_piece[thread(t)] };
2223 match *op {
2224 MicroOp::LoadConst { dst, value } => {
2225 let d = loc(dst);
2226 if d == 0 {
2227 buf.push_stencil(
2228 &ST_CONSTST,
2229 &[
2230 HoleValue::Const(0, value),
2231 HoleValue::Const(1, dst as i64),
2232 HoleValue::Cont(0, buf.label(next_op)),
2233 ],
2234 );
2235 } else {
2236 buf.push_stencil(
2237 V_CONST[d - 1],
2238 &[HoleValue::Const(0, value), HoleValue::Cont(0, buf.label(next_op))],
2239 );
2240 }
2241 }
2242 MicroOp::Move { dst, src } => {
2243 let (fd, fs) = (floc(dst), floc(src));
2244 if fd != 0 || fs != 0 {
2245 // FLOAT move: at least one operand rides an XMM pin, so this
2246 // is an f64 copy — thread it through V_FMOV (register f→f, or
2247 // a frame↔register half when the other end is frame-resident).
2248 // A float-pinned slot is never GP-pinned, so `loc` plays no
2249 // part. Register ends read/write the XMM directly (no hole);
2250 // a frame end uses hole 0 (src) or hole 1 (dst).
2251 let mut holes: Vec<HoleValue> = Vec::new();
2252 if fs == 0 {
2253 holes.push(HoleValue::Const(0, src as i64));
2254 }
2255 if fd == 0 {
2256 holes.push(HoleValue::Const(1, dst as i64));
2257 }
2258 holes.push(HoleValue::Cont(0, buf.label(next_op)));
2259 buf.push_stencil(V_FMOV[fd][fs], &holes);
2260 } else {
2261 let (d, sl) = (loc(dst), loc(src));
2262 if d == 0 && sl == 0 {
2263 buf.push_stencil(
2264 &ST_MOVSS,
2265 &[
2266 HoleValue::Const(0, src as i64),
2267 HoleValue::Const(1, dst as i64),
2268 HoleValue::Cont(0, buf.label(next_op)),
2269 ],
2270 );
2271 } else {
2272 let mut holes: Vec<HoleValue> = Vec::new();
2273 if sl == 0 {
2274 holes.push(HoleValue::Const(0, src as i64));
2275 }
2276 if d == 0 {
2277 holes.push(HoleValue::Const(1, dst as i64));
2278 }
2279 holes.push(HoleValue::Cont(0, buf.label(next_op)));
2280 buf.push_stencil(V_MOV[d][sl], &holes);
2281 }
2282 }
2283 }
2284 MicroOp::Add { dst, lhs, rhs }
2285 | MicroOp::Sub { dst, lhs, rhs }
2286 | MicroOp::Mul { dst, lhs, rhs }
2287 | MicroOp::BitAnd { dst, lhs, rhs }
2288 | MicroOp::BitOr { dst, lhs, rhs }
2289 | MicroOp::BitXor { dst, lhs, rhs }
2290 | MicroOp::Shl { dst, lhs, rhs }
2291 | MicroOp::Shr { dst, lhs, rhs }
2292 | MicroOp::Lt { dst, lhs, rhs }
2293 | MicroOp::LtEq { dst, lhs, rhs }
2294 | MicroOp::Eq { dst, lhs, rhs }
2295 | MicroOp::Neq { dst, lhs, rhs }
2296 | MicroOp::Gt { dst, lhs, rhs }
2297 | MicroOp::GtEq { dst, lhs, rhs } => {
2298 // Gt/GtEq map onto lt/le with swapped operands; Neq is ne.
2299 let (fam, a, b) = match *op {
2300 MicroOp::Add { .. } => (0usize, lhs, rhs),
2301 MicroOp::Sub { .. } => (1, lhs, rhs),
2302 MicroOp::Mul { .. } => (2, lhs, rhs),
2303 MicroOp::BitAnd { .. } => (3, lhs, rhs),
2304 MicroOp::BitOr { .. } => (4, lhs, rhs),
2305 MicroOp::BitXor { .. } => (5, lhs, rhs),
2306 MicroOp::Shl { .. } => (6, lhs, rhs),
2307 MicroOp::Shr { .. } => (7, lhs, rhs),
2308 MicroOp::Lt { .. } => (8, lhs, rhs),
2309 MicroOp::Gt { .. } => (8, rhs, lhs),
2310 MicroOp::LtEq { .. } => (9, lhs, rhs),
2311 MicroOp::GtEq { .. } => (9, rhs, lhs),
2312 MicroOp::Eq { .. } => (10, lhs, rhs),
2313 MicroOp::Neq { .. } => (11, lhs, rhs),
2314 _ => unreachable!(),
2315 };
2316 let (d, l, r) = (loc(dst), loc(a), loc(b));
2317 let mut holes: Vec<HoleValue> = Vec::new();
2318 if l == 0 {
2319 holes.push(HoleValue::Const(0, a as i64));
2320 }
2321 if r == 0 {
2322 holes.push(HoleValue::Const(1, b as i64));
2323 }
2324 if d == 0 {
2325 holes.push(HoleValue::Const(2, dst as i64));
2326 }
2327 holes.push(HoleValue::Cont(0, buf.label(next_op)));
2328 // add/sub/mul (fam 0/1/2) are the CHECKED families: their generated
2329 // stencil takes cont_1 to the shared deopt terminal on signed overflow,
2330 // so the exact tier recomputes and promotes to BigInt.
2331 if fam <= 2 {
2332 holes.push(HoleValue::Cont(1, buf.label(deopt_piece)));
2333 }
2334 buf.push_stencil(V_BINOP[fam][d][l][r], &holes);
2335 }
2336 // FLOAT 3-address (add/sub/mul): same location encoding, float
2337 // variant table.
2338 MicroOp::AddF { dst, lhs, rhs }
2339 | MicroOp::SubF { dst, lhs, rhs }
2340 | MicroOp::MulF { dst, lhs, rhs } => {
2341 let fam = match *op {
2342 MicroOp::AddF { .. } => 0usize,
2343 MicroOp::SubF { .. } => 1,
2344 MicroOp::MulF { .. } => 2,
2345 _ => unreachable!(),
2346 };
2347 // Float operands are XMM-pinned → index by the FLOAT pin
2348 // location (floc), NOT the GP location (loc). Using loc would
2349 // resolve an XMM-pinned float to frame (location 0) and read a
2350 // STALE frame cell instead of the live f-pin.
2351 let (d, l, r) = (floc(dst), floc(lhs), floc(rhs));
2352 let mut holes: Vec<HoleValue> = Vec::new();
2353 if l == 0 {
2354 holes.push(HoleValue::Const(0, lhs as i64));
2355 }
2356 if r == 0 {
2357 holes.push(HoleValue::Const(1, rhs as i64));
2358 }
2359 if d == 0 {
2360 holes.push(HoleValue::Const(2, dst as i64));
2361 }
2362 holes.push(HoleValue::Cont(0, buf.label(next_op)));
2363 buf.push_stencil(V_FBINOP[fam][d][l][r], &holes);
2364 }
2365 // FLOAT sqrt threaded through XMM pins (no side-exit).
2366 MicroOp::SqrtF { dst, src } => {
2367 let (d, s) = (floc(dst), floc(src));
2368 let mut holes: Vec<HoleValue> = Vec::new();
2369 if s == 0 {
2370 holes.push(HoleValue::Const(0, src as i64));
2371 }
2372 if d == 0 {
2373 holes.push(HoleValue::Const(1, dst as i64));
2374 }
2375 holes.push(HoleValue::Cont(0, buf.label(next_op)));
2376 buf.push_stencil(V_SQRTF[d][s], &holes);
2377 }
2378 // FLOAT divide threaded through XMM pins; a zero divisor side-exits
2379 // to the deopt terminal (cont 1), exactly like the mem-form form.
2380 MicroOp::DivF { dst, lhs, rhs } => {
2381 let (d, l, r) = (floc(dst), floc(lhs), floc(rhs));
2382 let mut holes: Vec<HoleValue> = Vec::new();
2383 if l == 0 {
2384 holes.push(HoleValue::Const(0, lhs as i64));
2385 }
2386 if r == 0 {
2387 holes.push(HoleValue::Const(1, rhs as i64));
2388 }
2389 if d == 0 {
2390 holes.push(HoleValue::Const(2, dst as i64));
2391 }
2392 holes.push(HoleValue::Cont(0, buf.label(next_op)));
2393 holes.push(HoleValue::Cont(1, buf.label(deopt_piece)));
2394 buf.push_stencil(V_DIVF[d][l][r], &holes);
2395 }
2396 // Int→Float threaded through XMM pins: int src at GP loc, f64 dst
2397 // at XMM pin.
2398 MicroOp::IntToFloat { dst, src } => {
2399 let (d, s) = (floc(dst), loc(src));
2400 let mut holes: Vec<HoleValue> = Vec::new();
2401 if s == 0 {
2402 holes.push(HoleValue::Const(0, src as i64));
2403 }
2404 if d == 0 {
2405 holes.push(HoleValue::Const(1, dst as i64));
2406 }
2407 holes.push(HoleValue::Cont(0, buf.label(next_op)));
2408 buf.push_stencil(V_I2F[d][s], &holes);
2409 }
2410 // FUSED float compare-and-branch threaded through XMM pins. Same
2411 // NaN-correct cmp→family mapping as the unpinned form (brlef for
2412 // <=/>=, never brltf-swapped — NaN makes `a<=b` ≠ `!(b<a)`).
2413 MicroOp::BranchF { cmp, lhs, rhs, target } => {
2414 let t = buf.label(lbl(target));
2415 let n = buf.label(next_op);
2416 let (fam, a, b) = match cmp {
2417 Cmp::Lt => (0usize, lhs, rhs),
2418 Cmp::Gt => (0, rhs, lhs),
2419 Cmp::LtEq => (1, lhs, rhs),
2420 Cmp::GtEq => (1, rhs, lhs),
2421 Cmp::Eq => (2, lhs, rhs),
2422 Cmp::NotEq => (2, lhs, rhs),
2423 };
2424 let (c0, c1) = if cmp == Cmp::NotEq { (t, n) } else { (n, t) };
2425 let (l, r) = (floc(a), floc(b));
2426 let mut holes: Vec<HoleValue> = Vec::new();
2427 if l == 0 {
2428 holes.push(HoleValue::Const(0, a as i64));
2429 }
2430 if r == 0 {
2431 holes.push(HoleValue::Const(1, b as i64));
2432 }
2433 holes.push(HoleValue::Cont(0, c0));
2434 holes.push(HoleValue::Cont(1, c1));
2435 buf.push_stencil(V_BRANCHF[fam][l][r], &holes);
2436 }
2437 MicroOp::Branch { cmp, lhs, rhs, target } => {
2438 // TRUE -> fall through (cont 0 of the variant is TAKEN when
2439 // the condition holds; mirror the unpinned mapping: jump
2440 // when FALSE).
2441 let t = buf.label(lbl(target));
2442 let n = buf.label(next_op);
2443 let (fam, a, b, c_true, c_false) = match cmp {
2444 Cmp::Lt => (0usize, lhs, rhs, n, t),
2445 Cmp::GtEq => (0, lhs, rhs, t, n),
2446 Cmp::Gt => (0, rhs, lhs, n, t),
2447 Cmp::LtEq => (0, rhs, lhs, t, n),
2448 Cmp::Eq => (2, lhs, rhs, n, t),
2449 Cmp::NotEq => (2, lhs, rhs, t, n),
2450 };
2451 let (l, r) = (loc(a), loc(b));
2452 let mut holes: Vec<HoleValue> = Vec::new();
2453 if l == 0 {
2454 holes.push(HoleValue::Const(0, a as i64));
2455 }
2456 if r == 0 {
2457 holes.push(HoleValue::Const(1, b as i64));
2458 }
2459 holes.push(HoleValue::Cont(0, c_true));
2460 holes.push(HoleValue::Cont(1, c_false));
2461 buf.push_stencil(V_BRANCH[fam][l][r], &holes);
2462 }
2463 MicroOp::Jump { target } => {
2464 buf.push_stencil(&ST_JUMP, &[HoleValue::Cont(0, buf.label(lbl(target)))]);
2465 }
2466 MicroOp::JumpIfFalse { cond, target } | MicroOp::JumpIfTrue { cond, target } => {
2467 let c = loc(cond);
2468 let mut brz_piece = here;
2469 if c != 0 {
2470 spill(&mut buf, cond, here + 1);
2471 brz_piece = here + 1;
2472 }
2473 let _ = brz_piece;
2474 let (c0, c1) = if matches!(op, MicroOp::JumpIfFalse { .. }) {
2475 (buf.label(next_op), buf.label(lbl(target)))
2476 } else {
2477 (buf.label(lbl(target)), buf.label(next_op))
2478 };
2479 buf.push_stencil(
2480 &ST_BRZ,
2481 &[HoleValue::Const(0, cond as i64), HoleValue::Cont(0, c0), HoleValue::Cont(1, c1)],
2482 );
2483 }
2484 MicroOp::Return { src } => {
2485 // EPILOGUE: spill every pinned slot to its frame cell before
2486 // returning, so the VM's success write-back (which reads the
2487 // frame) observes loop-carried values held in registers. Both
2488 // budgets spill — the GP pins through V_MOV, the float pins
2489 // through V_FMOV (the `spill` closure dispatches on which) —
2490 // matching the `n_pin + 1` pieces reserved for this return.
2491 // The deopt terminal is separate and discards the frame, so it
2492 // needs no spill — only this success path does.
2493 let mut cursor = here;
2494 for &slot in pins.iter().chain(fpins.iter()) {
2495 spill(&mut buf, slot, cursor + 1);
2496 cursor += 1;
2497 }
2498 let sl = loc(src);
2499 if sl == 0 {
2500 buf.push_stencil(&ST_RET2, &[HoleValue::Const(0, src as i64)]);
2501 } else {
2502 buf.push_stencil(V_RET[sl - 1], &[]);
2503 }
2504 }
2505 ref other => {
2506 // Memory-form op: spill pinned reads, run, reload pinned
2507 // writes — emitted as its own piece run.
2508 let (reads, writes) = mem_form_touch(other);
2509 let mut cursor = here;
2510 for &slot in &reads {
2511 spill(&mut buf, slot, cursor + 1);
2512 cursor += 1;
2513 }
2514 let after_op = cursor + 1;
2515 let next_for_op = if writes.is_empty() { next_op } else { after_op };
2516 let next_label = buf.label(next_for_op);
2517 // REGISTER-FORM array access: a word array whose base pointer is
2518 // GP-pinned reads the pointer from its register (no per-access
2519 // frame load). The index/length/value stay in the frame exactly
2520 // as the mem-form path, so the spill/reload bookkeeping above is
2521 // unchanged (mem_form_touch excludes ptr/len). Falls back to the
2522 // frame-pointer mem-form for byte arrays / unpinned pointers.
2523 let rptr = |buf: &mut JitBuffer, table: &[&'static crate::Stencil; 5],
2524 n: usize, idx: u16, vslot: u16, len_slot: u16, checked: bool| {
2525 let mut holes = vec![
2526 HoleValue::Const(0, idx as i64),
2527 HoleValue::Const(3, vslot as i64),
2528 HoleValue::Cont(0, next_label),
2529 ];
2530 if checked {
2531 holes.push(HoleValue::Const(2, len_slot as i64));
2532 holes.push(HoleValue::Cont(1, buf.label(deopt_piece)));
2533 }
2534 buf.push_stencil(table[n], &holes);
2535 };
2536 let used_rptr = match *other {
2537 MicroOp::ArrLoad { dst, idx, ptr_slot, len_slot, byte: false, narrow32: false, checked }
2538 if loc(ptr_slot) != 0 =>
2539 {
2540 let n = loc(ptr_slot);
2541 let t = if checked { &V_ARRLD_RPTR_C } else { &V_ARRLD_RPTR };
2542 rptr(&mut buf, t, n, idx, dst, len_slot, checked);
2543 true
2544 }
2545 MicroOp::ArrStore { src, idx, ptr_slot, len_slot, byte: false, narrow32: false, checked }
2546 if loc(ptr_slot) != 0 =>
2547 {
2548 let n = loc(ptr_slot);
2549 let t = if checked { &V_ARRST_RPTR_C } else { &V_ARRST_RPTR };
2550 rptr(&mut buf, t, n, idx, src, len_slot, checked);
2551 true
2552 }
2553 _ => false,
2554 };
2555 if !used_rptr {
2556 emit_mem_form(&mut buf, other, next_label, deopt_piece, &status);
2557 }
2558 cursor += 1;
2559 for (k, &slot) in writes.iter().enumerate() {
2560 let after = if k + 1 == writes.len() { next_op } else { cursor + 1 };
2561 reload(&mut buf, slot, after);
2562 cursor += 1;
2563 }
2564 }
2565 }
2566 }
2567 if let Some(cell) = &status {
2568 let addr = cell.as_ref() as *const AtomicI64 as i64;
2569 buf.push_stencil(&ST_DEOPT, &[HoleValue::Const(0, addr)]);
2570 }
2571 let chain = buf.finish().map_err(|e| JitCompileError::Assembly(e.to_string()))?;
2572 Ok(CompiledChain { chain, status, keepalive: Vec::new() })
2573}
2574
2575/// The returned chain is run with [`CompiledChain::run_with_frame`]; the
2576/// frame's slots are the program's registers (inputs pre-loaded by the
2577/// caller, outputs visible after the run). Programs containing checked ops
2578/// (Div/Mod) get a status cell and a shared side-exit terminal; their runs
2579/// can report [`ChainOutcome::Deopt`].
2580pub fn compile_straightline(ops: &[MicroOp]) -> Result<CompiledChain, JitCompileError> {
2581 compile_straightline_with(ops, None)
2582}
2583
2584/// [`compile_straightline`] with a caller-supplied (per-program) status
2585/// cell, so every chain of one program — and the call stencils between them
2586/// — share a single side-exit channel. The cell's address is what
2587/// `MicroOp::Call::status_addr` must carry.
2588pub fn compile_straightline_with(
2589 ops: &[MicroOp],
2590 shared_status: Option<std::sync::Arc<AtomicI64>>,
2591) -> Result<CompiledChain, JitCompileError> {
2592 compile_straightline_coded(ops, shared_status, None, 0)
2593}
2594
2595/// Like [`compile_straightline_with`], with an optional per-op DEOPT CODE
2596/// table (parallel to `ops`). An op whose code is not the plain marker `1`
2597/// side-exits through its own terminal piece storing that encoded value —
2598/// the function tier encodes `(bytecode_pc << 2) | 3` so the VM can resume
2599/// precisely. `None` (and code `1`) keep the shared replay terminal.
2600pub fn compile_straightline_coded(
2601 ops: &[MicroOp],
2602 shared_status: Option<std::sync::Arc<AtomicI64>>,
2603 deopt_codes: Option<&[i64]>,
2604 depth_addr: i64,
2605) -> Result<CompiledChain, JitCompileError> {
2606 if ops.is_empty() {
2607 return Err(JitCompileError::Empty);
2608 }
2609 if !matches!(ops.last(), Some(MicroOp::Return { .. }) | Some(MicroOp::Jump { .. })) {
2610 return Err(JitCompileError::FallsOffTheEnd);
2611 }
2612 for (i, op) in ops.iter().enumerate() {
2613 match *op {
2614 MicroOp::Jump { target }
2615 | MicroOp::JumpIfFalse { target, .. }
2616 | MicroOp::JumpIfTrue { target, .. }
2617 | MicroOp::Branch { target, .. }
2618 | MicroOp::BranchF { target, .. } => {
2619 if target >= ops.len() {
2620 return Err(JitCompileError::BadJumpTarget { op_index: i, target });
2621 }
2622 }
2623 _ => {}
2624 }
2625 }
2626
2627 // 3-address lowering: exactly ONE piece per micro-op (piece index ==
2628 // op index), so jump targets need no remapping. Programs with checked
2629 // ops share ONE side-exit terminal placed after every op piece; its
2630 // patched constant is the status cell's address.
2631 let has_checked = ops.iter().any(|op| {
2632 matches!(
2633 op,
2634 // Integer add/sub/mul side-exit (`cont_1`) on signed overflow so the exact
2635 // tier promotes to BigInt — they need the status cell + deopt terminal.
2636 MicroOp::Add { .. }
2637 | MicroOp::Sub { .. }
2638 | MicroOp::Mul { .. }
2639 | MicroOp::Div { .. }
2640 | MicroOp::Mod { .. }
2641 | MicroOp::DivF { .. }
2642 | MicroOp::ArrLoad { .. }
2643 | MicroOp::ArrLoadAffine { checked: true, .. }
2644 | MicroOp::ArrLoad2F { .. }
2645 | MicroOp::ArrLoad2 { checked: true, .. }
2646 | MicroOp::ArrStore { .. }
2647 | MicroOp::ArrRMW { checked: true, .. }
2648 | MicroOp::ArrCondSwap { checked: true, .. }
2649 | MicroOp::ArrSwap { checked: true, .. }
2650 | MicroOp::Call { .. }
2651 | MicroOp::CallSelf { .. }
2652 | MicroOp::CallSelfCopy { .. }
2653 | MicroOp::MapGet { .. }
2654 | MicroOp::MemMem { .. }
2655 )
2656 });
2657 let status: Option<std::sync::Arc<AtomicI64>> = if has_checked {
2658 Some(shared_status.unwrap_or_else(|| std::sync::Arc::new(AtomicI64::new(0))))
2659 } else {
2660 None
2661 };
2662 let deopt_piece = ops.len();
2663 // Distinct non-plain codes get their own terminal pieces after the
2664 // shared one, in first-use order (deterministic).
2665 let mut code_pieces: Vec<i64> = Vec::new();
2666 if let Some(codes) = deopt_codes {
2667 debug_assert_eq!(codes.len(), ops.len());
2668 for (i, op) in ops.iter().enumerate() {
2669 let coded = matches!(
2670 op,
2671 // add/sub/mul are CHECKED now (overflow → exact promotion): inside a
2672 // region their side-exit must carry the op's PRECISE deopt code so the
2673 // VM resumes with the right rollback, exactly like the other checked
2674 // ops — so their codes need terminal pieces too.
2675 MicroOp::Add { .. }
2676 | MicroOp::Sub { .. }
2677 | MicroOp::Mul { .. }
2678 | MicroOp::Div { .. }
2679 | MicroOp::Mod { .. }
2680 | MicroOp::DivF { .. }
2681 | MicroOp::ArrLoad { .. }
2682 | MicroOp::ArrLoadAffine { checked: true, .. }
2683 | MicroOp::ArrLoad2F { .. }
2684 | MicroOp::ArrLoad2 { checked: true, .. }
2685 | MicroOp::ArrStore { .. }
2686 | MicroOp::MapGet { .. }
2687 );
2688 if coded && codes[i] != 1 && !code_pieces.contains(&codes[i]) {
2689 code_pieces.push(codes[i]);
2690 }
2691 }
2692 }
2693 let piece_for = |code: i64| -> usize {
2694 if code == 1 {
2695 deopt_piece
2696 } else {
2697 deopt_piece + 1 + code_pieces.iter().position(|&c| c == code).unwrap()
2698 }
2699 };
2700 let code_at = |i: usize| -> i64 { deopt_codes.map(|c| c[i]).unwrap_or(1) };
2701
2702 let mut buf = JitBuffer::new();
2703 for (i, op) in ops.iter().enumerate() {
2704 let next = buf.label(i + 1);
2705 match *op {
2706 MicroOp::LoadConst { dst, value } => {
2707 buf.push_stencil(
2708 &ST_CONSTST,
2709 &[
2710 HoleValue::Const(0, value),
2711 HoleValue::Const(1, dst as i64),
2712 HoleValue::Cont(0, next),
2713 ],
2714 );
2715 }
2716 MicroOp::Move { dst, src } => {
2717 buf.push_stencil(
2718 &ST_MOVSS,
2719 &[
2720 HoleValue::Const(0, src as i64),
2721 HoleValue::Const(1, dst as i64),
2722 HoleValue::Cont(0, next),
2723 ],
2724 );
2725 }
2726 MicroOp::Add { dst, lhs, rhs }
2727 | MicroOp::Sub { dst, lhs, rhs }
2728 | MicroOp::Mul { dst, lhs, rhs }
2729 | MicroOp::Lt { dst, lhs, rhs }
2730 | MicroOp::LtEq { dst, lhs, rhs }
2731 | MicroOp::Eq { dst, lhs, rhs }
2732 | MicroOp::Neq { dst, lhs, rhs } => {
2733 let stencil = match op {
2734 MicroOp::Add { .. } => &ST_ADD3,
2735 MicroOp::Sub { .. } => &ST_SUB3,
2736 MicroOp::Mul { .. } => &ST_MUL3,
2737 MicroOp::Lt { .. } => &ST_LT3,
2738 MicroOp::LtEq { .. } => &ST_LE3,
2739 MicroOp::Eq { .. } => &ST_EQ3,
2740 MicroOp::Neq { .. } => &ST_NE3,
2741 _ => unreachable!(),
2742 };
2743 let mut holes = vec![
2744 HoleValue::Const(0, lhs as i64),
2745 HoleValue::Const(1, rhs as i64),
2746 HoleValue::Const(2, dst as i64),
2747 HoleValue::Cont(0, next),
2748 ];
2749 // add/sub/mul are CHECKED: their stencil takes cont_1 to this op's
2750 // deopt terminal on signed overflow so the exact tier promotes. The
2751 // comparisons (lt/le/eq/ne) never overflow → cont_0 only.
2752 if matches!(op, MicroOp::Add { .. } | MicroOp::Sub { .. } | MicroOp::Mul { .. }) {
2753 holes.push(HoleValue::Cont(1, buf.label(piece_for(code_at(i)))));
2754 }
2755 buf.push_stencil(stencil, &holes);
2756 }
2757 // a > b ⇔ b < a; a >= b ⇔ b <= a: swap operands, reuse lt3/le3
2758 // (IEEE-exact for the float twins too — NaN is false on both
2759 // sides of the swap).
2760 MicroOp::Gt { dst, lhs, rhs }
2761 | MicroOp::GtEq { dst, lhs, rhs }
2762 | MicroOp::GtF { dst, lhs, rhs }
2763 | MicroOp::GtEqF { dst, lhs, rhs } => {
2764 let stencil = match op {
2765 MicroOp::Gt { .. } => &ST_LT3,
2766 MicroOp::GtEq { .. } => &ST_LE3,
2767 MicroOp::GtF { .. } => &ST_LTF3,
2768 _ => &ST_LEF3,
2769 };
2770 buf.push_stencil(
2771 stencil,
2772 &[
2773 HoleValue::Const(0, rhs as i64),
2774 HoleValue::Const(1, lhs as i64),
2775 HoleValue::Const(2, dst as i64),
2776 HoleValue::Cont(0, next),
2777 ],
2778 );
2779 }
2780 MicroOp::BitAnd { dst, lhs, rhs }
2781 | MicroOp::BitOr { dst, lhs, rhs }
2782 | MicroOp::BitXor { dst, lhs, rhs }
2783 | MicroOp::Shl { dst, lhs, rhs }
2784 | MicroOp::Shr { dst, lhs, rhs }
2785 | MicroOp::AddF { dst, lhs, rhs }
2786 | MicroOp::SubF { dst, lhs, rhs }
2787 | MicroOp::MulF { dst, lhs, rhs }
2788 | MicroOp::LtF { dst, lhs, rhs }
2789 | MicroOp::LtEqF { dst, lhs, rhs }
2790 | MicroOp::EqF { dst, lhs, rhs }
2791 | MicroOp::NeqF { dst, lhs, rhs } => {
2792 let stencil = match op {
2793 MicroOp::BitAnd { .. } => &ST_AND3,
2794 MicroOp::BitOr { .. } => &ST_OR3,
2795 MicroOp::BitXor { .. } => &ST_XOR3,
2796 MicroOp::Shl { .. } => &ST_SHL3,
2797 MicroOp::Shr { .. } => &ST_SHR3,
2798 MicroOp::AddF { .. } => &ST_ADDF3,
2799 MicroOp::SubF { .. } => &ST_SUBF3,
2800 MicroOp::MulF { .. } => &ST_MULF3,
2801 MicroOp::LtF { .. } => &ST_LTF3,
2802 MicroOp::LtEqF { .. } => &ST_LEF3,
2803 MicroOp::EqF { .. } => &ST_EQF3,
2804 _ => &ST_NEF3,
2805 };
2806 buf.push_stencil(
2807 stencil,
2808 &[
2809 HoleValue::Const(0, lhs as i64),
2810 HoleValue::Const(1, rhs as i64),
2811 HoleValue::Const(2, dst as i64),
2812 HoleValue::Cont(0, next),
2813 ],
2814 );
2815 }
2816 MicroOp::DivF { dst, lhs, rhs } => {
2817 buf.push_stencil(
2818 &ST_DIVF3C,
2819 &[
2820 HoleValue::Const(0, lhs as i64),
2821 HoleValue::Const(1, rhs as i64),
2822 HoleValue::Const(2, dst as i64),
2823 HoleValue::Cont(0, next),
2824 HoleValue::Cont(1, buf.label(piece_for(code_at(i)))),
2825 ],
2826 );
2827 }
2828 MicroOp::Call {
2829 dst,
2830 args_start,
2831 table_addr,
2832 depth_addr,
2833 status_addr,
2834 limit_slot,
2835 depth_limit,
2836 } => {
2837 let code = code_at(i);
2838 if code != 1 {
2839 // Precise variant: hole 6 carries the encoded deopt
2840 // value; the depth limit is BAKED at the kernel's
2841 // locked MAX_CALL_DEPTH (the adapter verified it).
2842 debug_assert_eq!(depth_limit, BAKED_CALL_DEPTH);
2843 buf.push_stencil(
2844 &ST_CALL_PRECISE,
2845 &[
2846 HoleValue::Const(0, table_addr),
2847 HoleValue::Const(1, args_start as i64),
2848 HoleValue::Const(2, dst as i64),
2849 HoleValue::Const(3, depth_addr),
2850 HoleValue::Const(4, status_addr),
2851 HoleValue::Const(5, limit_slot as i64),
2852 HoleValue::Const(6, code),
2853 HoleValue::Cont(0, next),
2854 ],
2855 );
2856 } else {
2857 buf.push_stencil(
2858 &ST_CALL,
2859 &[
2860 HoleValue::Const(0, table_addr),
2861 HoleValue::Const(1, args_start as i64),
2862 HoleValue::Const(2, dst as i64),
2863 HoleValue::Const(3, depth_addr),
2864 HoleValue::Const(4, status_addr),
2865 HoleValue::Const(5, limit_slot as i64),
2866 HoleValue::Const(6, depth_limit),
2867 HoleValue::Cont(0, next),
2868 ],
2869 );
2870 }
2871 }
2872 MicroOp::CallSelf { dst, args_start, depth_addr, status_addr, limit_slot, frame_size } => {
2873 buf.push_stencil(
2874 &ST_CALL_SELF,
2875 &[
2876 HoleValue::Const(0, 0),
2877 HoleValue::Const(1, args_start as i64),
2878 HoleValue::Const(2, dst as i64),
2879 HoleValue::Const(3, depth_addr),
2880 HoleValue::Const(4, status_addr),
2881 HoleValue::Const(5, limit_slot as i64),
2882 HoleValue::Const(6, frame_size),
2883 HoleValue::Cont(0, next),
2884 ],
2885 );
2886 buf.mark_patch_hole(0);
2887 }
2888 MicroOp::CallSelfCopy {
2889 dst,
2890 args_start,
2891 src_start,
2892 arg_count,
2893 depth_addr,
2894 status_addr,
2895 limit_slot,
2896 frame_size,
2897 } => {
2898 buf.push_stencil(
2899 &ST_CALL_SELF_COPY,
2900 &[
2901 HoleValue::Const(0, 0),
2902 HoleValue::Const(1, args_start as i64),
2903 HoleValue::Const(2, dst as i64),
2904 HoleValue::Const(3, depth_addr),
2905 HoleValue::Const(4, status_addr),
2906 HoleValue::Const(5, limit_slot as i64),
2907 HoleValue::Const(6, frame_size),
2908 HoleValue::Const(7, src_start as i64),
2909 HoleValue::Const(8, arg_count as i64),
2910 HoleValue::Cont(0, next),
2911 ],
2912 );
2913 buf.mark_patch_hole(0);
2914 }
2915 MicroOp::NewList { vec_slot, ptr_slot, len_slot, helper_addr } => {
2916 buf.push_stencil(
2917 &ST_ALLOCLIST,
2918 &[
2919 HoleValue::Const(0, vec_slot as i64),
2920 HoleValue::Const(1, ptr_slot as i64),
2921 HoleValue::Const(2, len_slot as i64),
2922 HoleValue::Const(3, helper_addr),
2923 HoleValue::Cont(0, next),
2924 ],
2925 );
2926 }
2927 MicroOp::ListTriple { handle_slot, vec_slot, ptr_slot, len_slot, helper_addr } => {
2928 buf.push_stencil(
2929 &ST_LISTTRIPLE,
2930 &[
2931 HoleValue::Const(0, handle_slot as i64),
2932 HoleValue::Const(1, vec_slot as i64),
2933 HoleValue::Const(2, ptr_slot as i64),
2934 HoleValue::Const(3, len_slot as i64),
2935 HoleValue::Const(4, helper_addr),
2936 HoleValue::Cont(0, next),
2937 ],
2938 );
2939 }
2940 MicroOp::MapGet { dst, key, map_slot, helper_addr } => {
2941 buf.push_stencil(
2942 &ST_MAPHGET,
2943 &[
2944 HoleValue::Const(0, map_slot as i64),
2945 HoleValue::Const(1, key as i64),
2946 HoleValue::Const(2, dst as i64),
2947 HoleValue::Const(3, helper_addr),
2948 HoleValue::Cont(0, next),
2949 HoleValue::Cont(1, buf.label(piece_for(code_at(i)))),
2950 ],
2951 );
2952 }
2953 MicroOp::MapSet { src, key, map_slot, helper_addr } => {
2954 buf.push_stencil(
2955 &ST_MAPHSET,
2956 &[
2957 HoleValue::Const(0, map_slot as i64),
2958 HoleValue::Const(1, key as i64),
2959 HoleValue::Const(2, src as i64),
2960 HoleValue::Const(3, helper_addr),
2961 HoleValue::Cont(0, next),
2962 ],
2963 );
2964 }
2965 MicroOp::MemMem {
2966 h_ptr_slot,
2967 h_len_slot,
2968 n_ptr_slot,
2969 n_len_slot,
2970 needle_len_slot,
2971 i_slot,
2972 count_slot,
2973 helper_addr,
2974 } => {
2975 buf.push_stencil(
2976 &ST_MEMMEM,
2977 &[
2978 HoleValue::Const(0, h_ptr_slot as i64),
2979 HoleValue::Const(1, h_len_slot as i64),
2980 HoleValue::Const(2, n_ptr_slot as i64),
2981 HoleValue::Const(3, n_len_slot as i64),
2982 HoleValue::Const(4, needle_len_slot as i64),
2983 HoleValue::Const(5, i_slot as i64),
2984 HoleValue::Const(6, count_slot as i64),
2985 HoleValue::Const(7, helper_addr),
2986 HoleValue::Cont(0, next),
2987 HoleValue::Cont(1, buf.label(piece_for(code_at(i)))),
2988 ],
2989 );
2990 }
2991 MicroOp::MapHas { dst, key, map_slot, helper_addr } => {
2992 buf.push_stencil(
2993 &ST_MAPHHAS,
2994 &[
2995 HoleValue::Const(0, map_slot as i64),
2996 HoleValue::Const(1, key as i64),
2997 HoleValue::Const(2, dst as i64),
2998 HoleValue::Const(3, helper_addr),
2999 HoleValue::Cont(0, next),
3000 ],
3001 );
3002 }
3003 MicroOp::ArrPush { src, vec_slot, ptr_slot, len_slot, helper_addr, byte: _, narrow32: _ } => {
3004 buf.push_stencil(
3005 &ST_PUSH,
3006 &[
3007 HoleValue::Const(0, src as i64),
3008 HoleValue::Const(1, vec_slot as i64),
3009 HoleValue::Const(2, ptr_slot as i64),
3010 HoleValue::Const(3, len_slot as i64),
3011 HoleValue::Const(4, helper_addr),
3012 HoleValue::Cont(0, next),
3013 ],
3014 );
3015 }
3016 MicroOp::ListClear { vec_slot, ptr_slot, len_slot, helper_addr } => {
3017 buf.push_stencil(
3018 &ST_LIST_CLEAR,
3019 &[
3020 HoleValue::Const(0, vec_slot as i64),
3021 HoleValue::Const(1, ptr_slot as i64),
3022 HoleValue::Const(2, len_slot as i64),
3023 HoleValue::Const(3, helper_addr),
3024 HoleValue::Cont(0, next),
3025 ],
3026 );
3027 }
3028 MicroOp::ArrLoad { dst, idx, ptr_slot, len_slot, byte, narrow32, checked } => {
3029 if checked {
3030 buf.push_stencil(
3031 arrld_stencil(byte, narrow32, true),
3032 &[
3033 HoleValue::Const(0, idx as i64),
3034 HoleValue::Const(1, ptr_slot as i64),
3035 HoleValue::Const(2, len_slot as i64),
3036 HoleValue::Const(3, dst as i64),
3037 HoleValue::Cont(0, next),
3038 HoleValue::Cont(1, buf.label(piece_for(code_at(i)))),
3039 ],
3040 );
3041 } else {
3042 // Bounds-check eliminated (Oracle-proven): no length hole,
3043 // no out-of-bounds continuation.
3044 buf.push_stencil(
3045 arrld_stencil(byte, narrow32, false),
3046 &[
3047 HoleValue::Const(0, idx as i64),
3048 HoleValue::Const(1, ptr_slot as i64),
3049 HoleValue::Const(3, dst as i64),
3050 HoleValue::Cont(0, next),
3051 ],
3052 );
3053 }
3054 }
3055 MicroOp::ArrLoadAffine { dst, a, op, b, const_offset, ptr_slot, len_slot, checked } => {
3056 if checked {
3057 buf.push_stencil(
3058 affine_stencil(op, true),
3059 &[
3060 HoleValue::Const(0, a as i64),
3061 HoleValue::Const(1, b as i64),
3062 HoleValue::Const(2, const_offset),
3063 HoleValue::Const(3, ptr_slot as i64),
3064 HoleValue::Const(4, len_slot as i64),
3065 HoleValue::Const(5, dst as i64),
3066 HoleValue::Cont(0, next),
3067 HoleValue::Cont(1, buf.label(piece_for(code_at(i)))),
3068 ],
3069 );
3070 } else {
3071 buf.push_stencil(
3072 affine_stencil(op, false),
3073 &[
3074 HoleValue::Const(0, a as i64),
3075 HoleValue::Const(1, b as i64),
3076 HoleValue::Const(2, const_offset),
3077 HoleValue::Const(3, ptr_slot as i64),
3078 HoleValue::Const(5, dst as i64),
3079 HoleValue::Cont(0, next),
3080 ],
3081 );
3082 }
3083 }
3084 MicroOp::ArrStore { src, idx, ptr_slot, len_slot, byte, narrow32, checked } => {
3085 if checked {
3086 buf.push_stencil(
3087 arrst_stencil(byte, narrow32, true),
3088 &[
3089 HoleValue::Const(0, idx as i64),
3090 HoleValue::Const(1, ptr_slot as i64),
3091 HoleValue::Const(2, len_slot as i64),
3092 HoleValue::Const(3, src as i64),
3093 HoleValue::Cont(0, next),
3094 HoleValue::Cont(1, buf.label(piece_for(code_at(i)))),
3095 ],
3096 );
3097 } else {
3098 buf.push_stencil(
3099 arrst_stencil(byte, narrow32, false),
3100 &[
3101 HoleValue::Const(0, idx as i64),
3102 HoleValue::Const(1, ptr_slot as i64),
3103 HoleValue::Const(3, src as i64),
3104 HoleValue::Cont(0, next),
3105 ],
3106 );
3107 }
3108 }
3109 MicroOp::ArrRMW { idx, operand, ptr_slot, len_slot, op, checked } => {
3110 if checked {
3111 buf.push_stencil(
3112 rmw_stencil(op, true),
3113 &[
3114 HoleValue::Const(0, idx as i64),
3115 HoleValue::Const(1, ptr_slot as i64),
3116 HoleValue::Const(2, len_slot as i64),
3117 HoleValue::Const(3, operand as i64),
3118 HoleValue::Cont(0, next),
3119 HoleValue::Cont(1, buf.label(piece_for(code_at(i)))),
3120 ],
3121 );
3122 } else {
3123 buf.push_stencil(
3124 rmw_stencil(op, false),
3125 &[
3126 HoleValue::Const(0, idx as i64),
3127 HoleValue::Const(1, ptr_slot as i64),
3128 HoleValue::Const(3, operand as i64),
3129 HoleValue::Cont(0, next),
3130 ],
3131 );
3132 }
3133 }
3134 MicroOp::ArrCondSwap { idx1, idx2, ptr_slot, len_slot, cmp, checked } => {
3135 if checked {
3136 buf.push_stencil(
3137 condswap_stencil(cmp, true),
3138 &[
3139 HoleValue::Const(0, idx1 as i64),
3140 HoleValue::Const(1, idx2 as i64),
3141 HoleValue::Const(2, ptr_slot as i64),
3142 HoleValue::Const(3, len_slot as i64),
3143 HoleValue::Cont(0, next),
3144 HoleValue::Cont(1, buf.label(piece_for(code_at(i)))),
3145 ],
3146 );
3147 } else {
3148 buf.push_stencil(
3149 condswap_stencil(cmp, false),
3150 &[
3151 HoleValue::Const(0, idx1 as i64),
3152 HoleValue::Const(1, idx2 as i64),
3153 HoleValue::Const(2, ptr_slot as i64),
3154 HoleValue::Cont(0, next),
3155 ],
3156 );
3157 }
3158 }
3159 MicroOp::ArrSwap { idx1, idx2, ptr_slot, len_slot, checked } => {
3160 if checked {
3161 buf.push_stencil(
3162 &ST_ARRSWAP,
3163 &[
3164 HoleValue::Const(0, idx1 as i64),
3165 HoleValue::Const(1, idx2 as i64),
3166 HoleValue::Const(2, ptr_slot as i64),
3167 HoleValue::Const(3, len_slot as i64),
3168 HoleValue::Cont(0, next),
3169 HoleValue::Cont(1, buf.label(piece_for(code_at(i)))),
3170 ],
3171 );
3172 } else {
3173 buf.push_stencil(
3174 &ST_ARRSWAP_U,
3175 &[
3176 HoleValue::Const(0, idx1 as i64),
3177 HoleValue::Const(1, idx2 as i64),
3178 HoleValue::Const(2, ptr_slot as i64),
3179 HoleValue::Cont(0, next),
3180 ],
3181 );
3182 }
3183 }
3184 MicroOp::FmaF { dst, a, b, c } => {
3185 buf.push_stencil(
3186 &ST_FMAF,
3187 &[
3188 HoleValue::Const(0, a as i64),
3189 HoleValue::Const(1, b as i64),
3190 HoleValue::Const(2, c as i64),
3191 HoleValue::Const(3, dst as i64),
3192 HoleValue::Cont(0, next),
3193 ],
3194 );
3195 }
3196 MicroOp::ArrLoad2F { dst, i: ix, j: jx, ptr_slot, len_slot, op } => {
3197 let stencil = match op {
3198 FOp::Add => &ST_ARRLD2_ADDF,
3199 FOp::Sub => &ST_ARRLD2_SUBF,
3200 FOp::Mul => &ST_ARRLD2_MULF,
3201 };
3202 buf.push_stencil(
3203 stencil,
3204 &[
3205 HoleValue::Const(0, ix as i64),
3206 HoleValue::Const(1, jx as i64),
3207 HoleValue::Const(2, ptr_slot as i64),
3208 HoleValue::Const(3, len_slot as i64),
3209 HoleValue::Const(4, dst as i64),
3210 HoleValue::Cont(0, next),
3211 HoleValue::Cont(1, buf.label(piece_for(code_at(i)))),
3212 ],
3213 );
3214 }
3215 MicroOp::ArrLoad2 { dst, i: ix, j: jx, ptr_a, len_a, ptr_b, len_b, op, checked } => {
3216 if checked {
3217 buf.push_stencil(
3218 ld2_int_stencil(op, true),
3219 &[
3220 HoleValue::Const(0, ix as i64),
3221 HoleValue::Const(1, jx as i64),
3222 HoleValue::Const(2, ptr_a as i64),
3223 HoleValue::Const(3, len_a as i64),
3224 HoleValue::Const(4, ptr_b as i64),
3225 HoleValue::Const(5, len_b as i64),
3226 HoleValue::Const(6, dst as i64),
3227 HoleValue::Cont(0, next),
3228 HoleValue::Cont(1, buf.label(piece_for(code_at(i)))),
3229 ],
3230 );
3231 } else {
3232 buf.push_stencil(
3233 ld2_int_stencil(op, false),
3234 &[
3235 HoleValue::Const(0, ix as i64),
3236 HoleValue::Const(1, jx as i64),
3237 HoleValue::Const(2, ptr_a as i64),
3238 HoleValue::Const(3, ptr_b as i64),
3239 HoleValue::Const(4, dst as i64),
3240 HoleValue::Cont(0, next),
3241 ],
3242 );
3243 }
3244 }
3245 MicroOp::NotInt { dst, src } | MicroOp::NotBool { dst, src }
3246 | MicroOp::IntToFloat { dst, src } | MicroOp::SqrtF { dst, src } => {
3247 let stencil = match op {
3248 MicroOp::NotInt { .. } => &ST_NOTI2,
3249 MicroOp::NotBool { .. } => &ST_NOTB2,
3250 MicroOp::SqrtF { .. } => &ST_SQRTF2,
3251 _ => &ST_I2F2,
3252 };
3253 buf.push_stencil(
3254 stencil,
3255 &[
3256 HoleValue::Const(0, src as i64),
3257 HoleValue::Const(1, dst as i64),
3258 HoleValue::Cont(0, next),
3259 ],
3260 );
3261 }
3262 MicroOp::BranchF { cmp, lhs, rhs, target } => {
3263 // Jump to target when cmp is FALSE — under IEEE, the false
3264 // path also catches every NaN-unordered pair, so the mapping
3265 // may ONLY swap operands (a>b ⇔ b<a; a>=b ⇔ b<=a), never
3266 // continuations on the truth side.
3267 let t = buf.label(target);
3268 let (stencil, a, b) = match cmp {
3269 Cmp::Lt => (&ST_BRLTF, lhs, rhs),
3270 Cmp::Gt => (&ST_BRLTF, rhs, lhs),
3271 Cmp::LtEq => (&ST_BRLEF, lhs, rhs),
3272 Cmp::GtEq => (&ST_BRLEF, rhs, lhs),
3273 Cmp::Eq => (&ST_BREQF, lhs, rhs),
3274 Cmp::NotEq => (&ST_BREQF, lhs, rhs),
3275 };
3276 let (c0, c1) = if cmp == Cmp::NotEq {
3277 // NotEq: jump when eps-equal (the TRUE path of breqf).
3278 (t, next)
3279 } else {
3280 (next, t)
3281 };
3282 buf.push_stencil(
3283 stencil,
3284 &[
3285 HoleValue::Const(0, a as i64),
3286 HoleValue::Const(1, b as i64),
3287 HoleValue::Cont(0, c0),
3288 HoleValue::Cont(1, c1),
3289 ],
3290 );
3291 }
3292 MicroOp::Div { dst, lhs, rhs } | MicroOp::Mod { dst, lhs, rhs } => {
3293 let stencil = match op {
3294 MicroOp::Div { .. } => &ST_DIV3C,
3295 _ => &ST_MOD3C,
3296 };
3297 buf.push_stencil(
3298 stencil,
3299 &[
3300 HoleValue::Const(0, lhs as i64),
3301 HoleValue::Const(1, rhs as i64),
3302 HoleValue::Const(2, dst as i64),
3303 HoleValue::Cont(0, next),
3304 HoleValue::Cont(1, buf.label(piece_for(code_at(i)))),
3305 ],
3306 );
3307 }
3308 MicroOp::DivPow2 { dst, lhs, k } => {
3309 buf.push_stencil(
3310 &ST_DIVPOW2,
3311 &[
3312 HoleValue::Const(0, lhs as i64),
3313 HoleValue::Const(1, k as i64),
3314 HoleValue::Const(2, (1i64 << k) - 1),
3315 HoleValue::Const(3, dst as i64),
3316 HoleValue::Cont(0, next),
3317 ],
3318 );
3319 }
3320 MicroOp::Jump { target } => {
3321 buf.push_stencil(&ST_JUMP, &[HoleValue::Cont(0, buf.label(target))]);
3322 }
3323 MicroOp::JumpIfFalse { cond, target } => {
3324 // brz: nonzero → cont 0 (fall through), zero → cont 1 (target).
3325 buf.push_stencil(
3326 &ST_BRZ,
3327 &[
3328 HoleValue::Const(0, cond as i64),
3329 HoleValue::Cont(0, next),
3330 HoleValue::Cont(1, buf.label(target)),
3331 ],
3332 );
3333 }
3334 MicroOp::JumpIfTrue { cond, target } => {
3335 // Same stencil, swapped continuations: nonzero → target.
3336 buf.push_stencil(
3337 &ST_BRZ,
3338 &[
3339 HoleValue::Const(0, cond as i64),
3340 HoleValue::Cont(0, buf.label(target)),
3341 HoleValue::Cont(1, next),
3342 ],
3343 );
3344 }
3345 MicroOp::Branch { cmp, lhs, rhs, target } => {
3346 // Jump to target when cmp is FALSE; fall through when TRUE.
3347 // brlt/breq put the TRUE outcome on cont 0; operand and
3348 // continuation swaps express all six comparison kinds.
3349 let t = buf.label(target);
3350 let (stencil, a, b, c0, c1) = match cmp {
3351 Cmp::Lt => (&ST_BRLT, lhs, rhs, next, t),
3352 Cmp::GtEq => (&ST_BRLT, lhs, rhs, t, next),
3353 Cmp::Gt => (&ST_BRLT, rhs, lhs, next, t),
3354 Cmp::LtEq => (&ST_BRLT, rhs, lhs, t, next),
3355 Cmp::Eq => (&ST_BREQ, lhs, rhs, next, t),
3356 Cmp::NotEq => (&ST_BREQ, lhs, rhs, t, next),
3357 };
3358 buf.push_stencil(
3359 stencil,
3360 &[
3361 HoleValue::Const(0, a as i64),
3362 HoleValue::Const(1, b as i64),
3363 HoleValue::Cont(0, c0),
3364 HoleValue::Cont(1, c1),
3365 ],
3366 );
3367 }
3368 MicroOp::Return { src } => {
3369 buf.push_stencil(&ST_RET2, &[HoleValue::Const(0, src as i64)]);
3370 }
3371 // The mutable-Text append has NO per-piece stencil — it is emitted
3372 // ONLY for the contiguous regalloc backend (where it lowers to a
3373 // SysV helper call). Decline so the caller falls back to bytecode,
3374 // which is always correct.
3375 MicroOp::StrAppend { .. } => {
3376 return Err(JitCompileError::Unsupported("StrAppend"));
3377 }
3378 // The magic reciprocal: a single `mul`-high + shift piece (the
3379 // contiguous regalloc backend also lowers it, but the stencil tier
3380 // owns the mixed regions regalloc declines — e.g. a `% c` next to a
3381 // map insert). Holes: 0 = lhs, 1 = magic, 2 = more, 3 = mul_back,
3382 // 4 = dst. No deopt continuation (a literal `c > 0` never faults).
3383 MicroOp::MagicDivU { dst, lhs, magic, more, mul_back } => {
3384 buf.push_stencil(
3385 &ST_MAGICDIV,
3386 &[
3387 HoleValue::Const(0, lhs as i64),
3388 HoleValue::Const(1, magic as i64),
3389 HoleValue::Const(2, more as i64),
3390 HoleValue::Const(3, mul_back),
3391 HoleValue::Const(4, dst as i64),
3392 HoleValue::Cont(0, next),
3393 ],
3394 );
3395 }
3396 }
3397 }
3398
3399 if let Some(cell) = &status {
3400 let addr = cell.as_ref() as *const AtomicI64 as i64;
3401 buf.push_stencil(&ST_DEOPT, &[HoleValue::Const(0, addr)]);
3402 for &code in &code_pieces {
3403 buf.push_stencil(
3404 &ST_DEOPT_AT,
3405 &[
3406 HoleValue::Const(0, addr),
3407 HoleValue::Const(1, code),
3408 HoleValue::Const(2, depth_addr),
3409 ],
3410 );
3411 }
3412 }
3413
3414 let chain = buf.finish().map_err(|e| JitCompileError::Assembly(e.to_string()))?;
3415 Ok(CompiledChain { chain, status, keepalive: Vec::new() })
3416}
3417
3418/// Evaluate the unsigned magic reciprocal for [`MicroOp::MagicDivU`]:
3419/// `x / c` when `mul_back == 0`, else `x % c` (`mul_back == c`). The dividend is
3420/// reinterpreted as `u64`, sound because the op is emitted ONLY for a proven
3421/// non-negative `x` (the i64 bit pattern equals the value, and unsigned and
3422/// signed-truncating `/`/`%` agree there). The remainder is `x - q*c` in
3423/// wrapping i64 (its low 64 bits equal the u64 difference). The `more` encoding
3424/// is the `logicaffeine_data::LogosDivU64` one: low 6 bits = shift, `0x40`
3425/// = the 65-bit add-marker path, `0x80` = the pure-shift power-of-two path.
3426#[inline]
3427pub fn magic_eval(x: i64, magic: u64, more: u8, mul_back: i64) -> i64 {
3428 const SHIFT_MASK: u8 = 0x3F;
3429 const ADD_MARKER: u8 = 0x40;
3430 const SHIFT_PATH: u8 = 0x80;
3431 let n = x as u64;
3432 let q = if more & SHIFT_PATH != 0 {
3433 n >> (more & SHIFT_MASK)
3434 } else {
3435 let hi = (((magic as u128) * (n as u128)) >> 64) as u64;
3436 if more & ADD_MARKER != 0 {
3437 let t = (n.wrapping_sub(hi) >> 1).wrapping_add(hi);
3438 t >> (more & SHIFT_MASK)
3439 } else {
3440 hi >> (more & SHIFT_MASK)
3441 }
3442 };
3443 if mul_back == 0 {
3444 q as i64
3445 } else {
3446 x.wrapping_sub((q as i64).wrapping_mul(mul_back))
3447 }
3448}
3449
3450/// The reference evaluator — the independent model every differential runs
3451/// against. Deliberately the dumbest possible implementation: a pc loop.
3452/// `fuel` bounds the step count (None when caught in a loop that long).
3453/// A zero divisor also yields None — the reference's model of the chain's
3454/// side exit (differentials feed nonzero divisors when comparing values, and
3455/// assert Deopt agreement when not).
3456pub fn reference_eval(ops: &[MicroOp], frame: &mut [i64], mut fuel: u64) -> Option<i64> {
3457 let mut pc = 0usize;
3458 while pc < ops.len() {
3459 if fuel == 0 {
3460 return None;
3461 }
3462 fuel -= 1;
3463 match ops[pc] {
3464 MicroOp::LoadConst { dst, value } => frame[dst as usize] = value,
3465 MicroOp::Move { dst, src } => frame[dst as usize] = frame[src as usize],
3466 // EXACT integer arithmetic: signed overflow returns None — the JIT's
3467 // side-exit (deopt) is the matching signal, so the differential stays
3468 // consistent (overflow ⟺ reference None ⟺ JIT deopt).
3469 MicroOp::Add { dst, lhs, rhs } => {
3470 frame[dst as usize] = frame[lhs as usize].checked_add(frame[rhs as usize])?
3471 }
3472 MicroOp::Sub { dst, lhs, rhs } => {
3473 frame[dst as usize] = frame[lhs as usize].checked_sub(frame[rhs as usize])?
3474 }
3475 MicroOp::Mul { dst, lhs, rhs } => {
3476 frame[dst as usize] = frame[lhs as usize].checked_mul(frame[rhs as usize])?
3477 }
3478 MicroOp::Div { dst, lhs, rhs } => {
3479 let b = frame[rhs as usize];
3480 // Zero divisor AND the `i64::MIN / -1` overflow both deopt —
3481 // the exact tier recomputes (and promotes to BigInt).
3482 if b == 0 || (b == -1 && frame[lhs as usize] == i64::MIN) {
3483 return None;
3484 }
3485 frame[dst as usize] = frame[lhs as usize].wrapping_div(b)
3486 }
3487 MicroOp::DivPow2 { dst, lhs, k } => {
3488 let x = frame[lhs as usize];
3489 let mask = (1i64 << k) - 1;
3490 frame[dst as usize] = x.wrapping_add((x >> 63) & mask) >> k;
3491 }
3492 MicroOp::MagicDivU { dst, lhs, magic, more, mul_back } => {
3493 frame[dst as usize] = magic_eval(frame[lhs as usize], magic, more, mul_back);
3494 }
3495 MicroOp::Mod { dst, lhs, rhs } => {
3496 let b = frame[rhs as usize];
3497 if b == 0 {
3498 return None;
3499 }
3500 frame[dst as usize] = frame[lhs as usize].wrapping_rem(b)
3501 }
3502 MicroOp::Lt { dst, lhs, rhs } => {
3503 frame[dst as usize] = (frame[lhs as usize] < frame[rhs as usize]) as i64
3504 }
3505 MicroOp::Gt { dst, lhs, rhs } => {
3506 frame[dst as usize] = (frame[lhs as usize] > frame[rhs as usize]) as i64
3507 }
3508 MicroOp::LtEq { dst, lhs, rhs } => {
3509 frame[dst as usize] = (frame[lhs as usize] <= frame[rhs as usize]) as i64
3510 }
3511 MicroOp::GtEq { dst, lhs, rhs } => {
3512 frame[dst as usize] = (frame[lhs as usize] >= frame[rhs as usize]) as i64
3513 }
3514 MicroOp::Eq { dst, lhs, rhs } => {
3515 frame[dst as usize] = (frame[lhs as usize] == frame[rhs as usize]) as i64
3516 }
3517 MicroOp::Neq { dst, lhs, rhs } => {
3518 frame[dst as usize] = (frame[lhs as usize] != frame[rhs as usize]) as i64
3519 }
3520 MicroOp::BitAnd { dst, lhs, rhs } => {
3521 frame[dst as usize] = frame[lhs as usize] & frame[rhs as usize]
3522 }
3523 MicroOp::BitOr { dst, lhs, rhs } => {
3524 frame[dst as usize] = frame[lhs as usize] | frame[rhs as usize]
3525 }
3526 MicroOp::BitXor { dst, lhs, rhs } => {
3527 frame[dst as usize] = frame[lhs as usize] ^ frame[rhs as usize]
3528 }
3529 MicroOp::Shl { dst, lhs, rhs } => {
3530 frame[dst as usize] = frame[lhs as usize].wrapping_shl(frame[rhs as usize] as u32)
3531 }
3532 MicroOp::Shr { dst, lhs, rhs } => {
3533 frame[dst as usize] = frame[lhs as usize].wrapping_shr(frame[rhs as usize] as u32)
3534 }
3535 MicroOp::NotInt { dst, src } => frame[dst as usize] = !frame[src as usize],
3536 MicroOp::NotBool { dst, src } => frame[dst as usize] = frame[src as usize] ^ 1,
3537 MicroOp::AddF { dst, lhs, rhs } => {
3538 let a = f64::from_bits(frame[lhs as usize] as u64);
3539 let b = f64::from_bits(frame[rhs as usize] as u64);
3540 frame[dst as usize] = (a + b).to_bits() as i64
3541 }
3542 MicroOp::SubF { dst, lhs, rhs } => {
3543 let a = f64::from_bits(frame[lhs as usize] as u64);
3544 let b = f64::from_bits(frame[rhs as usize] as u64);
3545 frame[dst as usize] = (a - b).to_bits() as i64
3546 }
3547 MicroOp::MulF { dst, lhs, rhs } => {
3548 let a = f64::from_bits(frame[lhs as usize] as u64);
3549 let b = f64::from_bits(frame[rhs as usize] as u64);
3550 frame[dst as usize] = (a * b).to_bits() as i64
3551 }
3552 MicroOp::DivF { dst, lhs, rhs } => {
3553 let b = f64::from_bits(frame[rhs as usize] as u64);
3554 if b == 0.0 {
3555 return None;
3556 }
3557 let a = f64::from_bits(frame[lhs as usize] as u64);
3558 frame[dst as usize] = (a / b).to_bits() as i64
3559 }
3560 MicroOp::LtF { dst, lhs, rhs } => {
3561 let a = f64::from_bits(frame[lhs as usize] as u64);
3562 let b = f64::from_bits(frame[rhs as usize] as u64);
3563 frame[dst as usize] = (a < b) as i64
3564 }
3565 MicroOp::GtF { dst, lhs, rhs } => {
3566 let a = f64::from_bits(frame[lhs as usize] as u64);
3567 let b = f64::from_bits(frame[rhs as usize] as u64);
3568 frame[dst as usize] = (a > b) as i64
3569 }
3570 MicroOp::LtEqF { dst, lhs, rhs } => {
3571 let a = f64::from_bits(frame[lhs as usize] as u64);
3572 let b = f64::from_bits(frame[rhs as usize] as u64);
3573 frame[dst as usize] = (a <= b) as i64
3574 }
3575 MicroOp::GtEqF { dst, lhs, rhs } => {
3576 let a = f64::from_bits(frame[lhs as usize] as u64);
3577 let b = f64::from_bits(frame[rhs as usize] as u64);
3578 frame[dst as usize] = (a >= b) as i64
3579 }
3580 // IEEE float equality (`NaN != NaN`) — identical to the
3581 // tree-walker/VM `values_equal` and the compiled backend.
3582 MicroOp::EqF { dst, lhs, rhs } => {
3583 let a = f64::from_bits(frame[lhs as usize] as u64);
3584 let b = f64::from_bits(frame[rhs as usize] as u64);
3585 frame[dst as usize] = (a == b) as i64
3586 }
3587 MicroOp::NeqF { dst, lhs, rhs } => {
3588 let a = f64::from_bits(frame[lhs as usize] as u64);
3589 let b = f64::from_bits(frame[rhs as usize] as u64);
3590 frame[dst as usize] = (a != b) as i64
3591 }
3592 MicroOp::BranchF { cmp, lhs, rhs, target } => {
3593 let a = f64::from_bits(frame[lhs as usize] as u64);
3594 let b = f64::from_bits(frame[rhs as usize] as u64);
3595 let truth = match cmp {
3596 Cmp::Lt => a < b,
3597 Cmp::Gt => a > b,
3598 Cmp::LtEq => a <= b,
3599 Cmp::GtEq => a >= b,
3600 Cmp::Eq => a == b,
3601 Cmp::NotEq => a != b,
3602 };
3603 if !truth {
3604 pc = target;
3605 continue;
3606 }
3607 }
3608 MicroOp::IntToFloat { dst, src } => {
3609 frame[dst as usize] = (frame[src as usize] as f64).to_bits() as i64
3610 }
3611 MicroOp::SqrtF { dst, src } => {
3612 frame[dst as usize] =
3613 f64::from_bits(frame[src as usize] as u64).sqrt().to_bits() as i64
3614 }
3615 // Calls, pushes and map traffic touch live runtime state the
3616 // reference cannot model — VM-level differentials own their
3617 // coverage.
3618 MicroOp::Call { .. }
3619 | MicroOp::CallSelf { .. }
3620 | MicroOp::CallSelfCopy { .. }
3621 | MicroOp::ArrPush { .. }
3622 | MicroOp::ListClear { .. }
3623 | MicroOp::StrAppend { .. }
3624 | MicroOp::MapGet { .. }
3625 | MicroOp::MapSet { .. }
3626 | MicroOp::MapHas { .. }
3627 | MicroOp::NewList { .. }
3628 | MicroOp::ListTriple { .. } => return None,
3629 MicroOp::ArrLoad { dst, idx, ptr_slot, len_slot, byte, narrow32, checked } => {
3630 let i = frame[idx as usize];
3631 let len = frame[len_slot as usize];
3632 let im1 = i.wrapping_sub(1);
3633 // The reference keeps the bounds guard either way (an
3634 // unchecked load that the Oracle proved in-bounds is in
3635 // range by hypothesis; refusing OOB here is just safety).
3636 let _ = checked;
3637 if (im1 as u64) >= (len as u64) {
3638 return None;
3639 }
3640 // SAFETY: differentials pin a live buffer, like the chain.
3641 frame[dst as usize] = unsafe {
3642 if byte {
3643 *(frame[ptr_slot as usize] as *const u8).add(im1 as usize) as i64
3644 } else if narrow32 {
3645 *(frame[ptr_slot as usize] as *const i32).add(im1 as usize) as i64
3646 } else {
3647 *(frame[ptr_slot as usize] as *const i64).add(im1 as usize)
3648 }
3649 };
3650 }
3651 MicroOp::ArrLoadAffine { dst, a, op, b, const_offset, ptr_slot, len_slot, checked } => {
3652 let _ = checked;
3653 let idx = op.eval(frame[a as usize], frame[b as usize], const_offset);
3654 let len = frame[len_slot as usize];
3655 let im1 = idx.wrapping_sub(1);
3656 if (im1 as u64) >= (len as u64) {
3657 return None;
3658 }
3659 // SAFETY: differentials pin a live 8-byte buffer, like the chain.
3660 frame[dst as usize] =
3661 unsafe { *(frame[ptr_slot as usize] as *const i64).add(im1 as usize) };
3662 }
3663 MicroOp::ArrLoad2F { dst, i: ix, j: jx, ptr_slot, len_slot, op } => {
3664 let len = frame[len_slot as usize];
3665 let im1 = frame[ix as usize].wrapping_sub(1);
3666 let jm1 = frame[jx as usize].wrapping_sub(1);
3667 if (im1 as u64) >= (len as u64) || (jm1 as u64) >= (len as u64) {
3668 return None;
3669 }
3670 // SAFETY: differentials pin a live 8-byte buffer, like the chain.
3671 let (a, b) = unsafe {
3672 let ptr = frame[ptr_slot as usize] as *const i64;
3673 (
3674 f64::from_bits(*ptr.add(im1 as usize) as u64),
3675 f64::from_bits(*ptr.add(jm1 as usize) as u64),
3676 )
3677 };
3678 frame[dst as usize] = op.eval(a, b).to_bits() as i64;
3679 }
3680 MicroOp::ArrLoad2 { dst, i: ix, j: jx, ptr_a, len_a, ptr_b, len_b, op, checked } => {
3681 let _ = checked;
3682 let lena = frame[len_a as usize];
3683 let lenb = frame[len_b as usize];
3684 let im1 = frame[ix as usize].wrapping_sub(1);
3685 let jm1 = frame[jx as usize].wrapping_sub(1);
3686 if (im1 as u64) >= (lena as u64) || (jm1 as u64) >= (lenb as u64) {
3687 return None;
3688 }
3689 // SAFETY: differentials pin live 8-byte buffers, like the chain.
3690 let (a, b) = unsafe {
3691 let pa = frame[ptr_a as usize] as *const i64;
3692 let pb = frame[ptr_b as usize] as *const i64;
3693 (*pa.add(im1 as usize), *pb.add(jm1 as usize))
3694 };
3695 frame[dst as usize] = op.eval(a, b);
3696 }
3697 MicroOp::ArrStore { src, idx, ptr_slot, len_slot, byte, narrow32, checked } => {
3698 // The reference always validates — a sound proof never trips it.
3699 let _ = checked;
3700 let i = frame[idx as usize];
3701 let len = frame[len_slot as usize];
3702 let im1 = i.wrapping_sub(1);
3703 if (im1 as u64) >= (len as u64) {
3704 return None;
3705 }
3706 // SAFETY: see ArrLoad.
3707 unsafe {
3708 if byte {
3709 *(frame[ptr_slot as usize] as *mut u8).add(im1 as usize) =
3710 (frame[src as usize] != 0) as u8;
3711 } else if narrow32 {
3712 *(frame[ptr_slot as usize] as *mut i32).add(im1 as usize) =
3713 frame[src as usize] as i32;
3714 } else {
3715 *(frame[ptr_slot as usize] as *mut i64).add(im1 as usize) =
3716 frame[src as usize];
3717 }
3718 }
3719 }
3720 MicroOp::ArrRMW { idx, operand, ptr_slot, len_slot, op, checked } => {
3721 let _ = checked;
3722 let i = frame[idx as usize];
3723 let len = frame[len_slot as usize];
3724 let im1 = i.wrapping_sub(1);
3725 if (im1 as u64) >= (len as u64) {
3726 return None;
3727 }
3728 // SAFETY: see ArrLoad — a live 8-byte buffer is pinned.
3729 unsafe {
3730 let cell = (frame[ptr_slot as usize] as *mut i64).add(im1 as usize);
3731 *cell = op.eval(*cell, frame[operand as usize]);
3732 }
3733 }
3734 MicroOp::ArrCondSwap { idx1, idx2, ptr_slot, len_slot, cmp, checked } => {
3735 let _ = checked;
3736 let len = frame[len_slot as usize];
3737 let m1 = frame[idx1 as usize].wrapping_sub(1);
3738 let m2 = frame[idx2 as usize].wrapping_sub(1);
3739 if (m1 as u64) >= (len as u64) || (m2 as u64) >= (len as u64) {
3740 return None;
3741 }
3742 // SAFETY: see ArrLoad — a live 8-byte buffer is pinned.
3743 unsafe {
3744 let ptr = frame[ptr_slot as usize] as *mut i64;
3745 let a = *ptr.add(m1 as usize);
3746 let b = *ptr.add(m2 as usize);
3747 if cmp.eval(a, b) {
3748 *ptr.add(m1 as usize) = b;
3749 *ptr.add(m2 as usize) = a;
3750 }
3751 }
3752 }
3753 MicroOp::ArrSwap { idx1, idx2, ptr_slot, len_slot, checked } => {
3754 let _ = checked;
3755 let len = frame[len_slot as usize];
3756 let m1 = frame[idx1 as usize].wrapping_sub(1);
3757 let m2 = frame[idx2 as usize].wrapping_sub(1);
3758 if (m1 as u64) >= (len as u64) || (m2 as u64) >= (len as u64) {
3759 return None;
3760 }
3761 // SAFETY: see ArrLoad — a live 8-byte buffer is pinned.
3762 unsafe {
3763 let ptr = frame[ptr_slot as usize] as *mut i64;
3764 let a = *ptr.add(m1 as usize);
3765 let b = *ptr.add(m2 as usize);
3766 *ptr.add(m1 as usize) = b;
3767 *ptr.add(m2 as usize) = a;
3768 }
3769 }
3770 MicroOp::FmaF { dst, a, b, c } => {
3771 let av = f64::from_bits(frame[a as usize] as u64);
3772 let bv = f64::from_bits(frame[b as usize] as u64);
3773 let cv = f64::from_bits(frame[c as usize] as u64);
3774 frame[dst as usize] = ((av * bv) + cv).to_bits() as i64;
3775 }
3776 MicroOp::Jump { target } => {
3777 pc = target;
3778 continue;
3779 }
3780 MicroOp::JumpIfFalse { cond, target } => {
3781 if frame[cond as usize] == 0 {
3782 pc = target;
3783 continue;
3784 }
3785 }
3786 MicroOp::JumpIfTrue { cond, target } => {
3787 if frame[cond as usize] != 0 {
3788 pc = target;
3789 continue;
3790 }
3791 }
3792 MicroOp::Branch { cmp, lhs, rhs, target } => {
3793 if !cmp.eval(frame[lhs as usize], frame[rhs as usize]) {
3794 pc = target;
3795 continue;
3796 }
3797 }
3798 MicroOp::MemMem {
3799 h_ptr_slot,
3800 h_len_slot,
3801 n_ptr_slot,
3802 n_len_slot,
3803 needle_len_slot,
3804 i_slot,
3805 count_slot,
3806 ..
3807 } => {
3808 // Reference model of the naive-search collapse: count overlapping
3809 // matches over `[i, h_len - needle_len + 1]`, add to count, and
3810 // advance `i` to the exit value — the same contract the runtime
3811 // helper implements. A needle index past its buffer would deopt
3812 // (the nest's checked needle `Index`): None, like the divisors.
3813 let h_len = frame[h_len_slot as usize];
3814 let n_buf_len = frame[n_len_slot as usize];
3815 let needle_len = frame[needle_len_slot as usize];
3816 let start = frame[i_slot as usize];
3817 if needle_len > n_buf_len {
3818 return None;
3819 }
3820 let bound = h_len - needle_len + 1;
3821 let mut count = 0i64;
3822 if needle_len == 0 {
3823 if start <= bound {
3824 count = bound - start + 1;
3825 }
3826 } else {
3827 // SAFETY: the pinned byte buffers are live for the eval.
3828 let hay = frame[h_ptr_slot as usize] as *const u8;
3829 let ndl = frame[n_ptr_slot as usize] as *const u8;
3830 let mut p = start; // 1-based
3831 while p <= bound {
3832 let mut m = true;
3833 for j in 0..needle_len {
3834 let hb = unsafe { *hay.add((p + j - 1) as usize) };
3835 let nb = unsafe { *ndl.add(j as usize) };
3836 if hb != nb {
3837 m = false;
3838 break;
3839 }
3840 }
3841 if m {
3842 count += 1;
3843 }
3844 p += 1;
3845 }
3846 }
3847 frame[count_slot as usize] += count;
3848 frame[i_slot as usize] = core::cmp::max(start, bound + 1);
3849 }
3850 MicroOp::Return { src } => return Some(frame[src as usize]),
3851 }
3852 pc += 1;
3853 }
3854 unreachable!("validated programs cannot fall off the end")
3855}