Skip to main content

logicaffeine_forge/
regalloc.rs

1//! Contiguous register-allocated region codegen — the EXODIA 3.1 "closer"
2//! foundation (WS-G).
3//!
4//! Where [`crate::jit::compile_straightline_coded`] lowers ONE stencil PIECE
5//! per [`MicroOp`] (each piece re-establishing operands from the frame, doing
6//! its op, storing the result, and tail-calling the next), this backend emits
7//! the WHOLE supported region as ONE contiguous x86-64 function. A global
8//! register assignment keeps the hottest slots resident in physical registers
9//! across every op, so a reg-resident operand is read with a register move (or
10//! used directly) rather than a frame round-trip — eliminating the per-piece
11//! ABI/operand overhead that holds the tiered cluster at 3-6× V8.
12//!
13//! ## Soundness contract (the differential gate is sacred)
14//!
15//! The emitted function is BIT-IDENTICAL to [`crate::jit::reference_eval`] and
16//! thus to the tree-walker:
17//! - i64 wrapping arithmetic (x86 `add`/`sub`/`imul` wrap natively);
18//! - the kernel's exact shift spec (count truncates to the low 6 bits via `cl`);
19//! - `Div`/`Mod` side-EXIT on a zero divisor BEFORE any effect (a
20//!   `ChainOutcome::Deopt` through the shared status cell, replayed on
21//!   bytecode where the kernel raises the precise error), and reproduce
22//!   `MIN / -1 = MIN` / `MIN % -1 = 0` without the `#DE` overflow trap.
23//!
24//! On exit (every `Return`) ALL reg-resident slots are flushed back to the
25//! frame, so the frame the caller observes is consistent with the tree-walker's
26//! full frame state — the region tier can resume / deopt from frame slots.
27//!
28//! ## Assignment model
29//!
30//! A slot is either RESIDENT (lives in one fixed physical register for the
31//! whole function) or SPILLED (lives in its frame slot). Slots are ranked by
32//! reference count; the top ones that fit get the allocatable callee/caller
33//! saved GPRs. This is a *global* per-slot assignment (no per-point churn), so
34//! it is trivially correct across loops and back-edges: a slot is always in the
35//! same place. `rax`/`rdx`/`rcx` are reserved scratch (arithmetic, division,
36//! shift count); `r15` holds the frame base pointer.
37
38#![cfg(target_arch = "x86_64")]
39
40use std::sync::atomic::AtomicI64;
41
42use crate::buffer::JitChain;
43use crate::jit::{Cmp, CompiledChain, MicroOp, Slot};
44use crate::x64asm::{Asm, Cond, LabelId, Reg, Xmm};
45
46/// Where a slot's value lives for the whole function.
47#[derive(Clone, Copy, Debug, PartialEq, Eq)]
48enum Loc {
49    /// Resident in a physical general-purpose register (an INT-class slot).
50    Reg(Reg),
51    /// Resident in a physical XMM register (a FLOAT-class slot). The slot's f64
52    /// value lives here for the whole function; never simultaneously GP-resident.
53    Xmm(Xmm),
54    /// Spilled to `frame[slot]` (byte displacement = `slot * 8`). Both classes
55    /// load/store the SAME 8 raw bytes here, so a slot used by both an int and a
56    /// float op (or whose class is ambiguous) is kept in the frame, where bits
57    /// are reinterpreted freely.
58    Frame,
59}
60
61/// The frame base register (`*mut i64`, the chain ABI's first argument).
62const BASE: Reg = Reg::R15;
63/// Primary arithmetic scratch / division quotient / return value.
64const S0: Reg = Reg::Rax;
65/// Secondary arithmetic scratch / division remainder.
66const S1: Reg = Reg::Rdx;
67/// Shift-count scratch (the only register `shl`/`sar` by `cl` can use).
68const SC: Reg = Reg::Rcx;
69
70/// Primary XMM scratch (float arithmetic accumulator / load target).
71const FS0: Xmm = Xmm::Xmm14;
72/// Secondary XMM scratch (float second operand / abs-mask staging).
73const FS1: Xmm = Xmm::Xmm15;
74
75/// The BYTE offset of the `capacity` and `len` fields WITHIN a `Vec<T>`,
76/// discovered by PROBING the running binary's actual `Vec` layout — NEVER a
77/// hardcoded assumption.
78///
79/// `Vec<T>`'s field order (`ptr`/`cap`/`len`) is an implementation detail the
80/// Rust language does NOT guarantee (it differs across `std` builds — on the
81/// current toolchain the order is `cap, ptr, len`). The inline [`ArrPush`] fast
82/// path needs `cap` (the realloc test) and must keep the real `Vec::len` field
83/// coherent (so a later helper push / drop / deopt sees the appended element), so
84/// it reads/writes those two fields at these offsets. Probing the LIVE layout —
85/// constructing a real `Vec` and matching each word against the known
86/// `capacity()`/`len()` — makes the inline path bit-identical to `Vec::push` on
87/// EXACTLY this binary, with no cross-version layout assumption. The probe is
88/// computed once. The `ptr` field is never read inline (the mirrored frame
89/// `ptr_slot` is the source of truth and never moves on the fast path), so only
90/// these two offsets are needed.
91///
92/// The offsets are T-INDEPENDENT: `Vec<T>` is `RawVec<T> { ptr: NonNull<T>, cap }`
93/// + `len`, all word-sized regardless of `T`, so one `Vec<i64>` probe serves the
94/// Int / Float / Bool element buffers alike.
95struct VecLayout {
96    cap_off: i32,
97    len_off: i32,
98}
99
100fn vec_layout() -> &'static VecLayout {
101    use std::sync::OnceLock;
102    static LAYOUT: OnceLock<VecLayout> = OnceLock::new();
103    LAYOUT.get_or_init(|| {
104        // Distinct, recognizable cap and len so each maps to exactly one word.
105        let mut v: Vec<i64> = Vec::with_capacity(8);
106        // SAFETY: capacity is 8 ≥ 3, so setting len to 3 is in-bounds; the
107        // backing memory is uninitialized but never read (we forget `v`).
108        unsafe { v.set_len(3) };
109        let cap = v.capacity(); // 8
110        let len = v.len(); // 3
111        debug_assert_eq!(std::mem::size_of::<Vec<i64>>(), 24, "Vec is a 3-word triple");
112        let base = &v as *const Vec<i64> as *const usize;
113        // SAFETY: a `Vec<i64>` is exactly three `usize`-sized words.
114        let words = unsafe { std::slice::from_raw_parts(base, 3) };
115        let cap_off = words
116            .iter()
117            .position(|&w| w == cap)
118            .expect("Vec capacity field located") as i32
119            * 8;
120        let len_off = words
121            .iter()
122            .position(|&w| w == len)
123            .expect("Vec len field located") as i32
124            * 8;
125        // Restore len so the (forgotten) Vec drops its real (empty) contents
126        // without touching the uninitialized tail.
127        unsafe { v.set_len(0) };
128        VecLayout { cap_off, len_off }
129    })
130}
131
132/// XMM registers we may assign to FLOAT-class slots. All are caller-saved under
133/// SysV (no prologue save/restore); `xmm14`/`xmm15` are reserved as scratch
134/// (`FS0`/`FS1`). Ordered low→high so the assignment is deterministic.
135const FLOAT_REGS: [Xmm; 14] = [
136    Xmm::Xmm0, Xmm::Xmm1, Xmm::Xmm2, Xmm::Xmm3, Xmm::Xmm4, Xmm::Xmm5, Xmm::Xmm6,
137    Xmm::Xmm7, Xmm::Xmm8, Xmm::Xmm9, Xmm::Xmm10, Xmm::Xmm11, Xmm::Xmm12, Xmm::Xmm13,
138];
139
140/// Callee-saved registers we may assign (must be saved/restored).
141const CALLEE_SAVED: [Reg; 4] = [Reg::Rbx, Reg::R12, Reg::R13, Reg::R14];
142/// Caller-saved registers we may assign freely (no save/restore needed).
143/// `rdi`/`rsi` are the incoming `base`/`sp` args — `base` is copied to r15 in
144/// the prologue and `sp` is unused by the int subset, so both are free after
145/// entry.
146const CALLER_SAVED: [Reg; 6] = [Reg::R8, Reg::R9, Reg::R10, Reg::R11, Reg::Rdi, Reg::Rsi];
147
148/// Whether the contiguous register-allocating backend is selected. Default ON:
149/// it is bit-identical to the stencil tier (proven by the full corpus
150/// differential with the flag on) and 4-6x faster on the regions it supports,
151/// falling back to the per-piece tier on any unsupported op. `LOGOS_REGALLOC=0`
152/// is the kill-switch. Read once.
153pub fn regalloc_enabled() -> bool {
154    static ON: std::sync::OnceLock<bool> = std::sync::OnceLock::new();
155    *ON.get_or_init(|| std::env::var("LOGOS_REGALLOC").map_or(true, |v| v != "0"))
156}
157
158/// `LOGOS_SIMD=1` opts a recognized PURE ELEMENT-WISE MAP region (Oracle-proven
159/// in-bounds, unit-stride, no loop-carried scalar — see
160/// [`crate::vectorize::recognize_elementwise_map`]) into a 2-wide packed-double
161/// loop. DEFAULT OFF: the bit-identical differential gate must certify it before
162/// promotion, and no current benchmark exercises a float map. Read once.
163pub fn simd_enabled() -> bool {
164    static ON: std::sync::OnceLock<bool> = std::sync::OnceLock::new();
165    *ON.get_or_init(|| std::env::var("LOGOS_SIMD").is_ok_and(|v| v == "1"))
166}
167
168/// Whether the CALL-LOCALITY ranking bonus is applied (Fix 1). Default ON: it is
169/// a pure register-assignment reorder (bit-identical, proven by the corpus
170/// differential) that lands call-surviving values in callee-saved registers so a
171/// call-heavy loop pays no per-call spill/reload. `LOGOS_CALL_WEIGHT=0` is the
172/// kill-switch (also used to A/B the lever's effect on a quiet box). Read once.
173fn call_weight_enabled() -> bool {
174    static ON: std::sync::OnceLock<bool> = std::sync::OnceLock::new();
175    *ON.get_or_init(|| std::env::var("LOGOS_CALL_WEIGHT").map_or(true, |v| v != "0"))
176}
177
178/// Whether the INLINE `ArrPush` fast path is emitted (Wave 25). Default ON: it is
179/// bit-identical to the helper-call lowering (proven by the corpus differential
180/// and the dedicated `inline_push_*` tests), eliminating the per-push spill+call
181/// in the common (`len < cap`) case. `LOGOS_NO_INLINE_PUSH=1` falls back to the
182/// always-call lowering — the kill-switch AND the A/B toggle for the relative
183/// on/off timing on a noisy box. Read once.
184fn inline_push_enabled() -> bool {
185    static ON: std::sync::OnceLock<bool> = std::sync::OnceLock::new();
186    *ON.get_or_init(|| std::env::var("LOGOS_NO_INLINE_PUSH").map_or(true, |v| v == "0"))
187}
188
189/// Whether the SCALED-INDEX CSE is emitted (Wave 26). A straight-line run of
190/// array accesses sharing one index slot recomputes the 0-based index `im1 =
191/// idx - 1` (and its scaled byte offset `im1 * 8`) once into reserved
192/// caller-saved register(s), then reuses it for each `base + off` address — the
193/// LLVM/V8 "compute the scaled offset once, reuse across base pointers" idiom.
194/// nbody's inner force loop reads/writes 7 co-indexed arrays at the same `i`
195/// (and another set at `j`); matrix_mult / spectral_norm / prefix_sum likewise
196/// touch several arrays at a shared index. Default ON: it is bit-identical to
197/// the per-access recompute (the cache is recomputed whenever the index slot is
198/// reassigned, at every jump target, and across any SysV call), proven by the
199/// corpus differential and the dedicated `off_cse_*` tests. `LOGOS_NO_OFF_CSE=1`
200/// falls back to the per-access recompute — the kill-switch AND the A/B toggle
201/// for the relative on/off timing on a noisy box. Read once.
202fn off_cse_enabled() -> bool {
203    static ON: std::sync::OnceLock<bool> = std::sync::OnceLock::new();
204    *ON.get_or_init(|| std::env::var("LOGOS_NO_OFF_CSE").map_or(true, |v| v != "1"))
205}
206
207/// Whether the LOOP-INVARIANT ARRAY PTR/LEN HOIST is emitted (Wave 27a). When an
208/// array's ptr/len handle slots are FRAME-resident and are never written inside a
209/// loop (only in-place `ArrLoad`/`ArrStore` of that array — no reallocating
210/// `ArrPush`/`ListClear`/`NewList`/`ListTriple`, no scalar write to the slot),
211/// the per-access `emit_arr_addr` reload of those slots from the frame is
212/// loop-invariant. The hoist loads them ONCE into loop-persistent CALLEE-SAVED
213/// registers at the loop pre-header, so the inner accesses read the register
214/// instead of re-touching the frame every iteration (the knapsack inner `w`-loop
215/// reads `prev`'s ptr/len every iteration though `prev` only changes on the OUTER
216/// loop's `Set prev to curr`). Default ON: bit-identical to the per-access reload
217/// (the hoisted register holds exactly `frame[slot]`, which is invariant over the
218/// span, and the bounds check still runs per access against the hoisted length),
219/// proven by the corpus differential and the dedicated `ptr_hoist_*` tests.
220/// `LOGOS_NO_PTR_HOIST=1` falls back to the per-access reload — the kill-switch
221/// AND the A/B toggle for the relative on/off timing on a noisy box. Read once.
222fn ptr_hoist_enabled() -> bool {
223    static ON: std::sync::OnceLock<bool> = std::sync::OnceLock::new();
224    *ON.get_or_init(|| std::env::var("LOGOS_NO_PTR_HOIST").map_or(true, |v| v != "1"))
225}
226
227/// Whether the LOOP-INVARIANT CONSTANT HOIST is emitted (Wave 28). A
228/// `LoadConst { dst, value }` INSIDE a natural loop, whose `dst` is written by no
229/// other op in the span (so the value is invariant across iterations) and lands
230/// in a GP register, has its `mov reg, imm` re-executed every iteration in the
231/// body. The hoist emits it ONCE in the loop pre-header instead, leaving the
232/// register resident across the back-edge — the V8/LLVM loop-invariant-code-
233/// motion idiom (collatz −7%, loop_sum, every pure-GP scalar loop with an
234/// invariant constant).
235///
236/// FLOAT (XMM-resident) consts are EXCLUDED — the investigation began with
237/// mandelbrot's `2.0`/`4.0` GP→XMM reloads, the obvious target, but hoisting them
238/// into the SATURATED XMM file is bit-identical yet measured ~8% SLOWER (the
239/// per-iteration `movabs;movq` is FREE out-of-order work that overlaps the float
240/// chain; pinning the invariant in a read-heavy XMM register serializes worse).
241/// See the `const_hoist_*` tests and the call site's exclusion comment.
242///
243/// Default ON: bit-identical to the per-iteration `LoadConst` (the register holds
244/// exactly the same constant bits, the body simply stops re-loading them, and the
245/// exit flush of a written slot is unchanged), proven by the corpus differential
246/// and the dedicated `const_hoist_*` tests. `LOGOS_NO_CONST_HOIST=1` falls back to
247/// the per-iteration materialization — the kill-switch AND the A/B toggle for the
248/// relative on/off timing on a noisy box. Read LIVE (not cached) so a single
249/// process can A/B the two emissions for the structural gate.
250fn const_hoist_enabled() -> bool {
251    std::env::var("LOGOS_NO_CONST_HOIST").map_or(true, |v| v != "1")
252}
253
254/// Diagnostic: when `LOGOS_DUMP_CLASS=1`, [`compile_impl`] prints a one-line
255/// slot-class + placement histogram per compiled region that touches any float
256/// (Int/Float/Mixed counts, how many Float slots won an XMM register vs spilled
257/// to the frame under pressure, and the Mixed-slot indices). Pure observability,
258/// no behavior change — it is how the campaign tells a Mixed-frame wall apart
259/// from an XMM-pressure wall before choosing a lever.
260fn dump_class_enabled() -> bool {
261    static ON: std::sync::OnceLock<bool> = std::sync::OnceLock::new();
262    *ON.get_or_init(|| std::env::var("LOGOS_DUMP_CLASS").map_or(false, |v| v != "0"))
263}
264
265/// LS1 linear-scan register allocation (reuse a register across disjoint live
266/// ranges, float slots only). MEASURED LOSS — parked default-OFF. It is sound
267/// (forge 93/93 with `=1`, all benchmarks bit-identical) but a net SLOWDOWN on
268/// the float-pressured regions (nbody +74%, mandelbrot +104%): the floats the
269/// rank-based per-slot model leaves in the frame are COLD, so frame residency is
270/// already the cheap, correct choice — forcing them into reused registers adds a
271/// per-iteration spill-at-end store that exceeds their cold frame cost. `maxLive
272/// <= 14` measured liveness, not HOTNESS, so it overstated the win. Do NOT promote;
273/// kept behind the flag with the `LOGOS_DUMP_CLASS` diagnostic as a documented
274/// negative result. The float-cluster lever is op-count (fusion), not allocation.
275fn linear_scan_enabled() -> bool {
276    static ON: std::sync::OnceLock<bool> = std::sync::OnceLock::new();
277    *ON.get_or_init(|| std::env::var("LOGOS_LINEAR_SCAN").map_or(false, |v| v == "1"))
278}
279
280/// The maximum LOGOS call depth, baked into the self-call codegen exactly as it
281/// is baked into `logos_stencil_call_self` — the kernel's locked
282/// `MAX_CALL_DEPTH`. Reusing the single forge constant keeps the contiguous
283/// FUNCTION backend's depth guard in lockstep with the stencil tier and the
284/// kernel (a drift would diverge the depth-exceeded error). See the
285/// `max-call-depth-sync-jit-forge` invariant.
286const SELF_CALL_DEPTH_LIMIT: i64 = crate::jit::BAKED_CALL_DEPTH;
287
288/// Is this op in the supported subset of the contiguous backend? An op outside
289/// it makes [`compile_region_regalloc`] return `None`, so the caller falls back
290/// to the per-piece tier and behavior is unchanged.
291fn supported(op: &MicroOp) -> bool {
292    matches!(
293        op,
294        MicroOp::Move { .. }
295            | MicroOp::LoadConst { .. }
296            | MicroOp::Add { .. }
297            | MicroOp::Sub { .. }
298            | MicroOp::Mul { .. }
299            | MicroOp::Lt { .. }
300            | MicroOp::Gt { .. }
301            | MicroOp::LtEq { .. }
302            | MicroOp::GtEq { .. }
303            | MicroOp::Eq { .. }
304            | MicroOp::Neq { .. }
305            | MicroOp::BitAnd { .. }
306            | MicroOp::BitOr { .. }
307            | MicroOp::BitXor { .. }
308            | MicroOp::Shl { .. }
309            | MicroOp::Shr { .. }
310            | MicroOp::NotInt { .. }
311            | MicroOp::NotBool { .. }
312            | MicroOp::Div { .. }
313            | MicroOp::Mod { .. }
314            | MicroOp::DivPow2 { .. }
315            | MicroOp::MagicDivU { .. }
316            | MicroOp::Branch { .. }
317            | MicroOp::Jump { .. }
318            | MicroOp::JumpIfFalse { .. }
319            | MicroOp::JumpIfTrue { .. }
320            | MicroOp::Return { .. }
321            // Integer (8-byte element) array load/store. A FLOAT array element
322            // is also an 8-byte (`byte: false`) slot: the raw bits load into /
323            // store from a frame slot, and the float arithmetic reinterprets
324            // them — no separate "float array" op is needed.
325            | MicroOp::ArrLoad { byte: false, .. }
326            | MicroOp::ArrStore { byte: false, .. }
327            // BYTE (1-byte element) array load/store — the `Seq of Bool` buffer
328            // (sieve's `flags`, graph_bfs would-be `visited`). A `byte: true`
329            // load is a zero-extended `movzx` (`u8 as i64`); a `byte: true`
330            // store writes the BOOLEAN NORMALIZATION `(v != 0) as u8`. The
331            // address machinery and OOB side-exit are the 8-byte path with a
332            // unit stride — bit-identical to `ST_ARRLDB`/`ST_ARRSTB`.
333            | MicroOp::ArrLoad { byte: true, .. }
334            | MicroOp::ArrStore { byte: true, .. }
335            // FLOAT (f64) ops — the XMM register class (wave 11). Arithmetic is
336            // IEEE; `FmaF` is TWO roundings (mulsd then addsd, NOT a fused
337            // `vfmadd`); ordering compares are exact IEEE (NaN → false); `EqF`/
338            // `NeqF` use the kernel's `|a-b| < EPSILON` rule; `DivF` side-exits
339            // on a `0.0` divisor like the integer `Div`.
340            | MicroOp::AddF { .. }
341            | MicroOp::SubF { .. }
342            | MicroOp::MulF { .. }
343            | MicroOp::DivF { .. }
344            | MicroOp::SqrtF { .. }
345            | MicroOp::IntToFloat { .. }
346            | MicroOp::FmaF { .. }
347            | MicroOp::LtF { .. }
348            | MicroOp::GtF { .. }
349            | MicroOp::LtEqF { .. }
350            | MicroOp::GtEqF { .. }
351            | MicroOp::EqF { .. }
352            | MicroOp::NeqF { .. }
353            | MicroOp::BranchF { .. }
354            // LIST MUTATION (wave 13) — helper calls into the JIT runtime.
355            // `ArrPush` appends through `logos_rt_push_*` (the call MAY
356            // reallocate, so the pinned ptr/len are refreshed in the frame
357            // after); `ListClear` truncates a SOLE-OWNED buffer in place
358            // through `logos_rt_clear_*` (the alias-safety of the reuse is
359            // proven UPSTREAM in the micro-op lowering — a `ListClear` reaching
360            // here is unaliased). Both keep their vec/ptr/len handle slots
361            // FRAME-resident (the helper reads/writes those frame cells) and
362            // spill the caller-saved residents across the SysV call.
363            | MicroOp::ArrPush { .. }
364            | MicroOp::ListClear { .. }
365            // MUTABLE-TEXT append (`Set text to text + ch`). A helper call into
366            // the JIT runtime that grows the accumulator THROUGH the pinned
367            // `*mut Value` cell handle with the VM's exact `add_assign`
368            // (in-place / copy-on-write) semantics; the handle slot is forced
369            // frame-resident and the caller-saved residents are spilled across
370            // the SysV call, exactly like `ArrPush`.
371            | MicroOp::StrAppend { .. }
372    )
373}
374
375/// Is this op in the supported subset of the contiguous FUNCTION backend
376/// ([`compile_function_regalloc`])? It is the region subset PLUS the DIRECT
377/// self-call ops (`CallSelf`/`CallSelfCopy`). A cross-function `Call`, list/map
378/// ops, byte arrays, etc. remain unsupported → the function falls back to the
379/// per-piece stencil tier, byte-identical to today.
380fn supported_function(op: &MicroOp) -> bool {
381    supported(op) || matches!(op, MicroOp::CallSelf { .. } | MicroOp::CallSelfCopy { .. })
382}
383
384/// Is this op in the supported subset of the contiguous PRECISE (mode-B)
385/// FUNCTION backend ([`compile_function_regalloc_precise`])? This spans two
386/// recursion shapes:
387///
388///  - the IN-PLACE-ARRAY shape (heap_sort's `siftDown`, quicksort's `qs`):
389///    scalar arithmetic, checked int/byte array load/store (the in-place
390///    mutation), and the precise self-`Call` (windowed disjointly, resuming AT
391///    the faulting op on a side exit); and
392///  - the fresh-list-RETURN shape (mergesort: allocate fresh `left`/`right`/
393///    `result`, push into them, and return a new list): the registry-owned
394///    allocation `NewList`, the reallocating `ArrPush`, the in-place `ListClear`,
395///    and the `ListTriple` pin refresh from a live handle (a self-call's returned
396///    list, seen from the caller).
397///
398/// SOUNDNESS of the list-return shape: the regalloc body shares the SAME
399/// `adapt_function` micro stream, per-op `deopt_codes` table, runtime helpers
400/// (`logos_rt_alloc_list_i64` / push / triple), and `ChainFn::call`+`materialize`
401/// boundary as the per-piece precise stencil tier — only the body codegen
402/// changes. `NewList`/`ArrPush`/`ListTriple` each force their vec/ptr/len triple
403/// FRAME-resident (the helper's frame writes are the single source of truth, so a
404/// reallocating push's refreshed pointer/length is always read by the next
405/// access), every mode-B machinery slot (`>= rc`) stays frame-resident, and every
406/// precise side exit flushes the resident-written scalars to the frame. So a
407/// registry-owned fresh list materializes (detaches) on a precise resume exactly
408/// as in the stencil tier; the differential gate proves it bit-identical.
409///
410/// It still does NOT admit the classic `CallSelf`/`CallSelfCopy` (those replay
411/// from head — sound only for the scalar mode-A path), nor maps/cross-function
412/// calls.
413fn supported_function_precise(op: &MicroOp) -> bool {
414    supported(op)
415        || matches!(
416            op,
417            MicroOp::Call { .. } | MicroOp::ListTriple { .. } | MicroOp::NewList { .. }
418        )
419}
420
421/// Whether `op` is a CHECKED operation needing the deopt side-exit channel (the
422/// status cell): a divisor-check on `Div`/`Mod`, or a bounds-check on a checked
423/// integer array load/store. A self-call also writes the status cell (its depth/
424/// arena/entry guards and the propagation of an in-callee deopt), so it too
425/// requires the channel.
426fn needs_deopt(op: &MicroOp) -> bool {
427    matches!(
428        op,
429        MicroOp::Div { .. }
430            | MicroOp::Mod { .. }
431            | MicroOp::DivF { .. }
432            | MicroOp::ArrLoad { checked: true, .. }
433            | MicroOp::ArrStore { checked: true, .. }
434            // Integer add/sub/mul side-exit on signed overflow (`jo`) so the exact
435            // tier recomputes and promotes to BigInt — overflow is no longer a
436            // silent wrap. (A later perf pass elides the guard where the prover
437            // proves no overflow; for now every integer arith op is checked.)
438            | MicroOp::Add { .. }
439            | MicroOp::Sub { .. }
440            | MicroOp::Mul { .. }
441            | MicroOp::CallSelf { .. }
442            | MicroOp::CallSelfCopy { .. }
443            // The mode-B precise self-call writes the status cell (its depth/
444            // arena/entry guards and the propagation of an in-callee deopt).
445            | MicroOp::Call { .. }
446    )
447}
448
449/// Every slot index an op reads or writes (for reference-count ranking and the
450/// max-slot frame bound).
451fn slots_of(op: &MicroOp, out: &mut Vec<Slot>) {
452    match *op {
453        MicroOp::Move { dst, src } | MicroOp::NotInt { dst, src } | MicroOp::NotBool { dst, src } => {
454            out.extend([dst, src])
455        }
456        MicroOp::LoadConst { dst, .. } => out.push(dst),
457        MicroOp::Add { dst, lhs, rhs }
458        | MicroOp::Sub { dst, lhs, rhs }
459        | MicroOp::Mul { dst, lhs, rhs }
460        | MicroOp::Lt { dst, lhs, rhs }
461        | MicroOp::Gt { dst, lhs, rhs }
462        | MicroOp::LtEq { dst, lhs, rhs }
463        | MicroOp::GtEq { dst, lhs, rhs }
464        | MicroOp::Eq { dst, lhs, rhs }
465        | MicroOp::Neq { dst, lhs, rhs }
466        | MicroOp::BitAnd { dst, lhs, rhs }
467        | MicroOp::BitOr { dst, lhs, rhs }
468        | MicroOp::BitXor { dst, lhs, rhs }
469        | MicroOp::Shl { dst, lhs, rhs }
470        | MicroOp::Shr { dst, lhs, rhs }
471        | MicroOp::Div { dst, lhs, rhs }
472        | MicroOp::Mod { dst, lhs, rhs }
473        | MicroOp::AddF { dst, lhs, rhs }
474        | MicroOp::SubF { dst, lhs, rhs }
475        | MicroOp::MulF { dst, lhs, rhs }
476        | MicroOp::DivF { dst, lhs, rhs }
477        | MicroOp::LtF { dst, lhs, rhs }
478        | MicroOp::GtF { dst, lhs, rhs }
479        | MicroOp::LtEqF { dst, lhs, rhs }
480        | MicroOp::GtEqF { dst, lhs, rhs }
481        | MicroOp::EqF { dst, lhs, rhs }
482        | MicroOp::NeqF { dst, lhs, rhs } => out.extend([dst, lhs, rhs]),
483        MicroOp::FmaF { dst, a, b, c } => out.extend([dst, a, b, c]),
484        MicroOp::IntToFloat { dst, src } | MicroOp::SqrtF { dst, src } => out.extend([dst, src]),
485        MicroOp::DivPow2 { dst, lhs, .. } => out.extend([dst, lhs]),
486        MicroOp::MagicDivU { dst, lhs, .. } => out.extend([dst, lhs]),
487        MicroOp::Branch { lhs, rhs, .. } | MicroOp::BranchF { lhs, rhs, .. } => out.extend([lhs, rhs]),
488        MicroOp::JumpIfFalse { cond, .. } | MicroOp::JumpIfTrue { cond, .. } => out.push(cond),
489        MicroOp::Return { src } => out.push(src),
490        MicroOp::ArrLoad { dst, idx, ptr_slot, len_slot, checked, .. } => {
491            out.extend([dst, idx, ptr_slot]);
492            if checked {
493                out.push(len_slot);
494            }
495        }
496        MicroOp::ArrStore { src, idx, ptr_slot, len_slot, checked, .. } => {
497            out.extend([src, idx, ptr_slot]);
498            if checked {
499                out.push(len_slot);
500            }
501        }
502        // A push reads its `src` value (rank it — it may be register/XMM
503        // resident) and the vec/ptr/len handle TRIPLE, which is forced
504        // frame-resident below (the helper reads/writes those frame cells), so
505        // we list the triple only to extend `max_slot`, never to rank it.
506        MicroOp::ArrPush { src, vec_slot, ptr_slot, len_slot, .. } => {
507            out.extend([src, vec_slot, ptr_slot, len_slot])
508        }
509        MicroOp::ListClear { vec_slot, ptr_slot, len_slot, .. } => {
510            out.extend([vec_slot, ptr_slot, len_slot])
511        }
512        // A fresh-list allocation writes the vec/ptr/len handle TRIPLE (forced
513        // frame-resident below — the helper writes those frame cells); list it
514        // only to extend `max_slot`, never to rank it (the triple stays in the
515        // frame, so it competes for no register).
516        MicroOp::NewList { vec_slot, ptr_slot, len_slot, .. } => {
517            out.extend([vec_slot, ptr_slot, len_slot])
518        }
519        // A str-append reads its handle slot (forced frame-resident below — the
520        // helper indexes it) and, for the byte form, the value slot (ranked: it
521        // may be register-resident). The const form has no extra slot.
522        MicroOp::StrAppend { text_handle_slot, src, .. } => {
523            out.push(text_handle_slot);
524            if let crate::jit::StrSrc::Byte(s) = src {
525                out.push(s);
526            }
527        }
528        // The mode-B precise self-call writes `dst` and reads `limit_slot` (the
529        // arena bound). The callee window (args_start..) is forced frame-
530        // resident below; the staging Moves into it account those slots already,
531        // and `extend_self_call_slots` is not used (the disjoint window's extent
532        // is covered by the Move ops the adapter emits into it).
533        MicroOp::Call { dst, limit_slot, .. } => out.extend([dst, limit_slot]),
534        // A triple-plant reads `handle_slot` and writes the vec/ptr/len triple
535        // (all forced frame-resident); `handle_slot` is ranked so a register-
536        // resident handle is still read correctly (the codegen spills it to its
537        // forced frame cell so the helper sees it).
538        MicroOp::ListTriple { handle_slot, vec_slot, ptr_slot, len_slot, .. } => {
539            out.extend([handle_slot, vec_slot, ptr_slot, len_slot])
540        }
541        // A self-call writes `dst` and reads `limit_slot` (the arena bound). The
542        // callee window (args_start..) and a `CallSelfCopy`'s source block
543        // (src_start..) are accounted by `extend_self_call_slots` so `max_slot`
544        // covers them; the staging slots stay FRAME-resident (the callee reads
545        // them from the frame) and so are NOT ranked for a register here.
546        MicroOp::CallSelf { dst, limit_slot, .. } => out.extend([dst, limit_slot]),
547        MicroOp::CallSelfCopy { dst, src_start, arg_count, limit_slot, .. } => {
548            out.extend([dst, limit_slot]);
549            // The source args are READ; rank them (they may be register-resident
550            // — the codegen reads them from wherever they live, then stages).
551            for j in 0..arg_count {
552                out.push(src_start + j);
553            }
554        }
555        MicroOp::Jump { .. } => {}
556        _ => {}
557    }
558}
559
560/// The control-flow TARGET of a transfer op (`Jump`/`JumpIf*`/`Branch*`), if
561/// any. A target whose index is `<` the op's own index is a loop BACK-EDGE.
562fn op_target(op: &MicroOp) -> Option<usize> {
563    match *op {
564        MicroOp::Jump { target }
565        | MicroOp::JumpIfFalse { target, .. }
566        | MicroOp::JumpIfTrue { target, .. }
567        | MicroOp::Branch { target, .. }
568        | MicroOp::BranchF { target, .. } => Some(target),
569        _ => None,
570    }
571}
572
573/// The LOOP NESTING DEPTH of every op in the region. A loop is the span
574/// `[target, idx]` of a BACK-EDGE — a transfer op at `idx` whose `target <= idx`
575/// (so its body runs repeatedly). The depth of op `j` is the number of such
576/// spans that contain `j`; ops inside a doubly-nested loop have depth 2, etc.
577/// This is a structural over-approximation (it counts every back-edge span, not
578/// just reducible natural loops), which is exactly what the spill heuristic
579/// wants: a value referenced under a back-edge is hot and should win a register.
580fn loop_depths(ops: &[MicroOp]) -> Vec<u32> {
581    let n = ops.len();
582    let mut depth = vec![0u32; n];
583    for (idx, op) in ops.iter().enumerate() {
584        if let Some(target) = op_target(op) {
585            if target <= idx {
586                for d in depth.iter_mut().take(idx + 1).skip(target) {
587                    *d += 1;
588                }
589            }
590        }
591    }
592    depth
593}
594
595/// Does the region contain a SCALED-INDEX CSE opportunity — two or more array
596/// accesses (`ArrLoad`/`ArrStore`) sharing one index slot AND element stride
597/// across a straight-line run, with NO intervening invalidation? This is the
598/// gate for reserving a cache register: without a reusable run the CSE never
599/// fires, so the reservation (which spills the coldest int scalar to the frame)
600/// would be a pure cost. The invalidation here MIRRORS the runtime exactly —
601/// the cache is dropped at a jump TARGET (control join), when the cached index
602/// slot is reassigned, and across a SysV call — so this returns `true` iff at
603/// least one access would HIT the cache the codegen builds.
604fn has_shared_index_run(ops: &[MicroOp]) -> bool {
605    // Precompute jump-target indices (control joins drop the cache).
606    let mut is_target = vec![false; ops.len()];
607    for op in ops {
608        if let Some(t) = op_target(op) {
609            if let Some(slot) = is_target.get_mut(t) {
610                *slot = true;
611            }
612        }
613    }
614    let mut cache: Option<(Slot, bool)> = None; // (idx, byte)
615    for (i, op) in ops.iter().enumerate() {
616        if is_target[i] {
617            cache = None;
618        }
619        match *op {
620            MicroOp::ArrLoad { idx, byte, .. } | MicroOp::ArrStore { idx, byte, .. } => {
621                if cache == Some((idx, byte)) {
622                    return true; // a reuse hit — the run pays off.
623                }
624                cache = Some((idx, byte));
625            }
626            _ => {}
627        }
628        // Post-op invalidation, mirroring the codegen: a SysV call clobbers the
629        // reserved caller-saved register; a write to the cached index slot makes
630        // the held `im1` stale.
631        if is_sysv_call(op) {
632            cache = None;
633        } else if let Some((idx, _)) = cache {
634            if dest_of(op) == Some(idx) {
635                cache = None;
636            }
637        }
638    }
639    false
640}
641
642/// Whether `op` is a REAL SysV `call` (it clobbers the caller-saved registers per
643/// the ABI). This is the universe of ops the call-weight ranking and the
644/// per-call-site spill machinery key off: the self-call families (`CallSelf`,
645/// `CallSelfCopy`, the mode-B precise `Call`) and the list/string-mutation
646/// helpers (`NewList`, `ArrPush`, `ListClear`, `ListTriple`, `StrAppend`), each
647/// of which enters JIT-runtime code through a SysV `call`. Mirrors the
648/// `has_self_call || has_list_call || has_precise_call` disjunction that defines
649/// `has_call`.
650fn is_sysv_call(op: &MicroOp) -> bool {
651    matches!(
652        op,
653        MicroOp::CallSelf { .. }
654            | MicroOp::CallSelfCopy { .. }
655            | MicroOp::Call { .. }
656            | MicroOp::NewList { .. }
657            | MicroOp::ArrPush { .. }
658            | MicroOp::ListClear { .. }
659            | MicroOp::ListTriple { .. }
660            | MicroOp::StrAppend { .. }
661    )
662}
663
664/// A loop-invariant array ptr/len hoist (Wave 27a): one array, invariant over one
665/// loop span, whose handle slots are loaded ONCE at the loop pre-header into
666/// persistent callee-saved registers and reused for every in-span access.
667#[derive(Clone, Copy)]
668struct HoistPlan {
669    /// The loop head op index (the back-edge target). The hoist loads fire at this
670    /// op's PRE-HEADER (just before binding the head's label), reached only by the
671    /// entry fall-through — the back-edge jumps to the bound label and skips them.
672    head: usize,
673    /// The last op index INSIDE the loop (the back-edge op). The hoist registers
674    /// are live (used by `emit_arr_addr`) for ops in `head..=back`; outside this
675    /// span the array falls back to the per-access frame reload.
676    back: usize,
677    /// The frame-resident ptr slot loaded once and reused for every in-span access.
678    ptr_slot: Slot,
679    /// The len slot, loaded once and reused for every CHECKED in-span access; only
680    /// loaded when `needs_len` (some in-span access of this array is checked).
681    len_slot: Slot,
682    /// Whether any in-span access of this array is CHECKED (so `len` must hoist).
683    needs_len: bool,
684    /// How many `ArrLoad`/`ArrStore` of this array occur in the span — the reuse
685    /// count the hoist amortizes (one pre-header load serves all of them).
686    accesses: u32,
687}
688
689/// Whether op `op` WRITES the frame cell of slot `s` — either it is the op's
690/// destination (a scalar def, an `ArrLoad` dst, a self-call result), OR a
691/// list-mutation helper REFRESHES it through the frame (`ArrPush`/`NewList`/
692/// `ListClear`/`ListTriple` write `frame[vec/ptr/len]` after a possible realloc).
693/// An array whose ptr/len slot is written this way INSIDE a loop is NOT invariant
694/// there and must not be hoisted (its hoisted register would go stale).
695fn op_writes_slot(op: &MicroOp, s: Slot) -> bool {
696    if dest_of(op) == Some(s) {
697        return true;
698    }
699    match *op {
700        MicroOp::NewList { vec_slot, ptr_slot, len_slot, .. }
701        | MicroOp::ArrPush { vec_slot, ptr_slot, len_slot, .. }
702        | MicroOp::ListClear { vec_slot, ptr_slot, len_slot, .. } => {
703            s == vec_slot || s == ptr_slot || s == len_slot
704        }
705        MicroOp::ListTriple { handle_slot, vec_slot, ptr_slot, len_slot, .. } => {
706            s == handle_slot || s == vec_slot || s == ptr_slot || s == len_slot
707        }
708        _ => false,
709    }
710}
711
712/// Identify every loop-invariant array ptr/len hoist available in `ops`. A
713/// candidate is one (`ptr_slot`, `len_slot`) pair that:
714///
715///  1. is accessed by an `ArrLoad`/`ArrStore` INSIDE a natural loop span
716///     `[head, back]` (a back-edge whose `target == head <= back`);
717///  2. has NEITHER slot written anywhere inside `[head, back]` (no scalar def, no
718///     reallocating push/clear/triple of that array) — so `frame[ptr]`/`frame[len]`
719///     are constant across the whole span;
720///  3. enters the loop ONLY via the pre-header fall-through — every transfer that
721///     targets `head` originates INSIDE `[head, back]` (a back-edge). A forward
722///     jump into `head` from outside would skip the pre-header load and read a
723///     stale register, so such a loop is rejected (no hoist).
724///
725/// Frame-residency is NOT decided here (the analysis is purely structural over the
726/// op stream); the caller filters to slots that are actually `Loc::Frame` before
727/// reserving a register (a slot the linear scan already keeps in a register has
728/// no per-access reload to hoist).
729///
730/// For nested loops the INNERMOST span containing an access is preferred: its
731/// pre-header runs once per outer iteration (the highest reuse), and a slot
732/// invariant in the inner loop may be written between inner loops at the outer
733/// level (knapsack's `Set prev to curr`), so the outer span would not be
734/// invariant. We therefore emit one plan per (innermost-loop, array) pair.
735fn loop_invariant_array_hoists(ops: &[MicroOp]) -> Vec<HoistPlan> {
736    // Every natural-loop span: a back-edge at `back` with `target == head <= back`.
737    let mut loops: Vec<(usize, usize)> = Vec::new();
738    for (back, op) in ops.iter().enumerate() {
739        if let Some(head) = op_target(op) {
740            if head <= back {
741                loops.push((head, back));
742            }
743        }
744    }
745    let mut plans: Vec<HoistPlan> = Vec::new();
746    for &(head, back) in &loops {
747        // Entry-discipline guard: every transfer targeting `head` must originate
748        // inside `[head, back]` (a back-edge). A forward jump into `head` from
749        // outside the span would skip the pre-header load → reject this loop.
750        let entry_ok = ops.iter().enumerate().all(|(i, op)| match op_target(op) {
751            Some(t) if t == head => (head..=back).contains(&i),
752            _ => true,
753        });
754        if !entry_ok {
755            continue;
756        }
757        // The array (ptr, len) pairs accessed inside the span, and whether any
758        // access of each is checked. A pair is keyed by ptr_slot (the address
759        // base); the len_slot rides along (a checked access of a given array
760        // always names the same len_slot — the lowering pins one handle triple
761        // per array).
762        let mut accessed: std::collections::BTreeMap<Slot, (Slot, bool, u32)> =
763            std::collections::BTreeMap::new();
764        for op in &ops[head..=back] {
765            match *op {
766                MicroOp::ArrLoad { ptr_slot, len_slot, checked, .. }
767                | MicroOp::ArrStore { ptr_slot, len_slot, checked, .. } => {
768                    let e = accessed.entry(ptr_slot).or_insert((len_slot, false, 0));
769                    e.1 |= checked;
770                    e.2 += 1;
771                }
772                _ => {}
773            }
774        }
775        for (ptr_slot, (len_slot, any_checked, accesses)) in accessed {
776            // Invariance: neither handle slot is written anywhere in the span.
777            let invariant = !ops[head..=back]
778                .iter()
779                .any(|op| op_writes_slot(op, ptr_slot) || op_writes_slot(op, len_slot));
780            if invariant {
781                plans.push(HoistPlan {
782                    head,
783                    back,
784                    ptr_slot,
785                    len_slot,
786                    needs_len: any_checked,
787                    accesses,
788                });
789            }
790        }
791    }
792    // For an array accessed in several NESTED spans, keep only the innermost (the
793    // smallest span) so the pre-header sits at the tightest loop (highest reuse)
794    // and no two plans claim the same array with overlapping spans. Two disjoint
795    // loops touching the same array each keep their own plan.
796    plans.sort_by_key(|p| (p.ptr_slot, p.back - p.head));
797    let mut chosen: Vec<HoistPlan> = Vec::new();
798    for p in plans {
799        let overlaps_kept = chosen.iter().any(|c| {
800            c.ptr_slot == p.ptr_slot && c.head <= p.back && p.head <= c.back
801        });
802        if !overlaps_kept {
803            chosen.push(p);
804        }
805    }
806    chosen
807}
808
809/// A loop-invariant constant hoist (Wave 28): one `LoadConst` op inside a loop
810/// whose `dst` is invariant across the span, materialized ONCE at the loop
811/// pre-header instead of every iteration.
812#[derive(Clone, Copy)]
813struct ConstHoist {
814    /// The op index of the `LoadConst` being hoisted. The emit loop SKIPS this op
815    /// inside the span (the pre-header already loaded its register).
816    op: usize,
817    /// The loop head op index (the back-edge target). The hoist materialization
818    /// fires at this op's PRE-HEADER, reached only by the entry fall-through.
819    head: usize,
820    /// The destination slot the constant lands in (register-resident).
821    dst: Slot,
822    /// The raw 64-bit constant value (`MicroOp::LoadConst::value`).
823    value: i64,
824}
825
826/// Identify every loop-invariant `LoadConst` hoist available in `ops`. A
827/// `LoadConst { dst, value }` at op index `i` is a candidate when:
828///
829///  1. it sits INSIDE a natural loop span `[head, back]` (a back-edge whose
830///     `target == head <= back`) — the INNERMOST such span containing `i` (the
831///     tightest pre-header, the highest reuse: mandelbrot's `2.0`/`4.0` sit in
832///     the 50-iteration inner loop);
833///  2. the loop is entered ONLY via the pre-header fall-through (every transfer
834///     targeting `head` originates inside `[head, back]`) — otherwise a forward
835///     jump into `head` would skip the pre-header materialization and read a
836///     stale register;
837///  3. `dst` is written by NO OTHER op anywhere in `[head, back]` (the ONLY
838///     writer in the span is this `LoadConst`) — so the register holds the same
839///     constant bits on every iteration. A slot reused for two different
840///     constants (a scratch `LoadConst {dst,0}` then `LoadConst {dst,1}`) has two
841///     writers and is therefore NOT hoisted.
842///
843/// Register-residency is NOT decided here (the analysis is purely structural over
844/// the op stream); the caller filters to `dst`s that are actually `Loc::Reg`/
845/// `Loc::Xmm` before materializing (a frame-resident const has no per-iteration
846/// register reload to amortize).
847fn loop_invariant_const_hoists(ops: &[MicroOp]) -> Vec<ConstHoist> {
848    // Every natural-loop span: a back-edge at `back` with `target == head <= back`.
849    let mut loops: Vec<(usize, usize)> = Vec::new();
850    for (back, op) in ops.iter().enumerate() {
851        if let Some(head) = op_target(op) {
852            if head <= back {
853                loops.push((head, back));
854            }
855        }
856    }
857    let mut chosen: Vec<ConstHoist> = Vec::new();
858    for (i, op) in ops.iter().enumerate() {
859        let MicroOp::LoadConst { dst, value } = *op else { continue };
860        // The INNERMOST loop span that contains this op (smallest span). A const is
861        // invariant in every enclosing loop, but the innermost pre-header runs the
862        // most often, and an enclosing loop might write `dst` between inner loops.
863        let inner = loops
864            .iter()
865            .copied()
866            .filter(|&(head, back)| (head..=back).contains(&i))
867            .min_by_key(|&(head, back)| back - head);
868        let Some((head, back)) = inner else { continue };
869        // Entry-discipline: every transfer targeting `head` must originate inside
870        // `[head, back]`. A forward jump into `head` from outside would skip the
871        // pre-header materialization.
872        let entry_ok = ops.iter().enumerate().all(|(j, o)| match op_target(o) {
873            Some(t) if t == head => (head..=back).contains(&j),
874            _ => true,
875        });
876        if !entry_ok {
877            continue;
878        }
879        // Invariance: NO other op in the span writes `dst` (this LoadConst is the
880        // sole writer). A slot reused for two constants has a second writer here.
881        let sole_writer = ops[head..=back]
882            .iter()
883            .enumerate()
884            .all(|(off, o)| head + off == i || dest_of(o) != Some(dst));
885        if sole_writer {
886            chosen.push(ConstHoist { op: i, head, dst, value });
887        }
888    }
889    chosen
890}
891
892/// The largest slot index the region touches, INCLUDING each self-call window's
893/// full extent (`args_start + frame_size - 1`). The frame indexing allocates
894/// `loc`/`written`/liveness bitvectors up to this bound, so it must cover the
895/// callee window staging slots even though those stay frame-resident.
896fn max_slot_of(ops: &[MicroOp]) -> usize {
897    let mut max_slot: usize = 0;
898    let mut buf = Vec::new();
899    for op in ops {
900        buf.clear();
901        slots_of(op, &mut buf);
902        for &s in &buf {
903            max_slot = max_slot.max(s as usize);
904        }
905        if let MicroOp::CallSelf { args_start, frame_size, .. }
906        | MicroOp::CallSelfCopy { args_start, frame_size, .. } = *op
907        {
908            let last = (args_start as i64) + frame_size - 1;
909            if last >= 0 {
910                max_slot = max_slot.max(last as usize);
911            }
912        }
913    }
914    max_slot
915}
916
917/// Loop-weighted reference ranking with a CALL-LOCALITY tie-break.
918///
919/// The PRIMARY key is the loop-weighted reference count: each reference at loop
920/// depth `d` is worth `base^depth`, so a loop-carried value out-ranks a colder
921/// slot referenced more times outside any loop. This key — and therefore the
922/// RESIDENT-vs-SPILLED boundary it induces (the first `pool.len()` slots are
923/// resident) — is EXACTLY the pre-Fix-1 ranking. The call bonus is a SECONDARY
924/// key only, so it can NEVER evict a hotter loop slot from a register: it merely
925/// reorders slots that would be resident anyway.
926///
927/// The SECONDARY key prices CALL-LOCALITY. A slot LIVE ACROSS a SysV call (in
928/// `live_after[call_idx]`) pays a caller-saved spill+reload pair on EVERY
929/// execution of that call unless it lives in a callee-saved register. The bonus
930/// is `call_weight * base^depth(call)` per surviving call (a call inside a hot
931/// loop pays the pair per iteration). Because the `has_call` pool puts the four
932/// CALLEE-SAVED registers FIRST, and a slot's callee-vs-caller placement is
933/// decided by its rank position, lifting call-survivors above call-dead slots OF
934/// EQUAL LOOP WEIGHT steers exactly the survivors into the callee-saved regs —
935/// eliminating their per-call reload for a one-time prologue push/pop — without
936/// disturbing which slots are resident at all.
937///
938/// This ONLY changes which physical register a resident slot occupies (and, on a
939/// primary-key tie, which equally-cold slot spills). A slot still lives in
940/// exactly one place for the whole function and its value never changes, so the
941/// assignment is bit-identical to any other ranking. `live_after` is `None` for a
942/// call-free region (no bonus). Returns the order (descending by the composite
943/// key, then ascending by slot for determinism) and the observed `max_slot`. The
944/// returned per-slot `u64` is the PRIMARY loop-weight (the secondary key is
945/// internal to the sort) — downstream consumes only the slot ORDER.
946fn rank_slots(
947    ops: &[MicroOp],
948    loop_weight_base: u64,
949    loop_depth_cap: u32,
950    call_weight: u64,
951    live_after: Option<&[Vec<bool>]>,
952) -> (Vec<(Slot, u64)>, usize) {
953    let depths = loop_depths(ops);
954    let max_slot = max_slot_of(ops);
955    // PRIMARY: loop-weighted reference count.
956    let mut refs: std::collections::HashMap<Slot, u64> = std::collections::HashMap::new();
957    // SECONDARY: call-survival weight (a separate key, never folded into refs).
958    let mut call_surv: std::collections::HashMap<Slot, u64> = std::collections::HashMap::new();
959    let mut buf = Vec::new();
960    for (idx, op) in ops.iter().enumerate() {
961        let weight = loop_weight_base.saturating_pow(depths[idx].min(loop_depth_cap));
962        buf.clear();
963        slots_of(op, &mut buf);
964        for &s in &buf {
965            let e = refs.entry(s).or_insert(0);
966            *e = e.saturating_add(weight);
967        }
968    }
969    if let Some(la) = live_after {
970        for (idx, op) in ops.iter().enumerate() {
971            if !is_sysv_call(op) {
972                continue;
973            }
974            let call_depth_weight =
975                loop_weight_base.saturating_pow(depths[idx].min(loop_depth_cap));
976            let bonus = call_weight.saturating_mul(call_depth_weight);
977            for (s, &live) in la[idx].iter().enumerate() {
978                if live {
979                    // Only slots that ALSO carry loop/ref weight can ever be
980                    // resident; recording survival for a slot with no refs is
981                    // harmless (it sorts last on the primary key regardless).
982                    let e = call_surv.entry(s as Slot).or_insert(0);
983                    *e = e.saturating_add(bonus);
984                }
985            }
986        }
987    }
988    let mut order: Vec<(Slot, u64)> = refs.iter().map(|(&s, &c)| (s, c)).collect();
989    order.sort_by(|a, b| {
990        // PRIMARY desc, then call-survival desc (the tie-break that picks
991        // callee-saved for the survivor), then slot asc for determinism.
992        b.1.cmp(&a.1)
993            .then_with(|| call_surv.get(&b.0).cmp(&call_surv.get(&a.0)))
994            .then(a.0.cmp(&b.0))
995    });
996    (order, max_slot)
997}
998
999/// Every slot an op READS (its use set), for backward liveness. This enumerates
1000/// the READ operands by POSITION (not by value), so a self-referential op like
1001/// `Add { dst: i, lhs: i, rhs: 1 }` (`i = i + 1`) correctly reports `i` as a USE
1002/// even though it is also the destination — a value-based "all-slots minus dst"
1003/// would wrongly drop it. Every op in the supported subset has a pure write-only
1004/// destination (the operands `lhs`/`rhs`/`src`/`a`/`b`/`c`/`idx`/`cond`/… are the
1005/// reads); `dst` is never itself read by the op. A self-call READS `limit_slot`
1006/// (and a `CallSelfCopy`'s source args) and WRITES `dst`. An
1007/// `ArrStore`/`ArrPush`/`ListClear`/`ListTriple` has no `dst`, so all its slots
1008/// are reads. Used ONLY by the per-call-site spill-liveness lever (a pure
1009/// scheduling optimization): a missed read here would be UNSOUND, so this is the
1010/// authoritative read set — derived as `slots_of` (read ∪ write) minus the
1011/// POSITIONAL destination, by removing exactly the dst's first occurrence is
1012/// wrong; instead we list reads directly.
1013fn read_slots_of(op: &MicroOp, out: &mut Vec<Slot>) {
1014    match *op {
1015        MicroOp::Move { src, .. } | MicroOp::NotInt { src, .. } | MicroOp::NotBool { src, .. } => {
1016            out.push(src)
1017        }
1018        MicroOp::LoadConst { .. } => {}
1019        MicroOp::Add { lhs, rhs, .. }
1020        | MicroOp::Sub { lhs, rhs, .. }
1021        | MicroOp::Mul { lhs, rhs, .. }
1022        | MicroOp::Lt { lhs, rhs, .. }
1023        | MicroOp::Gt { lhs, rhs, .. }
1024        | MicroOp::LtEq { lhs, rhs, .. }
1025        | MicroOp::GtEq { lhs, rhs, .. }
1026        | MicroOp::Eq { lhs, rhs, .. }
1027        | MicroOp::Neq { lhs, rhs, .. }
1028        | MicroOp::BitAnd { lhs, rhs, .. }
1029        | MicroOp::BitOr { lhs, rhs, .. }
1030        | MicroOp::BitXor { lhs, rhs, .. }
1031        | MicroOp::Shl { lhs, rhs, .. }
1032        | MicroOp::Shr { lhs, rhs, .. }
1033        | MicroOp::Div { lhs, rhs, .. }
1034        | MicroOp::Mod { lhs, rhs, .. }
1035        | MicroOp::AddF { lhs, rhs, .. }
1036        | MicroOp::SubF { lhs, rhs, .. }
1037        | MicroOp::MulF { lhs, rhs, .. }
1038        | MicroOp::DivF { lhs, rhs, .. }
1039        | MicroOp::LtF { lhs, rhs, .. }
1040        | MicroOp::GtF { lhs, rhs, .. }
1041        | MicroOp::LtEqF { lhs, rhs, .. }
1042        | MicroOp::GtEqF { lhs, rhs, .. }
1043        | MicroOp::EqF { lhs, rhs, .. }
1044        | MicroOp::NeqF { lhs, rhs, .. } => out.extend([lhs, rhs]),
1045        MicroOp::FmaF { a, b, c, .. } => out.extend([a, b, c]),
1046        MicroOp::IntToFloat { src, .. } | MicroOp::SqrtF { src, .. } => out.push(src),
1047        MicroOp::DivPow2 { lhs, .. } => out.push(lhs),
1048        MicroOp::MagicDivU { lhs, .. } => out.push(lhs),
1049        MicroOp::Branch { lhs, rhs, .. } | MicroOp::BranchF { lhs, rhs, .. } => out.extend([lhs, rhs]),
1050        MicroOp::JumpIfFalse { cond, .. } | MicroOp::JumpIfTrue { cond, .. } => out.push(cond),
1051        MicroOp::Return { src } => out.push(src),
1052        MicroOp::ArrLoad { idx, ptr_slot, len_slot, checked, .. } => {
1053            out.extend([idx, ptr_slot]);
1054            if checked {
1055                out.push(len_slot);
1056            }
1057        }
1058        MicroOp::ArrStore { src, idx, ptr_slot, len_slot, checked, .. } => {
1059            out.extend([src, idx, ptr_slot]);
1060            if checked {
1061                out.push(len_slot);
1062            }
1063        }
1064        MicroOp::ArrPush { src, vec_slot, ptr_slot, len_slot, .. } => {
1065            out.extend([src, vec_slot, ptr_slot, len_slot])
1066        }
1067        MicroOp::ListClear { vec_slot, ptr_slot, len_slot, .. } => {
1068            out.extend([vec_slot, ptr_slot, len_slot])
1069        }
1070        MicroOp::StrAppend { text_handle_slot, src, .. } => {
1071            out.push(text_handle_slot);
1072            if let crate::jit::StrSrc::Byte(s) = src {
1073                out.push(s);
1074            }
1075        }
1076        MicroOp::Call { limit_slot, .. } => out.push(limit_slot),
1077        MicroOp::ListTriple { handle_slot, vec_slot, ptr_slot, len_slot, .. } => {
1078            out.extend([handle_slot, vec_slot, ptr_slot, len_slot])
1079        }
1080        MicroOp::CallSelf { limit_slot, .. } => out.push(limit_slot),
1081        MicroOp::CallSelfCopy { src_start, arg_count, limit_slot, .. } => {
1082            out.push(limit_slot);
1083            for j in 0..arg_count {
1084                out.push(src_start + j);
1085            }
1086        }
1087        MicroOp::Jump { .. } => {}
1088        _ => {}
1089    }
1090}
1091
1092/// Backward LIVENESS: `live_after[i]` is the set of slots that may be READ on
1093/// some control-flow path AFTER op `i` finishes (before being overwritten). A
1094/// standard iterative backward dataflow to a fixpoint over the op stream's CFG
1095/// (fall-through `i+1` plus the branch/jump TARGET of `i`):
1096///
1097///   live_in[i]  = uses(i) ∪ (live_out[i] \ {def(i)})
1098///   live_out[i] = ⋃ live_in[succ]   over the successors of i
1099///
1100/// This is a SOUND OVER-approximation of "definitely dead after the call": every
1101/// slot a later op (on any path, including a loop back-edge) reads is marked
1102/// live, and an unconditional `Jump`/`Return` has no fall-through successor. The
1103/// result drives the spill-liveness lever, which spills/reloads only the
1104/// caller-saved residents that are live AFTER a call site; a slot NOT in
1105/// `live_after[call]` is read by no later op on any path (so the success-path
1106/// reload is dead) and is never observed by a deopt resume either (classic
1107/// replay recomputes mid-call temps from the boundary args; a precise resume at
1108/// a later op re-boxes only its own live slots — a subset of these). Eliding its
1109/// spill is therefore bit-identical. Returns one bitset (`Vec<bool>` over
1110/// `0..=max_slot`) per op.
1111fn liveness_after(ops: &[MicroOp], max_slot: usize) -> Vec<Vec<bool>> {
1112    let n = ops.len();
1113    let width = max_slot + 1;
1114    let mut live_in: Vec<Vec<bool>> = vec![vec![false; width]; n];
1115    // Precompute each op's use set and def (stable across iterations).
1116    let mut uses: Vec<Vec<Slot>> = Vec::with_capacity(n);
1117    let mut defs: Vec<Option<Slot>> = Vec::with_capacity(n);
1118    for op in ops {
1119        let mut u = Vec::new();
1120        read_slots_of(op, &mut u);
1121        uses.push(u);
1122        defs.push(dest_of(op));
1123    }
1124
1125    let mut changed = true;
1126    while changed {
1127        changed = false;
1128        // Process in reverse for faster convergence (data flows backward).
1129        for i in (0..n).rev() {
1130            // live_out[i] = union of live_in over successors.
1131            let mut out = vec![false; width];
1132            // Fall-through successor (i+1) unless this op is an unconditional
1133            // transfer (Jump has no fall-through; Return terminates).
1134            let unconditional_transfer = matches!(ops[i], MicroOp::Jump { .. } | MicroOp::Return { .. });
1135            if !unconditional_transfer {
1136                if let Some(succ) = live_in.get(i + 1) {
1137                    for (o, &s) in out.iter_mut().zip(succ.iter()) {
1138                        *o |= s;
1139                    }
1140                }
1141            }
1142            // Branch/jump target successor.
1143            if let Some(t) = op_target(&ops[i]) {
1144                if let Some(succ) = live_in.get(t) {
1145                    for (o, &s) in out.iter_mut().zip(succ.iter()) {
1146                        *o |= s;
1147                    }
1148                }
1149            }
1150            // live_in[i] = uses ∪ (live_out \ def).
1151            let mut new_in = out.clone();
1152            if let Some(d) = defs[i] {
1153                if (d as usize) < width {
1154                    new_in[d as usize] = false;
1155                }
1156            }
1157            for &u in &uses[i] {
1158                if (u as usize) < width {
1159                    new_in[u as usize] = true;
1160                }
1161            }
1162            if new_in != live_in[i] {
1163                live_in[i] = new_in;
1164                changed = true;
1165            }
1166        }
1167    }
1168
1169    // live_after[i] == live_out[i]: rebuild from the converged live_in.
1170    let mut live_after: Vec<Vec<bool>> = vec![vec![false; width]; n];
1171    for i in 0..n {
1172        let unconditional_transfer = matches!(ops[i], MicroOp::Jump { .. } | MicroOp::Return { .. });
1173        if !unconditional_transfer {
1174            if let Some(succ) = live_in.get(i + 1) {
1175                for (o, &s) in live_after[i].iter_mut().zip(succ.iter()) {
1176                    *o |= s;
1177                }
1178            }
1179        }
1180        if let Some(t) = op_target(&ops[i]) {
1181            if let Some(succ) = live_in.get(t) {
1182                for (o, &s) in live_after[i].iter_mut().zip(succ.iter()) {
1183                    *o |= s;
1184                }
1185            }
1186        }
1187    }
1188    live_after
1189}
1190
1191/// The condition for a `Cmp`.
1192fn cond_of(cmp: Cmp) -> Cond {
1193    match cmp {
1194        Cmp::Lt => Cond::Lt,
1195        Cmp::Gt => Cond::Gt,
1196        Cmp::LtEq => Cond::Le,
1197        Cmp::GtEq => Cond::Ge,
1198        Cmp::Eq => Cond::Eq,
1199        Cmp::NotEq => Cond::Ne,
1200    }
1201}
1202
1203/// The register CLASS a slot may be resident in. A slot that appears in BOTH an
1204/// integer role and a float role (or only ever as a raw-bits/neutral operand) is
1205/// kept frame-resident, where the two classes share the same 8 raw bytes.
1206#[derive(Clone, Copy, Debug, PartialEq, Eq)]
1207enum Class {
1208    /// Used only in integer roles → eligible for a GP register.
1209    Int,
1210    /// Used only in float (f64) roles → eligible for an XMM register.
1211    Float,
1212    /// Used in both classes (or class-ambiguous): frame-resident only.
1213    Mixed,
1214}
1215
1216/// Per-slot role tally for [`Class`] assignment.
1217#[derive(Clone, Copy, Default)]
1218struct Roles {
1219    int: bool,
1220    float: bool,
1221}
1222
1223/// Mark every slot an op touches with its INT and/or FLOAT roles. Type-NEUTRAL
1224/// roles (a `Move`/`LoadConst`/`Return` operand, an `ArrLoad` dst / `ArrStore`
1225/// src — all raw-bits transfers) impose NO class; the slot's class comes from
1226/// the strongly-typed ops it also appears in. A slot with no typed role at all
1227/// defaults to INT (the historical behavior).
1228fn tally_roles(op: &MicroOp, roles: &mut [Roles]) {
1229    macro_rules! mark_int {
1230        ($s:expr) => {
1231            roles[$s as usize].int = true
1232        };
1233    }
1234    match *op {
1235        // Integer arithmetic / compare / bitwise / shift: all operands + dst int.
1236        MicroOp::Add { dst, lhs, rhs }
1237        | MicroOp::Sub { dst, lhs, rhs }
1238        | MicroOp::Mul { dst, lhs, rhs }
1239        | MicroOp::Div { dst, lhs, rhs }
1240        | MicroOp::Mod { dst, lhs, rhs }
1241        | MicroOp::Lt { dst, lhs, rhs }
1242        | MicroOp::Gt { dst, lhs, rhs }
1243        | MicroOp::LtEq { dst, lhs, rhs }
1244        | MicroOp::GtEq { dst, lhs, rhs }
1245        | MicroOp::Eq { dst, lhs, rhs }
1246        | MicroOp::Neq { dst, lhs, rhs }
1247        | MicroOp::BitAnd { dst, lhs, rhs }
1248        | MicroOp::BitOr { dst, lhs, rhs }
1249        | MicroOp::BitXor { dst, lhs, rhs }
1250        | MicroOp::Shl { dst, lhs, rhs }
1251        | MicroOp::Shr { dst, lhs, rhs } => {
1252            mark_int!(dst);
1253            mark_int!(lhs);
1254            mark_int!(rhs);
1255        }
1256        MicroOp::DivPow2 { dst, lhs, .. } | MicroOp::MagicDivU { dst, lhs, .. } => {
1257            mark_int!(dst);
1258            mark_int!(lhs);
1259        }
1260        MicroOp::NotInt { dst, src } | MicroOp::NotBool { dst, src } => {
1261            mark_int!(dst);
1262            mark_int!(src);
1263        }
1264        MicroOp::Branch { lhs, rhs, .. } => {
1265            mark_int!(lhs);
1266            mark_int!(rhs);
1267        }
1268        MicroOp::JumpIfFalse { cond, .. } | MicroOp::JumpIfTrue { cond, .. } => mark_int!(cond),
1269        // Array address machinery is INT; the loaded/stored value is NEUTRAL.
1270        MicroOp::ArrLoad { idx, ptr_slot, len_slot, checked, .. } => {
1271            mark_int!(idx);
1272            mark_int!(ptr_slot);
1273            if checked {
1274                mark_int!(len_slot);
1275            }
1276        }
1277        MicroOp::ArrStore { idx, ptr_slot, len_slot, checked, .. } => {
1278            mark_int!(idx);
1279            mark_int!(ptr_slot);
1280            if checked {
1281                mark_int!(len_slot);
1282            }
1283        }
1284        // The list-mutation handle slots (vec/ptr/len) are INT machinery; the
1285        // pushed VALUE is NEUTRAL (its raw bits travel to the buffer regardless
1286        // of class — a float-list push bit-copies through a GP reg). The handle
1287        // slots are also forced frame-resident, so their class is moot, but
1288        // marking them INT keeps the role tally accurate.
1289        MicroOp::ArrPush { vec_slot, ptr_slot, len_slot, .. } => {
1290            mark_int!(vec_slot);
1291            mark_int!(ptr_slot);
1292            mark_int!(len_slot);
1293        }
1294        MicroOp::ListClear { vec_slot, ptr_slot, len_slot, .. } => {
1295            mark_int!(vec_slot);
1296            mark_int!(ptr_slot);
1297            mark_int!(len_slot);
1298        }
1299        // Float arithmetic: all operands + dst float.
1300        MicroOp::AddF { dst, lhs, rhs }
1301        | MicroOp::SubF { dst, lhs, rhs }
1302        | MicroOp::MulF { dst, lhs, rhs }
1303        | MicroOp::DivF { dst, lhs, rhs } => {
1304            roles[dst as usize].float = true;
1305            roles[lhs as usize].float = true;
1306            roles[rhs as usize].float = true;
1307        }
1308        // Float compares: operands float, RESULT (dst) is an int 0/1.
1309        MicroOp::LtF { dst, lhs, rhs }
1310        | MicroOp::GtF { dst, lhs, rhs }
1311        | MicroOp::LtEqF { dst, lhs, rhs }
1312        | MicroOp::GtEqF { dst, lhs, rhs }
1313        | MicroOp::EqF { dst, lhs, rhs }
1314        | MicroOp::NeqF { dst, lhs, rhs } => {
1315            roles[dst as usize].int = true;
1316            roles[lhs as usize].float = true;
1317            roles[rhs as usize].float = true;
1318        }
1319        MicroOp::BranchF { lhs, rhs, .. } => {
1320            roles[lhs as usize].float = true;
1321            roles[rhs as usize].float = true;
1322        }
1323        MicroOp::FmaF { dst, a, b, c } => {
1324            roles[dst as usize].float = true;
1325            roles[a as usize].float = true;
1326            roles[b as usize].float = true;
1327            roles[c as usize].float = true;
1328        }
1329        MicroOp::SqrtF { dst, src } => {
1330            roles[dst as usize].float = true;
1331            roles[src as usize].float = true;
1332        }
1333        // IntToFloat: src is INT, dst is FLOAT.
1334        MicroOp::IntToFloat { dst, src } => {
1335            roles[dst as usize].float = true;
1336            mark_int!(src);
1337        }
1338        // A self-call: `dst` (the result) and `limit_slot` (the arena bound) are
1339        // INT. The staged scalar args are 8-byte raw copies (NEUTRAL — the copy
1340        // preserves their bits regardless of class), so they impose no class
1341        // here; their class comes from the ops that produced them.
1342        MicroOp::CallSelf { dst, limit_slot, .. } => {
1343            mark_int!(dst);
1344            mark_int!(limit_slot);
1345        }
1346        MicroOp::CallSelfCopy { dst, limit_slot, .. } => {
1347            mark_int!(dst);
1348            mark_int!(limit_slot);
1349        }
1350        // The mode-B precise self-call: `dst` (the result handle, an i64) and
1351        // `limit_slot` are INT. The staged args are NEUTRAL raw copies (their
1352        // class comes from the producing ops).
1353        MicroOp::Call { dst, limit_slot, .. } => {
1354            mark_int!(dst);
1355            mark_int!(limit_slot);
1356        }
1357        // List-machinery handle/triple slots are INT (raw `*mut Vec` words);
1358        // they are also forced frame-resident, so the class is moot, but the
1359        // tally stays accurate.
1360        MicroOp::ListTriple { handle_slot, vec_slot, ptr_slot, len_slot, .. } => {
1361            mark_int!(handle_slot);
1362            mark_int!(vec_slot);
1363            mark_int!(ptr_slot);
1364            mark_int!(len_slot);
1365        }
1366        // The str-append handle (a raw `*mut Value` word) and a byte-form value
1367        // (an ASCII byte) are INT. The handle is forced frame-resident, so its
1368        // class is moot, but the tally stays accurate.
1369        MicroOp::StrAppend { text_handle_slot, src, .. } => {
1370            mark_int!(text_handle_slot);
1371            if let crate::jit::StrSrc::Byte(s) = src {
1372                mark_int!(s);
1373            }
1374        }
1375        // Move / LoadConst / Return are NEUTRAL — no class imposed.
1376        MicroOp::Move { .. }
1377        | MicroOp::LoadConst { .. }
1378        | MicroOp::Return { .. }
1379        | MicroOp::Jump { .. } => {}
1380        _ => {}
1381    }
1382}
1383
1384/// Classify every slot up to `max_slot` into its register [`Class`].
1385fn classify_slots(ops: &[MicroOp], max_slot: usize) -> Vec<Class> {
1386    let mut roles = vec![Roles::default(); max_slot + 1];
1387    for op in ops {
1388        tally_roles(op, &mut roles);
1389    }
1390    roles
1391        .iter()
1392        .map(|r| match (r.int, r.float) {
1393            (_, true) if r.int => Class::Mixed,
1394            (false, true) => Class::Float,
1395            // int-only OR neutral-only (default int).
1396            _ => Class::Int,
1397        })
1398        .collect()
1399}
1400
1401/// A slot's coarse LIVE INTERVAL as a single `[start, end]` span over op indices:
1402/// `start` = the first op that defines or uses it, `end` = the last op that uses
1403/// or defines it. This omits holes (a slot dead in the middle of its span still
1404/// occupies it), so the max overlap of these intervals is an UPPER BOUND on true
1405/// register pressure — the pessimistic input a classic linear-scan allocator
1406/// consumes. Used by the linear-scan SIMULATION the class diagnostic reports, to
1407/// confirm a simple interval allocator would reclaim the spills the whole-region
1408/// per-slot model leaves on the table.
1409#[derive(Clone, Copy, Debug, PartialEq, Eq)]
1410struct LiveInterval {
1411    slot: u16,
1412    start: usize,
1413    end: usize,
1414}
1415
1416/// One coarse `[first-touch, last-touch]` interval per slot the op stream reads
1417/// or writes. A slot never touched gets no interval (it needs no register).
1418fn live_intervals(ops: &[MicroOp], max_slot: usize) -> Vec<LiveInterval> {
1419    let mut first = vec![usize::MAX; max_slot + 1];
1420    let mut last = vec![0usize; max_slot + 1];
1421    let mut reads = Vec::new();
1422    for (i, op) in ops.iter().enumerate() {
1423        reads.clear();
1424        read_slots_of(op, &mut reads);
1425        let mut touch = |s: Slot| {
1426            let s = s as usize;
1427            if s <= max_slot {
1428                if first[s] == usize::MAX {
1429                    first[s] = i;
1430                }
1431                last[s] = i;
1432            }
1433        };
1434        for &s in &reads {
1435            touch(s);
1436        }
1437        if let Some(d) = dest_of(op) {
1438            touch(d);
1439        }
1440    }
1441    (0..=max_slot)
1442        .filter(|&s| first[s] != usize::MAX)
1443        .map(|s| LiveInterval { slot: s as u16, start: first[s], end: last[s] })
1444        .collect()
1445}
1446
1447/// Classic linear-scan over the coarse intervals of ONE register class: how many
1448/// of that class's intervals would SPILL given `regs` physical registers,
1449/// reusing a register the instant an interval expires. The spill victim on
1450/// overflow is the active interval whose end is farthest (the textbook
1451/// heuristic) — but the COUNT (what we report) is heuristic-independent: it is
1452/// `intervals − (intervals placeable in `regs` colors)`, i.e. one spill per
1453/// point where more than `regs` intervals overlap. This is the number the
1454/// whole-region per-slot model fails to achieve (it spills every slot beyond the
1455/// top-`regs` ranked, even when their ranges never overlap).
1456fn linscan_spills(intervals: &[LiveInterval], class: &[Class], want: Class, regs: usize) -> usize {
1457    let mut iv: Vec<&LiveInterval> =
1458        intervals.iter().filter(|i| class[i.slot as usize] == want).collect();
1459    iv.sort_by_key(|i| (i.start, i.end));
1460    // `active` ends, kept sorted ascending so the farthest end is last.
1461    let mut active: Vec<usize> = Vec::new();
1462    let mut spills = 0usize;
1463    for cur in iv {
1464        active.retain(|&e| e >= cur.start); // expire intervals that ended before cur starts
1465        if regs == 0 {
1466            spills += 1;
1467            continue;
1468        }
1469        if active.len() < regs {
1470            let pos = active.partition_point(|&e| e <= cur.end);
1471            active.insert(pos, cur.end);
1472        } else {
1473            // Pool full: spill the farthest-ending of {actives ∪ cur}.
1474            let far = *active.last().unwrap();
1475            if far > cur.end {
1476                active.pop();
1477                let pos = active.partition_point(|&e| e <= cur.end);
1478                active.insert(pos, cur.end);
1479            }
1480            spills += 1;
1481        }
1482    }
1483    spills
1484}
1485
1486/// Per-slot loop-aware live RANGE `[start, end]` (op indices), or `None` if the
1487/// slot is never touched. Built from the `liveness_after` backward fixpoint, so a
1488/// loop-carried slot — live on the back-edge path at every body op — spans the
1489/// whole loop automatically (min..max over its live points), with NO explicit
1490/// loop-extension step. This is the SOUND interval the real allocator consumes
1491/// (unlike `live_intervals`, naive first/last touch, used only by the diagnostic).
1492fn live_ranges(ops: &[MicroOp], max_slot: usize) -> Vec<Option<(usize, usize)>> {
1493    let la = liveness_after(ops, max_slot);
1494    let mut start = vec![usize::MAX; max_slot + 1];
1495    let mut end = vec![0usize; max_slot + 1];
1496    let mut reads = Vec::new();
1497    for i in 0..ops.len() {
1498        // A slot OCCUPIES op i if it is live out of i, used at i, or defined at i
1499        // (live_in is uses ∪ (live_after \ def), all subsumed by these three).
1500        for s in 0..=max_slot {
1501            if la[i][s] {
1502                if start[s] == usize::MAX {
1503                    start[s] = i;
1504                }
1505                end[s] = i;
1506            }
1507        }
1508        reads.clear();
1509        read_slots_of(&ops[i], &mut reads);
1510        let mut touch = |s: usize| {
1511            if s <= max_slot {
1512                if start[s] == usize::MAX {
1513                    start[s] = i;
1514                }
1515                if i > end[s] {
1516                    end[s] = i;
1517                }
1518            }
1519        };
1520        for &u in &reads {
1521            touch(u as usize);
1522        }
1523        if let Some(d) = dest_of(&ops[i]) {
1524            touch(d as usize);
1525        }
1526    }
1527    (0..=max_slot)
1528        .map(|s| (start[s] != usize::MAX).then_some((start[s], end[s])))
1529        .collect()
1530}
1531
1532/// LINEAR-SCAN ASSIGNMENT (the LS1 keystone). Assign each slot a physical
1533/// register of its class for its whole loop-aware live range, REUSING a register
1534/// the instant its previous holder's range expires — so disjoint temporaries
1535/// share registers instead of each consuming a whole-region frame home. Returns a
1536/// per-slot [`Loc`]; a slot that finds no register stays `Loc::Frame` (exactly the
1537/// current behavior, so the fallback is bit-identical).
1538///
1539/// SOUNDNESS — register REUSE is permitted only across a STRAIGHT-LINE gap: a
1540/// register freed at op `e` may host a new range starting at `s` only when no
1541/// control point (a branch/jump OR a jump target) lies in `(e, s]`. That confines
1542/// every register reassignment to a single-predecessor, single-successor stretch,
1543/// so the binding is identical on every path reaching it — no inconsistent state
1544/// at a control join. Overlapping ranges never share (a holder is freed only once
1545/// its end is strictly before the newcomer's start). Loop-carried slots span the
1546/// whole loop (their range covers every body op), so their register is never
1547/// reused mid-loop. The caller gates this to no-deopt / no-call regions so there
1548/// is no mid-region side-exit whose flush would observe a transient binding.
1549fn linscan_assign(ops: &[MicroOp], max_slot: usize, class: &[Class], gp: &[Reg], xmm: &[Xmm]) -> Vec<Loc> {
1550    let n = ops.len();
1551    let ranges = live_ranges(ops, max_slot);
1552    // Control-point prefix sum: ctrl[j] = 1 if op j transfers OR is a jump target.
1553    let mut is_jt = vec![false; n];
1554    for op in ops {
1555        if let Some(t) = op_target(op) {
1556            if t < n {
1557                is_jt[t] = true;
1558            }
1559        }
1560    }
1561    let mut pref = vec![0u32; n + 1];
1562    for j in 0..n {
1563        pref[j + 1] = pref[j] + u32::from(op_target(&ops[j]).is_some() || is_jt[j]);
1564    }
1565    // Any control point in the half-open gap (e, s]?
1566    let ctrl_between = |e: usize, s: usize| -> bool {
1567        let lo = (e + 1).min(n);
1568        let hi = (s + 1).min(n);
1569        pref[hi].saturating_sub(pref[lo]) > 0
1570    };
1571    // Slots with a range, in start order (then end, then slot — deterministic).
1572    let mut order: Vec<usize> = (0..=max_slot).filter(|&s| ranges[s].is_some()).collect();
1573    order.sort_by_key(|&s| {
1574        let (a, b) = ranges[s].unwrap();
1575        (a, b, s)
1576    });
1577    // Fresh pools (never used) popped from the front; freed pools carry the op
1578    // index at which each register became free, for the gap test.
1579    let mut gp_fresh: Vec<Reg> = gp.iter().rev().copied().collect();
1580    let mut xmm_fresh: Vec<Xmm> = xmm.iter().rev().copied().collect();
1581    let mut gp_freed: Vec<(Reg, usize)> = Vec::new();
1582    let mut xmm_freed: Vec<(Xmm, usize)> = Vec::new();
1583    let mut active: Vec<(usize, usize)> = Vec::new(); // (end, slot)
1584    let mut out = vec![Loc::Frame; max_slot + 1];
1585    for &s in &order {
1586        let (st, en) = ranges[s].unwrap();
1587        // Expire holders whose range ended strictly before this one starts.
1588        let mut still = Vec::with_capacity(active.len());
1589        for &(ae, asl) in &active {
1590            if ae < st {
1591                match out[asl] {
1592                    Loc::Reg(r) => gp_freed.push((r, ae)),
1593                    Loc::Xmm(x) => xmm_freed.push((x, ae)),
1594                    Loc::Frame => {}
1595                }
1596            } else {
1597                still.push((ae, asl));
1598            }
1599        }
1600        active = still;
1601        // PREFER A FRESH register over reusing a freed one: a slot in a register
1602        // all to itself needs no spill-at-end, so as long as a never-used register
1603        // remains we pay no reuse overhead — reuse (and its one end-spill) kicks in
1604        // ONLY under real pressure, once the pool is exhausted. This makes a region
1605        // that already fits (<= pool size) bit-identical in COST to the classic
1606        // per-slot assignment, and reclaims frame spills only where they exist.
1607        match class[s] {
1608            Class::Int => {
1609                let pick = gp_fresh.pop().or_else(|| {
1610                    gp_freed
1611                        .iter()
1612                        .position(|&(_, e)| !ctrl_between(e, st))
1613                        .map(|i| gp_freed.swap_remove(i).0)
1614                });
1615                if let Some(r) = pick {
1616                    out[s] = Loc::Reg(r);
1617                    active.push((en, s));
1618                }
1619            }
1620            Class::Float => {
1621                let pick = xmm_fresh.pop().or_else(|| {
1622                    xmm_freed
1623                        .iter()
1624                        .position(|&(_, e)| !ctrl_between(e, st))
1625                        .map(|i| xmm_freed.swap_remove(i).0)
1626                });
1627                if let Some(x) = pick {
1628                    out[s] = Loc::Xmm(x);
1629                    active.push((en, s));
1630                }
1631            }
1632            Class::Mixed => {}
1633        }
1634    }
1635    out
1636}
1637
1638/// Per-self-call support: the label of the "bare return" epilogue that a
1639/// PROPAGATED in-callee deopt jumps to — flush the frame and return WITHOUT
1640/// touching the status cell (which already holds the inner exit code). The
1641/// per-call-site caller-saved spill/reload lists live on [`Gen`]
1642/// (`call_gp`/`call_xmm`), shared with the list-mutation helper-call path.
1643struct SelfCall {
1644    /// The bare flush+return epilogue (no status write) for deopt PROPAGATION.
1645    propagate_label: LabelId,
1646}
1647
1648/// PRECISE (mode-B) deopt support. Every checked op and the precise self-call's
1649/// guards side-exit with the op's ENCODED resume tag (`(pc << 2) | 3`) OR'd with
1650/// the live call depth in the high 32 bits, mirroring `logos_stencil_deopt_at` /
1651/// `logos_stencil_call_precise`. A checked op routes its bounds/divisor failure
1652/// to the out-of-line block for its code (`code_blocks[code]`), which stages the
1653/// tag in `S1` and jumps to the shared `epilogue`. The epilogue reads the live
1654/// depth, ORs it into the high bits, writes the status cell, flushes every
1655/// resident-written slot (so the precise-deopt walk reads the right frame), and
1656/// returns.
1657struct Precise<'a> {
1658    /// The per-op resume codes (parallel to the op stream).
1659    codes: &'a [i64],
1660    /// Distinct non-plain code → its out-of-line tag-staging block label (each
1661    /// block stages the tag in `S1` and jumps to the shared precise epilogue).
1662    code_blocks: &'a std::collections::HashMap<i64, LabelId>,
1663}
1664
1665/// The codegen context: the slot→location map plus the assembler.
1666struct Gen<'a> {
1667    asm: Asm,
1668    /// Per-slot location (GP reg / XMM reg / frame). The location already
1669    /// encodes the slot's register class, so Move/LoadConst/array transfers read
1670    /// `loc` directly to decide whether to bridge between the GP and XMM classes.
1671    /// OWNED (not borrowed) so the linear-scan path can MUTATE a slot's home as
1672    /// the emission walks the ops — a register cache reused across disjoint live
1673    /// ranges (a reused slot is spilled to its frame home and set `Frame` at its
1674    /// interval end). The classic per-slot path sets it once and never mutates.
1675    loc: Vec<Loc>,
1676    /// Op-index → label, for jump targets.
1677    op_labels: &'a [LabelId],
1678    /// The label of the deopt epilogue (set status = 1, flush, return), if any.
1679    deopt_label: Option<LabelId>,
1680    /// Stack-pointer realignment padding applied in the prologue (so RSP is
1681    /// 16-aligned at every `call`) and undone in every epilogue. 0 or 8.
1682    stack_pad: i32,
1683    /// Self-call support (present only for the FUNCTION backend with self-calls).
1684    self_call: Option<SelfCall>,
1685    /// PER-CALL-SITE caller-saved GP residents to spill/reload around the SysV
1686    /// call at op index `i` — `call_gp[i]` is the global caller-saved-GP-resident
1687    /// universe INTERSECTED with the slots LIVE AFTER op `i` (so a resident dead
1688    /// after the call is not spilled/reloaded). Empty for a non-call op. The XMM
1689    /// counterpart lives in `call_xmm`.
1690    call_gp: &'a [Vec<Slot>],
1691    /// PER-CALL-SITE XMM residents to spill/reload around the call at op `i` (all
1692    /// XMM are caller-saved). The live-after subset, parallel to `call_gp`.
1693    call_xmm: &'a [Vec<Slot>],
1694    /// PRECISE (mode-B) deopt support; `None` for the classic region/function
1695    /// paths (which replay from head). When `Some`, every checked op's side exit
1696    /// and the precise self-call's guards route through it.
1697    precise: Option<Precise<'a>>,
1698    /// SCALED-INDEX CSE (Wave 26). Reserved caller-saved register(s) that hold a
1699    /// run's shared 0-based index `im1 = idx - 1` (`im1_reg`) and, when a second
1700    /// register is free, its scaled byte offset `im1 * 8` (`scaled_reg`). Both
1701    /// are `None` when no caller-saved register is free (CSE disabled for the
1702    /// region) or the kill-switch is off. The reserved registers are never
1703    /// assigned to a resident slot and are touched by NOTHING but [`emit_arr_addr`],
1704    /// so they stay live across intervening arithmetic — invalidated only when the
1705    /// index slot is reassigned, at a jump target, or across a SysV call.
1706    off_im1_reg: Option<Reg>,
1707    /// The second reserved register holding the scaled byte offset, when free.
1708    off_scaled_reg: Option<Reg>,
1709    /// The currently-cached run, or `None` when the cache is invalid. `byte`
1710    /// records the element stride the offset was scaled for (1 vs 8); a different
1711    /// stride for the same index slot is a cache miss (recompute). `verified`
1712    /// records whether the FIRST access of the run bounds-checked `im1` (so a
1713    /// later checked access may reuse the cached scaled offset soundly).
1714    off_cache: Option<OffCacheState>,
1715    /// LOOP-INVARIANT PTR/LEN HOIST (Wave 27a). The per-plan reserved registers
1716    /// holding the invariant `frame[ptr_slot]` (and `frame[len_slot]`) for a loop
1717    /// span. Each entry is one [`HoistPlan`] plus the callee-saved registers it
1718    /// claimed. `emit_arr_addr` consults [`Gen::hoisted`] to read the register
1719    /// instead of reloading the frame, but ONLY while codegen is inside the plan's
1720    /// span — the active-set is keyed per op index in the emit loop.
1721    hoists: &'a [HoistEntry],
1722    /// The ptr/len slots whose hoist registers are ACTIVE at the current op (the
1723    /// codegen is inside their span). Reset/recomputed per op in the emit loop;
1724    /// `emit_arr_addr` reads it to decide whether a given ptr/len slot is hoisted
1725    /// here. Indexed lookups are over a tiny vec (at most a few hoists per loop).
1726    active_hoists: Vec<usize>,
1727}
1728
1729/// A resolved [`HoistPlan`]: the plan plus the callee-saved registers reserved to
1730/// hold its invariant `frame[ptr_slot]` (`ptr_reg`) and, when `needs_len`, its
1731/// `frame[len_slot]` (`len_reg`). A plan that could not claim a register is not
1732/// materialized into a `HoistEntry` (it falls back to the per-access reload).
1733#[derive(Clone, Copy)]
1734struct HoistEntry {
1735    plan: HoistPlan,
1736    ptr_reg: Reg,
1737    len_reg: Option<Reg>,
1738}
1739
1740/// The live scaled-index cache: the reserved registers hold `im1`/`im1*8` for
1741/// this `idx` slot at this element stride.
1742#[derive(Clone, Copy)]
1743struct OffCacheState {
1744    idx: Slot,
1745    byte: bool,
1746}
1747
1748impl Gen<'_> {
1749    /// The label an op at index `idx` jumps to on a CHECKED side exit, or `None`
1750    /// when no deopt channel exists (an UNCHECKED op in a region with no checked
1751    /// op — the label is never referenced). In the classic paths this is the
1752    /// shared `deopt_label` (writes status = 1); in the PRECISE path it is the
1753    /// out-of-line tag-staging block for this op's code (which stages the precise
1754    /// tag and jumps to the precise epilogue). A precise op whose code is the
1755    /// plain marker `1` still uses `deopt_label`. The CHECKED-op emitters
1756    /// `.expect()` this where a missing label would be a real bug; the unchecked
1757    /// path never reads it.
1758    fn checked_exit(&self, idx: usize) -> Option<LabelId> {
1759        if let Some(p) = &self.precise {
1760            let code = p.codes[idx];
1761            if code != 1 {
1762                return Some(p.code_blocks[&code]);
1763            }
1764        }
1765        self.deopt_label
1766    }
1767
1768    /// Drop the scaled-index cache: the reserved register(s) no longer hold a
1769    /// usable `im1`/offset for any index. Called at a jump target (control could
1770    /// enter here with a stale register), across a SysV call (which clobbers the
1771    /// caller-saved reserved register), and whenever the cached index slot is
1772    /// reassigned (so the next access recomputes from the fresh index value).
1773    fn invalidate_off_cache(&mut self) {
1774        self.off_cache = None;
1775    }
1776
1777    /// Drop the cache if `op` writes the cached index slot. A write to the slot
1778    /// the cache keys off makes the held `im1` stale; the next access must
1779    /// recompute from the new index value. (A write to ANY other slot leaves the
1780    /// cached `im1` valid, so cross-arithmetic reuse is preserved.)
1781    fn invalidate_off_cache_if_writes_idx(&mut self, op: &MicroOp) {
1782        if let Some(state) = self.off_cache {
1783            if dest_of(op) == Some(state.idx) {
1784                self.off_cache = None;
1785            }
1786        }
1787    }
1788
1789    /// The hoisted register holding `frame[ptr_slot]` for an ACTIVE hoist of this
1790    /// ptr slot, or `None` (use the per-access frame reload). A ptr slot is the
1791    /// address base, so we key on it; the entry's `len_reg` is consulted via
1792    /// [`Gen::hoisted_len`]. Only entries whose span is active at the current op
1793    /// (recorded in `active_hoists`) are eligible — outside the span the register
1794    /// may hold a stale value (a different loop's array, or the slot was rewritten
1795    /// at the outer level), so it must not be read.
1796    fn hoisted_ptr(&self, ptr_slot: Slot) -> Option<Reg> {
1797        self.active_hoists
1798            .iter()
1799            .map(|&k| self.hoists[k])
1800            .find(|e| e.plan.ptr_slot == ptr_slot)
1801            .map(|e| e.ptr_reg)
1802    }
1803
1804    /// The hoisted register holding `frame[len_slot]` for an ACTIVE hoist whose
1805    /// `ptr_slot` matches (the array is identified by its ptr base), or `None`.
1806    /// `None` when the access is unchecked-only-hoisted (no `len_reg` reserved) —
1807    /// the checked path then reloads `len` from the frame as before.
1808    fn hoisted_len(&self, ptr_slot: Slot) -> Option<Reg> {
1809        self.active_hoists
1810            .iter()
1811            .map(|&k| self.hoists[k])
1812            .find(|e| e.plan.ptr_slot == ptr_slot)
1813            .and_then(|e| e.len_reg)
1814    }
1815}
1816
1817impl Gen<'_> {
1818    /// Spill the per-call-site live-after caller-saved residents (GP + XMM) for
1819    /// the call op at index `idx` to their frame slots before a SysV call: the
1820    /// callee clobbers all caller-saved registers. Reloaded by
1821    /// [`Gen::reload_volatiles_at`] after the call so loop-carried values survive.
1822    /// The `call_gp`/`call_xmm` lists are the LIVE-AFTER subset (a resident dead
1823    /// after the call is read by no later op and not observed by a deopt resume,
1824    /// so its spill is elided — bit-identical). The slices live on a separate
1825    /// borrow than `asm`, so this resolves the spill list before mutating the
1826    /// assembler.
1827    fn spill_volatiles_at(&mut self, idx: usize) {
1828        let gp: &[Slot] = self.call_gp.get(idx).map_or(&[], Vec::as_slice);
1829        for &s in gp {
1830            if let Loc::Reg(r) = self.loc[s as usize] {
1831                self.asm.mov_mr(BASE, (s as i32) * 8, r);
1832            }
1833        }
1834        let xmm: &[Slot] = self.call_xmm.get(idx).map_or(&[], Vec::as_slice);
1835        for &s in xmm {
1836            if let Loc::Xmm(x) = self.loc[s as usize] {
1837                self.asm.movsd_mr(BASE, (s as i32) * 8, x);
1838            }
1839        }
1840    }
1841
1842    /// Reload the per-call-site live-after residents for the call op at `idx`.
1843    fn reload_volatiles_at(&mut self, idx: usize) {
1844        let gp: &[Slot] = self.call_gp.get(idx).map_or(&[], Vec::as_slice);
1845        for &s in gp {
1846            if let Loc::Reg(r) = self.loc[s as usize] {
1847                self.asm.mov_rm(r, BASE, (s as i32) * 8);
1848            }
1849        }
1850        let xmm: &[Slot] = self.call_xmm.get(idx).map_or(&[], Vec::as_slice);
1851        for &s in xmm {
1852            if let Loc::Xmm(x) = self.loc[s as usize] {
1853                self.asm.movsd_rm(x, BASE, (s as i32) * 8);
1854            }
1855        }
1856    }
1857}
1858
1859impl<'a> Gen<'a> {
1860    /// Load slot `s` into GP register `r`. An XMM-resident slot is bit-copied
1861    /// via `movq` (defensive — INT-class slots never land in XMM).
1862    fn load(&mut self, r: Reg, s: Slot) {
1863        match self.loc[s as usize] {
1864            Loc::Reg(src) => self.asm.mov_rr(r, src),
1865            Loc::Frame => self.asm.mov_rm(r, BASE, (s as i32) * 8),
1866            Loc::Xmm(x) => self.asm.movq_rx(r, x),
1867        }
1868    }
1869
1870    /// Store GP register `r` into slot `s`. An XMM-resident slot is bit-copied
1871    /// via `movq` (defensive — INT-class slots never land in XMM).
1872    fn store(&mut self, s: Slot, r: Reg) {
1873        match self.loc[s as usize] {
1874            Loc::Xmm(x) => self.asm.movq_xr(x, r),
1875            Loc::Reg(dst) => self.asm.mov_rr(dst, r),
1876            Loc::Frame => self.asm.mov_mr(BASE, (s as i32) * 8, r),
1877        }
1878    }
1879
1880    /// Materialize slot `s` into a register usable as an operand, preferring its
1881    /// resident register (no move) and falling back to scratch `scratch`. Slot
1882    /// must be INT-class or frame-resident (an XMM-resident slot would be a
1883    /// classification bug — caught by `debug_assert`).
1884    fn operand(&mut self, s: Slot, scratch: Reg) -> Reg {
1885        match self.loc[s as usize] {
1886            Loc::Reg(r) => r,
1887            Loc::Frame => {
1888                self.asm.mov_rm(scratch, BASE, (s as i32) * 8);
1889                scratch
1890            }
1891            Loc::Xmm(_) => {
1892                debug_assert!(false, "int operand on an XMM-resident slot {s}");
1893                self.asm.mov_rm(scratch, BASE, (s as i32) * 8);
1894                scratch
1895            }
1896        }
1897    }
1898
1899    /// Load f64 slot `s` into XMM register `x`.
1900    fn fload(&mut self, x: Xmm, s: Slot) {
1901        match self.loc[s as usize] {
1902            Loc::Xmm(src) => self.asm.movsd_rr(x, src),
1903            Loc::Frame => self.asm.movsd_rm(x, BASE, (s as i32) * 8),
1904            Loc::Reg(r) => self.asm.movq_xr(x, r), // a Mixed slot can never be here; defensive bit-copy.
1905        }
1906    }
1907
1908    /// Store XMM register `x` into f64 slot `s`.
1909    fn fstore(&mut self, s: Slot, x: Xmm) {
1910        match self.loc[s as usize] {
1911            Loc::Xmm(dst) => self.asm.movsd_rr(dst, x),
1912            Loc::Frame => self.asm.movsd_mr(BASE, (s as i32) * 8, x),
1913            Loc::Reg(r) => self.asm.movq_rx(r, x), // defensive (Mixed never lands here).
1914        }
1915    }
1916
1917    /// Materialize f64 slot `s` into an XMM operand, preferring its resident XMM
1918    /// register and falling back to scratch `scratch`.
1919    fn foperand(&mut self, s: Slot, scratch: Xmm) -> Xmm {
1920        match self.loc[s as usize] {
1921            Loc::Xmm(x) => x,
1922            Loc::Frame => {
1923                self.asm.movsd_rm(scratch, BASE, (s as i32) * 8);
1924                scratch
1925            }
1926            Loc::Reg(r) => {
1927                self.asm.movq_xr(scratch, r);
1928                scratch
1929            }
1930        }
1931    }
1932}
1933
1934/// Compile a region of micro-ops into ONE contiguous register-allocated x86-64
1935/// function, callable through the [`crate::buffer::JitChain`] ABI. Returns
1936/// `None` (caller falls back to the stencil tier) when the region contains an
1937/// unsupported op or has no terminating `Return`/`Jump`.
1938///
1939/// A REGION never contains a call, so this gates on the call-free [`supported`]
1940/// subset and emits no self-call machinery.
1941pub fn compile_region_regalloc(
1942    ops: &[MicroOp],
1943    shared_status: Option<std::sync::Arc<AtomicI64>>,
1944) -> Option<CompiledChain> {
1945    compile_impl(ops, shared_status, false, 0, None)
1946}
1947
1948/// Compile a PRECISE REGION — the in-place-array-mutation shape that ALSO does a
1949/// reallocating `ArrPush` (the fannkuch permutation rebuild, the graph_bfs BFS
1950/// frontier) — into ONE contiguous register-allocated x86-64 region with PRECISE
1951/// deopt. Returns `None` (caller falls back to the per-piece precise stencil
1952/// tier) on any unsupported op or a missing terminator.
1953///
1954/// A precise region is the keystone Wave 13 (`ArrPush` in NON-precise regions)
1955/// and Wave 15 (PRECISE deopt for in-place-mutating recursion) each handled HALF
1956/// of: a `ListPush` that REALLOCS the pinned buffer coexisting with an in-place
1957/// `SetIndex` whose replay-from-head would double-apply. Under the classic
1958/// discard-replay deopt the truncate rolls the pushes back but the in-place
1959/// write persists, so the region must instead resume AT the faulting op (no
1960/// replay). [`compile_region_regalloc`] handles the reallocating push (the
1961/// helper refreshes the pinned ptr/len in the frame after a possible realloc),
1962/// but only with the CLASSIC deopt; this entry adds the per-op PRECISE deopt
1963/// codes so the push+SetIndex region is sound.
1964///
1965/// SOUNDNESS of the deopt resume (no double-apply, grown-array materialization):
1966/// every checked op's side exit stores its encoded resume pc `(pc << 2) | 3`
1967/// (depth in the high bits) through the shared status cell and the precise
1968/// epilogue FLUSHES every resident-written scalar to its frame slot — so the
1969/// VM's region precise resume reads each scalar from the frame and re-boxes it
1970/// by kind. The grown array needs NO materialization step: the push helper grew
1971/// the `Vec` IN PLACE inside the same `Rc<RefCell<…>>` the VM register still
1972/// holds, so the VM keeps that register's live value (the precise re-box kind is
1973/// `None` for a pinned array) and it already reflects the post-push buffer +
1974/// length. The resume pc is AFTER the push, so the bytecode never re-runs it —
1975/// no double-apply, no lost appended element. (The VM's per-array entry snapshot
1976/// / truncate rollback is gated on a CLASSIC `Deopt`, never a precise `DeoptAt`,
1977/// so completed pushes stand.)
1978///
1979/// `deopt_codes` is the per-op resume table the region adapter built (parallel
1980/// to `ops`): a plain `1` keeps the op on the ordinary deopt terminal; any other
1981/// value is the precise tag emitted on that op's side exit. `depth_addr` is the
1982/// live-depth cell whose value rides the resume tag's high 32 bits.
1983pub fn compile_region_regalloc_precise(
1984    ops: &[MicroOp],
1985    shared_status: Option<std::sync::Arc<AtomicI64>>,
1986    depth_addr: i64,
1987    deopt_codes: &[i64],
1988) -> Option<CompiledChain> {
1989    if deopt_codes.len() != ops.len() {
1990        return None;
1991    }
1992    // A precise region always needs the status channel — every checked op's side
1993    // exit stores its tagged resume value through it.
1994    if shared_status.is_none() {
1995        return None;
1996    }
1997    compile_impl(ops, shared_status, false, depth_addr, Some(deopt_codes))
1998}
1999
2000/// Compile a recursive FUNCTION's micro-op stream into ONE contiguous
2001/// register-allocated x86-64 function — including its DIRECT self-calls
2002/// (`CallSelf`/`CallSelfCopy`) — callable through the [`crate::buffer::JitChain`]
2003/// ABI. Returns `None` (caller falls back to the per-piece stencil tier) on any
2004/// unsupported op (a cross-function `Call`, list/map ops, byte arrays, …) or a
2005/// missing terminator.
2006///
2007/// The self-call is a REAL SysV `call` to this same chain's entry. The entry
2008/// address is unknown until the code is mapped, so it rides an `Arc<AtomicI64>`
2009/// ENTRY CELL whose address is baked into every call site (`mov rax,[cell];
2010/// call rax`) and which is written with `chain.base()` AFTER mapping — mirroring
2011/// `logos_stencil_call_self`'s patched-entry word (an unpatched `0` deopts). The
2012/// cell is kept alive by the returned [`CompiledChain`].
2013///
2014/// `depth_addr` is the live-depth cell's address (the caller's `ctx.depth`),
2015/// matching the function tier's wiring; the self-call increments/decrements it
2016/// and side-exits (status = 5) at [`SELF_CALL_DEPTH_LIMIT`].
2017pub fn compile_function_regalloc(
2018    ops: &[MicroOp],
2019    shared_status: Option<std::sync::Arc<AtomicI64>>,
2020    depth_addr: i64,
2021) -> Option<CompiledChain> {
2022    // A function with no self-call is just a region; route it through the
2023    // call-free path (no entry cell, no spill machinery) for uniformity.
2024    let has_self_call = ops
2025        .iter()
2026        .any(|op| matches!(op, MicroOp::CallSelf { .. } | MicroOp::CallSelfCopy { .. }));
2027    if !has_self_call {
2028        return compile_impl(ops, shared_status, true, depth_addr, None);
2029    }
2030    // A self-call always needs the status channel (its guards write it).
2031    if shared_status.is_none() {
2032        return None;
2033    }
2034    compile_impl(ops, shared_status, true, depth_addr, None)
2035}
2036
2037/// Compile a LIST-PARAMETER (mode-B) recursive FUNCTION — one that mutates a
2038/// SHARED pinned array in place and may self-call — into ONE contiguous
2039/// register-allocated x86-64 function with PRECISE deopt. Returns `None`
2040/// (caller falls back to the per-piece stencil tier) on any unsupported op.
2041///
2042/// Where [`compile_function_regalloc`] uses CLASSIC deopt (a side exit replays
2043/// the whole function from its boundary args — sound only when every effect is
2044/// confined to the private frame, the scalar mode-A case), a list-param
2045/// function's in-place array writes land in SHARED state, so a replay would
2046/// DOUBLE-APPLY them. This entry instead emits PRECISE deopt: every checked op
2047/// and the self-call's guards side-exit through the shared status cell with the
2048/// op's ENCODED resume value (`(bytecode_pc << 2) | 3 | (depth << 32)`), exactly
2049/// as `logos_stencil_deopt_at` / `logos_stencil_call_precise` do, so the VM
2050/// materializes the native call chain and resumes interpreting AT the faulting
2051/// op — every prior in-place mutation intact, never re-applied.
2052///
2053/// `deopt_codes` is the per-op resume table the function adapter built (parallel
2054/// to `ops`): a plain `1` keeps the op on the ordinary deopt terminal; any other
2055/// value is the precise tag emitted on that op's side exit.
2056///
2057/// SOUNDNESS: the precise contract requires that on a side exit the native
2058/// frame mirror the tree-walker's full register state. The backend therefore
2059/// keeps every mode-B machinery slot (the plant window/resume/dst, the pin
2060/// triples, the disjoint callee window) FRAME-RESIDENT, every `ListTriple`
2061/// HANDLE slot frame-resident (the helper reads/writes the frame), and FLUSHES
2062/// every resident-written VM register to its frame slot at every precise side
2063/// exit (so `materialize` reads the correct value). All slots at or above the
2064/// function's register count `rc` are mode-B machinery and stay frame-resident.
2065pub fn compile_function_regalloc_precise(
2066    ops: &[MicroOp],
2067    shared_status: Option<std::sync::Arc<AtomicI64>>,
2068    depth_addr: i64,
2069    deopt_codes: &[i64],
2070) -> Option<CompiledChain> {
2071    if deopt_codes.len() != ops.len() {
2072        return None;
2073    }
2074    // A precise function always needs the status channel (its checked ops and
2075    // self-call guards write the tagged value through it).
2076    if shared_status.is_none() {
2077        return None;
2078    }
2079    compile_impl(ops, shared_status, true, depth_addr, Some(deopt_codes))
2080}
2081
2082/// The VM register count `rc` of a mode-B precise function, INFERRED from the
2083/// op stream: the prologue's plant-window invalidation is `LoadConst { dst:
2084/// rc + 2, value: -1 }` (the adapter's first micro). Every slot `>= rc` is
2085/// mode-B machinery (plant slots, pin triples, the disjoint callee window) and
2086/// must stay frame-resident. Returns `None` if the stream does not open with
2087/// that recognizable prologue (then the precise path declines and the caller
2088/// falls back).
2089fn infer_mode_b_rc(ops: &[MicroOp]) -> Option<u16> {
2090    match ops.first() {
2091        Some(MicroOp::LoadConst { dst, value: -1 }) if *dst >= 2 => Some(dst - 2),
2092        _ => None,
2093    }
2094}
2095
2096/// The shared codegen core for both the region and the function backends.
2097/// `is_function` selects the op-support gate ([`supported_function`] adds the
2098/// self-call ops); `depth_addr` is the live-depth cell (function backend only).
2099fn compile_impl(
2100    ops: &[MicroOp],
2101    shared_status: Option<std::sync::Arc<AtomicI64>>,
2102    is_function: bool,
2103    depth_addr: i64,
2104    precise: Option<&[i64]>,
2105) -> Option<CompiledChain> {
2106    if ops.is_empty() {
2107        return None;
2108    }
2109    // SIMD: a recognized pure element-wise map region compiles to a 2-wide packed
2110    // loop. It has NO deopt paths (the recognizer requires `checked:false` Oracle-
2111    // proven-in-bounds accesses and pure float arithmetic), so it runs to
2112    // Completed and the VM resumes at the stored exit_pc — independent of the
2113    // `precise` mode-B machinery. Bit-identical to the scalar region (each lane is
2114    // the scalar op on that index); the differential gate certifies it. Default
2115    // OFF (`LOGOS_SIMD=1`).
2116    if !is_function && simd_enabled() {
2117        if let Some(plan) = crate::vectorize::recognize_elementwise_map(ops) {
2118            if let Some(code) =
2119                crate::vectorize::emit_map_kernel(&ops[plan.body_start..plan.body_end], &plan)
2120            {
2121                let chain = JitChain::from_code(&code, ops.len()).ok()?;
2122                return Some(CompiledChain::from_chain(chain, None));
2123            }
2124        }
2125    }
2126    // The PRECISE mode-B FUNCTION path needs the function's VM register count
2127    // `rc` to know which slots are machinery (the plant window/resume/dst, pin
2128    // triples, the disjoint callee window — every slot `>= rc`). Infer it from
2129    // the adapter's prologue micro; decline (fall back) if the stream does not
2130    // open with it. A precise REGION has NO mode-B machinery (no self-call plant
2131    // window — its in-place mutation + reallocating push flow through the
2132    // enclosing frame's pinned arrays, and the precise epilogue flushes every
2133    // resident-written scalar back to the frame for materialization), so it
2134    // leaves `mode_b_rc = None` and its scalars are register-allocated freely.
2135    // The classic paths also leave `mode_b_rc = None`.
2136    let mode_b_rc: Option<u16> = if precise.is_some() && is_function {
2137        Some(infer_mode_b_rc(ops)?)
2138    } else {
2139        None
2140    };
2141    let gate: fn(&MicroOp) -> bool = match (is_function, precise.is_some()) {
2142        (true, true) => supported_function_precise,
2143        (true, false) => supported_function,
2144        // A precise REGION uses the region op-support gate (`supported`, which
2145        // already admits the reallocating `ArrPush` and `ListClear` — W13); the
2146        // precise deopt codes ride the same per-op side-exit machinery as the
2147        // function path.
2148        _ => supported,
2149    };
2150    if !ops.iter().all(gate) {
2151        return None;
2152    }
2153    if !matches!(ops.last(), Some(MicroOp::Return { .. }) | Some(MicroOp::Jump { .. })) {
2154        return None;
2155    }
2156
2157    let has_self_call = ops
2158        .iter()
2159        .any(|op| matches!(op, MicroOp::CallSelf { .. } | MicroOp::CallSelfCopy { .. }));
2160    // The mode-B precise self-call is a REAL SysV call through the entry table.
2161    let has_precise_call = ops.iter().any(|op| matches!(op, MicroOp::Call { .. }));
2162
2163    // A list-mutation / list-machinery op (`ArrPush`/`ListClear`/`NewList`/
2164    // `ListTriple`) is a HELPER CALL into the JIT runtime — a real SysV `call`,
2165    // so it clobbers the caller-saved registers exactly like a self-call and
2166    // needs the same spill/reload machinery (the `gp_volatile`/`xmm_volatile`
2167    // lists + the stack-alignment pad). Treat ANY such helper call, not just a
2168    // self-call, as requiring the call ABI.
2169    let has_list_call = ops.iter().any(|op| {
2170        matches!(
2171            op,
2172            MicroOp::NewList { .. }
2173                | MicroOp::ArrPush { .. }
2174                | MicroOp::ListClear { .. }
2175                | MicroOp::ListTriple { .. }
2176                | MicroOp::StrAppend { .. }
2177        )
2178    });
2179    let has_call = has_self_call || has_list_call || has_precise_call;
2180
2181    // The vec/ptr/len handle TRIPLE of every list-mutation op MUST stay
2182    // FRAME-resident: the helper reads `frame[vec_slot]` and WRITES
2183    // `frame[ptr_slot]`/`frame[len_slot]` (refreshing them after a possible
2184    // realloc). A register-resident handle slot would be stale after the call
2185    // (the helper updates the frame, not the register) and would feed a wrong
2186    // pointer/length into the next access — the exact realloc-coherence trap.
2187    // Forcing the triple frame-resident makes the helper's frame writes the
2188    // single source of truth, so the next access reads the fresh pointer.
2189    let mut force_frame_set: std::collections::HashSet<Slot> = std::collections::HashSet::new();
2190    for op in ops {
2191        match *op {
2192            // `NewList` allocates a registry-owned fresh buffer and WRITES its
2193            // vec/ptr/len triple through the frame (the helper indexes the frame);
2194            // the same realloc-coherence rule as `ArrPush` requires the triple be
2195            // frame-resident so the helper's writes are the single source of
2196            // truth and every later push/load reads the fresh pointer/length.
2197            MicroOp::NewList { vec_slot, ptr_slot, len_slot, .. }
2198            | MicroOp::ArrPush { vec_slot, ptr_slot, len_slot, .. }
2199            | MicroOp::ListClear { vec_slot, ptr_slot, len_slot, .. } => {
2200                force_frame_set.insert(vec_slot);
2201                force_frame_set.insert(ptr_slot);
2202                force_frame_set.insert(len_slot);
2203            }
2204            // A triple-plant's HANDLE slot is read by the helper FROM THE FRAME,
2205            // and its vec/ptr/len triple is WRITTEN to the frame — all four must
2206            // be frame-resident so the helper's view and the next access agree.
2207            MicroOp::ListTriple { handle_slot, vec_slot, ptr_slot, len_slot, .. } => {
2208                force_frame_set.insert(handle_slot);
2209                force_frame_set.insert(vec_slot);
2210                force_frame_set.insert(ptr_slot);
2211                force_frame_set.insert(len_slot);
2212            }
2213            // The str-append's handle slot (a `*mut Value`) is read by the helper
2214            // FROM THE FRAME, so it must stay frame-resident (a register-resident
2215            // handle would not be seen by the SysV call). The accumulator itself
2216            // lives in the VM register file, off-frame — nothing else here needs
2217            // forcing.
2218            MicroOp::StrAppend { text_handle_slot, .. } => {
2219                force_frame_set.insert(text_handle_slot);
2220            }
2221            _ => {}
2222        }
2223    }
2224
2225    // The callee-window staging slots (`args_start..`) of a self-call MUST stay
2226    // FRAME-resident: the callee reads its parameters from its own frame window,
2227    // and the codegen stages them by writing those frame cells. A `CallSelfCopy`
2228    // also carries `frame_size` (the callee's full frame, = limit_slot + 1); the
2229    // window therefore spans `args_start .. args_start + frame_size`. Force every
2230    // slot at or above the lowest such window base to the frame.
2231    let mut force_frame_above: Option<u16> = None;
2232    for op in ops {
2233        let args_start = match *op {
2234            MicroOp::CallSelf { args_start, .. }
2235            | MicroOp::CallSelfCopy { args_start, .. }
2236            | MicroOp::Call { args_start, .. } => args_start,
2237            _ => continue,
2238        };
2239        // The callee window starts at args_start; everything at/above it stages
2240        // a callee frame slot and must stay frame-resident.
2241        force_frame_above = Some(force_frame_above.map_or(args_start, |a| a.min(args_start)));
2242    }
2243    // PRECISE (mode-B): EVERY slot at or above the VM register count `rc` is
2244    // machinery — the plant window/resume/dst, the pin triples and the disjoint
2245    // callee window. All must stay FRAME-resident: the precise-deopt walk
2246    // (`ChainFn::materialize`) reads each ancestor frame's plant slots and pin
2247    // handles straight from the frame, so a register copy would be stale. This
2248    // subsumes (and is stricter than) the per-call window forcing above.
2249    if let Some(rc) = mode_b_rc {
2250        force_frame_above = Some(force_frame_above.map_or(rc, |a| a.min(rc)));
2251    }
2252
2253    // Whether the program has any checked op needing a deopt channel (divisor
2254    // check, a checked array bounds check, or a self-call guard). When one is
2255    // present but the caller passed no shared cell, allocate a private one —
2256    // exactly what the stencil compilers do (`has_checked` ⇒ `unwrap_or_else`).
2257    let needs_status = ops.iter().any(needs_deopt);
2258    let status: Option<std::sync::Arc<AtomicI64>> = if needs_status {
2259        Some(shared_status.unwrap_or_else(|| std::sync::Arc::new(AtomicI64::new(0))))
2260    } else {
2261        shared_status
2262    };
2263
2264    // The largest slot index — sizes every per-slot vector below (loc / written
2265    // / liveness). It covers each self-call window's full extent because the
2266    // frame indexing allocates against it.
2267    let max_slot = max_slot_of(ops);
2268
2269    // LS1 — linear-scan register allocation gate. Enabled for NO-CALL classic
2270    // regions (the precise mode-B path is excluded). A CHECKED op (bounds/divisor)
2271    // is allowed: its classic side-exit replays from the head with prefix-
2272    // idempotence (native_tier.rs), so the deopt flush must hold each slot's value
2273    // AT the faulting op. Linear scan keeps that true by WRITING THROUGH every
2274    // REUSED slot to its frame home on each write (so the shared epilogue, which
2275    // skips it, finds the current value in the frame); NON-reused slots keep a
2276    // fixed register for their whole range and are flushed by the epilogue from
2277    // that register. Register reuse is confined to straight-line gaps
2278    // (`linscan_assign`), so no reassignment crosses a control join.
2279    let linscan_active = linear_scan_enabled() && !has_call && precise.is_none();
2280
2281    // Backward liveness, computed ONCE when the region has a SysV call and reused
2282    // by both the call-weight ranking (below) and the per-call-site spill-elision
2283    // pass (further down). `None` for a call-free region: there are no calls to
2284    // survive, so no bonus and no spill machinery. Computing it before the
2285    // ranking is what lets the ranking SEE which slots are live across each call.
2286    let live_after: Option<Vec<Vec<bool>>> = has_call.then(|| liveness_after(ops, max_slot));
2287
2288    // Rank slots by LOOP-WEIGHTED reference count, with CALL-LOCALITY as a
2289    // SECONDARY tie-break; assign the hottest to physical registers. A raw count
2290    // under-prices loop-carried values: a slot touched once per iteration of a
2291    // hot loop costs a memory round-trip on EVERY iteration, whereas a slot
2292    // touched many times in straight-line setup code costs a round-trip only that
2293    // once. The linear-scan loop weight (each reference at loop depth `d` worth
2294    // `LOOP_WEIGHT_BASE^d`) closes this so a back-edge value (nbody's dx/dy/dist,
2295    // mandelbrot's zr/zi) outranks colder slots and STAYS RESIDENT across
2296    // iterations. This PRIMARY key — and the resident-vs-spilled boundary it
2297    // induces — is identical to the pre-Fix-1 ranking.
2298    //
2299    // The call-weight breaks TIES among equal-primary-weight slots toward those
2300    // LIVE ACROSS a SysV call. Combined with the `has_call` pool order
2301    // (callee-saved first), a call-survivor wins a callee-saved register over a
2302    // call-dead slot of equal weight and pays NO per-call spill/reload — only a
2303    // one-time prologue push/pop, the dominant cost in a recursion-heavy loop
2304    // (fib / binary_trees / coins). Because it is only a tie-break it can never
2305    // evict a hotter loop slot, so it adds the steering without the spill-boundary
2306    // perturbation an additive bonus would cause.
2307    //
2308    // Ranking is semantically inert: it changes only WHICH register a resident
2309    // slot occupies (and, on a primary-key tie, which equally-cold slot spills). A
2310    // slot still lives in exactly one place for the whole function and its value
2311    // never changes, so the result is bit-identical to any other ranking.
2312    //
2313    // The base/cap mirror the per-piece tier's `select_pins` loop weight
2314    // (`BASE=16`, depth capped at 4) so both backends rank loop-carried slots on
2315    // the SAME frequency model. Every weight is `u64` and saturating, so even a
2316    // pathologically deep/large region cannot overflow the ranking key (a
2317    // saturated tie falls back to the call-survival then deterministic slot-index
2318    // order).
2319    const LOOP_WEIGHT_BASE: u64 = 16;
2320    const LOOP_DEPTH_CAP: u32 = 4;
2321    const CALL_WEIGHT: u64 = 4;
2322    // A zero call-weight zeroes the secondary key, reducing `rank_slots` to the
2323    // pure loop-weighted ranking — the kill-switch is exactly pre-Fix-1 behavior.
2324    let call_weight = if call_weight_enabled() { CALL_WEIGHT } else { 0 };
2325    let (order, _) = rank_slots(
2326        ops,
2327        LOOP_WEIGHT_BASE,
2328        LOOP_DEPTH_CAP,
2329        call_weight,
2330        live_after.as_deref(),
2331    );
2332
2333    // Classify each slot. INT-class slots compete for GP registers, FLOAT-class
2334    // slots for XMM registers; MIXED slots (used by both classes) stay frame-
2335    // resident. The two pools are disjoint, so the assignment is sound: a slot
2336    // is always in exactly one place, of the right class, for the whole function.
2337    let class = classify_slots(ops, max_slot);
2338
2339    // Register-pool ORDER. Pool order is semantically inert — it only decides
2340    // WHICH physical register a slot occupies, never where (a slot is still in
2341    // exactly one place for the whole function), so every ordering is
2342    // bit-identical. But it changes the prologue/epilogue and per-call spill
2343    // bill, so we order by what is cheapest for THIS region's call shape:
2344    //
2345    //  - CALL-FREE region: a caller-saved register costs NOTHING (no prologue
2346    //    push/pop, no spill — nothing clobbers it). A callee-saved register
2347    //    costs a prologue `push` + epilogue `pop` (and a stack-alignment pad
2348    //    risk). So prefer CALLER-SAVED first: a hot scalar loop (primes,
2349    //    pi_leibniz, fib_iterative, the matrix/histogram inner loops) pays no
2350    //    callee-save prologue/epilogue at all when it fits the 6 caller-saved
2351    //    GP regs.
2352    //  - region/function WITH a SysV call (self-call or list-mutation helper):
2353    //    a caller-saved resident is clobbered by every call, so it is
2354    //    spilled+reloaded around each one (gp_volatile below) — a pair of
2355    //    memory ops per call. A callee-saved resident survives the call for the
2356    //    one-time prologue/epilogue push/pop. In a call-heavy loop (fib /
2357    //    binary_trees / coins recursion) the per-call spill pair dominates the
2358    //    one-time save, so prefer CALLEE-SAVED first.
2359    let mut pool: Vec<Reg> = Vec::new();
2360    if has_call {
2361        pool.extend_from_slice(&CALLEE_SAVED);
2362        pool.extend_from_slice(&CALLER_SAVED);
2363    } else {
2364        pool.extend_from_slice(&CALLER_SAVED);
2365        pool.extend_from_slice(&CALLEE_SAVED);
2366    }
2367
2368    // SCALED-INDEX CSE register reservation (Wave 26). A region with array
2369    // accesses RESERVES up to two CALLER-SAVED registers OUT of the resident
2370    // pool — held for the run-shared `im1 = idx - 1` (and, with the second, its
2371    // scaled byte offset `im1 * 8`). Reserving up front (rather than scavenging a
2372    // register the scan happened to leave free) is what makes the CSE fire in the
2373    // HIGH-PRESSURE loops it targets — nbody/matrix touch many co-indexed
2374    // arrays, whose ptr/len handle slots would otherwise consume every GP
2375    // register and leave none free. The cost is that the one or two
2376    // LOWEST-ranked int slots spill to the frame instead of a register — a good
2377    // trade in an array-heavy loop, where eliminating the per-access index
2378    // recompute across N arrays dominates one extra frame round-trip on a cold
2379    // scalar. CALLER-SAVED registers need no prologue save, and the cache is
2380    // dropped across every SysV call, so a call's clobber of them is harmless.
2381    // The reserved registers are removed from `pool`, so the linear scan never
2382    // assigns them to a resident — they are exclusively the CSE cache's.
2383    // Reserve ONLY when the region actually has a reusable run — two or more
2384    // array accesses sharing one index slot with no intervening invalidation.
2385    // Without a shared-index run the CSE never fires, so reserving a register
2386    // (spilling a hot scalar) would be a pure cost — exactly the matrix_mult
2387    // inner loop, whose three accesses use THREE different indices (`i*n+k`,
2388    // `k*n+j`, `i*n+j`) and so must NOT pay the reservation. The static check
2389    // mirrors the runtime cache invalidation precisely.
2390    let (off_im1_reg, off_scaled_reg) = if off_cse_enabled() && has_shared_index_run(ops) {
2391        // The reservable caller-saved registers still in the pool, in pool order;
2392        // take from the END so residents keep the higher-priority (earlier)
2393        // registers and only the coldest int slots lose a register.
2394        let mut reservable: Vec<Reg> =
2395            pool.iter().copied().filter(|r| CALLER_SAVED.contains(r)).collect();
2396        // Leave at least one register for residents when the pool can spare it;
2397        // reserve TWO only when ≥ 2 caller-saved remain beyond that margin (so
2398        // the loop's index/counter still gets a register). With a tiny pool a
2399        // single reservation (a pure array-copy loop with no other live int) is
2400        // still sound — the reserved register is the cache's alone.
2401        let want = if reservable.len() >= 3 { 2 } else { usize::from(!reservable.is_empty()) };
2402        let mut reserved = Vec::new();
2403        for _ in 0..want {
2404            if let Some(r) = reservable.pop() {
2405                reserved.push(r);
2406            }
2407        }
2408        // Remove the reserved registers from the resident pool.
2409        pool.retain(|r| !reserved.contains(r));
2410        (reserved.first().copied(), reserved.get(1).copied())
2411    } else {
2412        (None, None)
2413    };
2414
2415    // LOOP-INVARIANT PTR/LEN HOIST register reservation (Wave 27a). Reserve
2416    // CALLEE-SAVED registers for each invariant-array plan and load the handle(s)
2417    // ONCE at the loop pre-header (`g.load(reg, slot)` copies whatever the slot's
2418    // current home holds — a register or a frame cell — so the pre-header captures
2419    // the value the OUTER context just set). Callee-saved registers SURVIVE the
2420    // SysV calls (the `ArrPush`/`ListClear` helpers, a self-call) inside the
2421    // loop, so a handle that the linear scan would otherwise put in a CALLER-saved
2422    // register — spilled + reloaded around EVERY in-loop call — instead stays live
2423    // for a one-time prologue push/pop; and a handle that would land in the FRAME
2424    // (forced by `force_frame_set`, or out of registers under pressure) avoids its
2425    // per-access reload. The reserved registers are removed from the resident pool
2426    // (so the scan never assigns them) and added to `used_callee` (prologue
2427    // save/restore), always leaving at least one callee-saved register for the
2428    // scan's own call-surviving residents.
2429    let mut used_callee: Vec<Reg> = Vec::new();
2430    let mut hoists: Vec<HoistEntry> = Vec::new();
2431    if ptr_hoist_enabled() {
2432        // The callee-saved registers still in the pool, reservable for hoisting.
2433        // We take from the FRONT (callee-saved lead the pool in a call-heavy
2434        // region) but always leave AT LEAST ONE callee-saved register for the
2435        // scan's call-surviving residents — a loop-carried scalar live across the
2436        // push wants a callee-saved home too. With fewer than two callee-saved
2437        // available, no hoist is reserved (the trade would starve the loop).
2438        let mut reservable_callee: Vec<Reg> =
2439            pool.iter().copied().filter(|r| CALLEE_SAVED.contains(r)).collect();
2440        let mut reserved_for_hoist: Vec<Reg> = Vec::new();
2441        let plans = loop_invariant_array_hoists(ops);
2442        for plan in plans {
2443            // Hoist only when it amortizes: either the array is accessed at least
2444            // twice in the span (each access otherwise reloads/uses the handle), or
2445            // the span contains a SysV call (a register-resident handle is then
2446            // spilled+reloaded around every call — a callee-saved hoist survives
2447            // it). A single un-called access has nothing to amortize → skip.
2448            let span_calls = ops[plan.head..=plan.back].iter().any(is_sysv_call);
2449            if plan.accesses < 2 && !span_calls {
2450                continue;
2451            }
2452            let need = 1 + usize::from(plan.needs_len);
2453            // Reserve from the available callee-saved, leaving one for the scan.
2454            if reservable_callee.len().saturating_sub(need) < 1 {
2455                continue;
2456            }
2457            let ptr_reg = reservable_callee.remove(0);
2458            reserved_for_hoist.push(ptr_reg);
2459            let len_reg = if plan.needs_len {
2460                let r = reservable_callee.remove(0);
2461                reserved_for_hoist.push(r);
2462                Some(r)
2463            } else {
2464                None
2465            };
2466            hoists.push(HoistEntry { plan, ptr_reg, len_reg });
2467        }
2468        // Remove the reserved registers from the resident pool and record them for
2469        // the prologue/epilogue callee-saved save (deterministic order).
2470        pool.retain(|r| !reserved_for_hoist.contains(r));
2471        for r in reserved_for_hoist {
2472            used_callee.push(r);
2473        }
2474    }
2475
2476    let mut loc = vec![Loc::Frame; max_slot + 1];
2477    {
2478        let mut pi = 0usize; // GP pool cursor
2479        let mut fi = 0usize; // XMM pool cursor
2480        for (s, _) in &order {
2481            // Force callee-window staging slots to the frame (the callee reads them
2482            // from its frame; a register-resident window slot would never reach it).
2483            if let Some(thresh) = force_frame_above {
2484                if *s >= thresh {
2485                    continue;
2486                }
2487            }
2488            // Force list-mutation handle slots (vec/ptr/len) to the frame — the
2489            // helper reads/writes them there, so a register copy would go stale.
2490            if force_frame_set.contains(s) {
2491                continue;
2492            }
2493            match class[*s as usize] {
2494                Class::Int => {
2495                    if pi >= pool.len() {
2496                        continue;
2497                    }
2498                    let r = pool[pi];
2499                    pi += 1;
2500                    loc[*s as usize] = Loc::Reg(r);
2501                    if CALLEE_SAVED.contains(&r) {
2502                        used_callee.push(r);
2503                    }
2504                }
2505                Class::Float => {
2506                    if fi >= FLOAT_REGS.len() {
2507                        continue;
2508                    }
2509                    loc[*s as usize] = Loc::Xmm(FLOAT_REGS[fi]);
2510                    fi += 1;
2511                }
2512                Class::Mixed => {} // frame-resident only.
2513            }
2514        }
2515    }
2516
2517    // LS1 — linear-scan register REUSE for FLOAT slots only. The integer side
2518    // keeps the classic per-slot assignment above plus its GP-only optimizations
2519    // (scaled-index CSE, ptr/const hoists), which the XMM file never touches — so
2520    // the two compose. This OVERRIDES the float locs with a live-range allocation
2521    // that reuses an XMM register across disjoint float temporaries, reclaiming
2522    // the frame spills the whole-region model leaves under XMM pressure (nbody's
2523    // 47 floats / 14 registers / 33 spilled — but only ~10 live at once).
2524    if linscan_active {
2525        let gp_all: Vec<Reg> = CALLER_SAVED.iter().chain(CALLEE_SAVED.iter()).copied().collect();
2526        let scan = linscan_assign(ops, max_slot, &class, &gp_all, &FLOAT_REGS);
2527        for s in 0..=max_slot {
2528            if class[s] == Class::Float {
2529                let forced = force_frame_set.contains(&(s as u16))
2530                    || force_frame_above.is_some_and(|t| s as u16 >= t);
2531                loc[s] = if forced { Loc::Frame } else { scan[s] };
2532            }
2533        }
2534    }
2535
2536    // LS1 per-point schedule: each slot's loop-aware [start,end] and whether its
2537    // register is SHARED with another slot. A shared-register slot is WRITTEN
2538    // THROUGH to its frame home on each write and set `Frame` at its range end —
2539    // because at any region exit (or mid-region deopt) the physical register may
2540    // hold a DIFFERENT member of the share-group, so the slot's authoritative
2541    // value must live in the frame, not be read back from the register by the
2542    // flush. A slot in a register all to itself (the common case) needs neither:
2543    // the register always holds exactly its value, so the exit flush is correct.
2544    // Empty unless `linscan_active`.
2545    let (ls_range, ls_shared): (Vec<Option<(usize, usize)>>, Vec<bool>) = if linscan_active {
2546        let ranges = live_ranges(ops, max_slot);
2547        let mut shared = vec![false; max_slot + 1];
2548        for a in 0..=max_slot {
2549            if matches!(loc[a], Loc::Frame) || ranges[a].is_none() {
2550                continue;
2551            }
2552            for b in 0..=max_slot {
2553                if a != b && loc[b] == loc[a] && ranges[b].is_some() {
2554                    shared[a] = true;
2555                    break;
2556                }
2557            }
2558        }
2559        (ranges, shared)
2560    } else {
2561        (Vec::new(), Vec::new())
2562    };
2563
2564    if linscan_active && std::env::var_os("LOGOS_DUMP_LS").is_some() {
2565        eprintln!("LS-REGION ops={} max_slot={}", ops.len(), max_slot);
2566        for (i, op) in ops.iter().enumerate() {
2567            eprintln!("  op{i:>3}: {op:?}");
2568        }
2569        for s in 0..=max_slot {
2570            if let Some((a, b)) = ls_range[s] {
2571                eprintln!("  slot{s:>3} [{a:>3},{b:>3}] {:?} shared={}", loc[s], ls_shared[s]);
2572            }
2573        }
2574    }
2575
2576    // Diagnostic class+placement histogram (LOGOS_DUMP_CLASS=1). Only for regions
2577    // that touch a float, so the float-cluster investigation isn't drowned in
2578    // scalar regions. Reports how many Float-class slots actually won an XMM
2579    // register vs spilled to the frame under pressure, and which slots are Mixed
2580    // (frame-resident class-ambiguous) — the empirical signal that decides whether
2581    // the float wall is a Mixed-class problem (K1) or an XMM-pressure problem (K2).
2582    if dump_class_enabled() {
2583        let (mut n_int, mut n_float, mut n_mixed) = (0usize, 0usize, 0usize);
2584        let (mut f_xmm, mut f_frame) = (0usize, 0usize);
2585        let mut mixed_slots: Vec<u16> = Vec::new();
2586        for s in 0..=max_slot {
2587            match class[s] {
2588                Class::Int => n_int += 1,
2589                Class::Float => {
2590                    n_float += 1;
2591                    match loc[s] {
2592                        Loc::Xmm(_) => f_xmm += 1,
2593                        _ => f_frame += 1,
2594                    }
2595                }
2596                Class::Mixed => {
2597                    n_mixed += 1;
2598                    mixed_slots.push(s as u16);
2599                }
2600            }
2601        }
2602        // Max SIMULTANEOUSLY-LIVE float/int slots = the true register pressure in
2603        // the live-interval sense. If max-live-float <= 14 (the XMM pool), a
2604        // linear-scan allocator that reuses a register once a slot's range ends
2605        // fits every float with ZERO spill — even though the whole-region per-slot
2606        // model spills `frame` of them. This is the number that decides whether
2607        // interval allocation is the lever.
2608        let la = liveness_after(ops, max_slot);
2609        let (mut max_live_f, mut max_live_i) = (0usize, 0usize);
2610        for row in &la {
2611            let (mut lf, mut li) = (0usize, 0usize);
2612            for s in 0..=max_slot {
2613                if s < row.len() && row[s] {
2614                    match class[s] {
2615                        Class::Float => lf += 1,
2616                        Class::Int => li += 1,
2617                        Class::Mixed => {}
2618                    }
2619                }
2620            }
2621            max_live_f = max_live_f.max(lf);
2622            max_live_i = max_live_i.max(li);
2623        }
2624        // Linear-scan SIMULATION: how many float/int slots a simple interval
2625        // allocator (the lever we'd build) would spill, given the same XMM pool
2626        // (14) and the actual GP `pool` this region has after CSE/hoist
2627        // reservations. Compare `lsF`/`lsI` against `frame=`: the gap is the spill
2628        // the whole-region per-slot model leaves on the table.
2629        let intervals = live_intervals(ops, max_slot);
2630        let ls_float = linscan_spills(&intervals, &class, Class::Float, FLOAT_REGS.len());
2631        let ls_int = linscan_spills(&intervals, &class, Class::Int, pool.len());
2632        if n_float > 0 || n_mixed > 0 {
2633            eprintln!(
2634                "DUMP-CLASS region ops={} max_slot={} int={} float={} (xmm={} frame={}) mixed={} maxLiveF={} maxLiveI={} lsF={} lsI={} mixed_slots={:?}",
2635                ops.len(), max_slot, n_int, n_float, f_xmm, f_frame, n_mixed, max_live_f, max_live_i, ls_float, ls_int, mixed_slots
2636            );
2637        }
2638    }
2639
2640    // The caller-saved residents (GP + XMM) that a SysV call CLOBBERS — a
2641    // self-call OR a list-mutation helper call both trash the caller-saved
2642    // registers per the ABI. This is the UNIVERSE; the per-call-site lists below
2643    // intersect it with each call's live-after set. Callee-saved residents
2644    // (rbx/r12/r13/r14, BASE=r15) survive a call automatically and need no spill.
2645    let (gp_volatile, xmm_volatile): (Vec<Slot>, Vec<Slot>) = if has_call {
2646        let mut gp = Vec::new();
2647        let mut xmm = Vec::new();
2648        for s in 0..=max_slot as u16 {
2649            match loc[s as usize] {
2650                Loc::Reg(r) if CALLER_SAVED.contains(&r) => gp.push(s),
2651                Loc::Xmm(_) => xmm.push(s),
2652                _ => {}
2653            }
2654        }
2655        (gp, xmm)
2656    } else {
2657        (Vec::new(), Vec::new())
2658    };
2659
2660    // Slots that are resident AND written somewhere must be flushed to the
2661    // frame on exit. (A resident read-only slot was loaded from the frame at
2662    // entry, never changed, so the frame is already correct.)
2663    let mut written: Vec<bool> = vec![false; max_slot + 1];
2664    for op in ops {
2665        if let Some(d) = dest_of(op) {
2666            written[d as usize] = true;
2667        }
2668    }
2669
2670    // PER-CALL-SITE spill liveness. A caller-saved resident is spilled+reloaded
2671    // around a call iff its value must survive the call's register clobber. That
2672    // is true exactly when the slot is either:
2673    //   - WRITTEN somewhere (so it is flushed to the frame at EVERY region exit —
2674    //     `Return` and every deopt side-exit — which the broader VM may read; its
2675    //     last-written value MUST be preserved through the clobbering call), OR
2676    //   - LIVE AFTER this call (read by some later op in the region).
2677    // A caller-saved resident that is READ-ONLY (never written) AND dead after
2678    // the call is the ONLY case we elide: its frame cell already holds the value
2679    // loaded at entry (it never changed), it is read by no later op, and being
2680    // unwritten it is never flushed at exit — so eliding its spill/reload is
2681    // bit-identical to the full-frame stencil tier (verified by the
2682    // `*_matches_stencil` post-frame differentials). This removes the redundant
2683    // spill/reload of cold read-only operands (e.g. a constant or bound consulted
2684    // only before the call) from call-heavy loops. For a non-call op the lists
2685    // are empty. When `has_call` is false the liveness pass is skipped entirely.
2686    let (call_gp, call_xmm): (Vec<Vec<Slot>>, Vec<Vec<Slot>>) = if let Some(live_after) =
2687        live_after.as_deref()
2688    {
2689        let keep = |s: Slot, la: &[bool]| -> bool {
2690            written.get(s as usize).copied().unwrap_or(true)
2691                || la.get(s as usize).copied().unwrap_or(true)
2692        };
2693        let mut cgp: Vec<Vec<Slot>> = vec![Vec::new(); ops.len()];
2694        let mut cxmm: Vec<Vec<Slot>> = vec![Vec::new(); ops.len()];
2695        for idx in 0..ops.len() {
2696            let la = &live_after[idx];
2697            cgp[idx] = gp_volatile.iter().copied().filter(|&s| keep(s, la)).collect();
2698            cxmm[idx] = xmm_volatile.iter().copied().filter(|&s| keep(s, la)).collect();
2699        }
2700        (cgp, cxmm)
2701    } else {
2702        (Vec::new(), Vec::new())
2703    };
2704
2705    // One label per op (jump targets) plus the deopt epilogue label and the
2706    // self-call deopt-PROPAGATION (bare flush+return, no status write) label.
2707    let mut asm = Asm::new();
2708    let op_labels: Vec<LabelId> = (0..ops.len()).map(|_| asm.new_label()).collect();
2709    // The plain (classic) deopt epilogue is still needed for any precise op whose
2710    // code is the plain marker `1` (a deoptable op the adapter chose not to make
2711    // precise) and for every classic checked op.
2712    let deopt_label = if needs_status { Some(asm.new_label()) } else { None };
2713    // A PROPAGATION epilogue (bare flush+return) is needed when ANY real SysV
2714    // self-call is present — the classic self-call OR the mode-B precise call —
2715    // because the inner exit code (already in the status cell) must not be
2716    // overwritten on the way out.
2717    let propagate_label = (has_self_call || has_precise_call).then(|| asm.new_label());
2718
2719    // PRECISE (mode-B) deopt machinery: a shared precise epilogue plus one
2720    // out-of-line tag-staging block per DISTINCT non-plain code (deterministic,
2721    // first-use order). Each block stages its tag in `S1` and jumps to the
2722    // epilogue, which reads the live depth, ORs it into the high 32 bits, writes
2723    // the status cell, flushes every resident-written slot, and returns. This is
2724    // the contiguous-codegen analog of the stencil tier's `ST_DEOPT_AT` family.
2725    let mut code_blocks: std::collections::HashMap<i64, LabelId> = std::collections::HashMap::new();
2726    let precise_epilogue = if precise.is_some() { Some(asm.new_label()) } else { None };
2727    if let Some(codes) = precise {
2728        for &c in codes {
2729            if c != 1 {
2730                code_blocks.entry(c).or_insert_with(|| asm.new_label());
2731            }
2732        }
2733    }
2734
2735    // Stack-pointer alignment: at function entry RSP ≡ 8 (mod 16) (the caller's
2736    // `call` pushed the return address). After `push BASE` + K callee-saved
2737    // pushes, RSP ≡ 8 + 8*(1+K) (mod 16). A SysV `call` requires RSP ≡ 0 (mod
2738    // 16) at the call instruction. Pad with one 8-byte slot when (1+K) is even
2739    // so RSP is 16-aligned at every call (self-call OR list-mutation helper);
2740    // undo it in every epilogue. (No calls ⇒ no padding needed, none emitted.)
2741    let pushes = 1 + used_callee.len();
2742    let stack_pad: i32 = if has_call && pushes % 2 == 0 { 8 } else { 0 };
2743
2744    // Prologue: save callee-saved regs, move base (rdi) into r15, apply the
2745    // alignment pad, then load every resident slot from the frame.
2746    asm.push(BASE);
2747    for &r in &used_callee {
2748        asm.push(r);
2749    }
2750    asm.mov_rr(BASE, Reg::Rdi);
2751    if stack_pad != 0 {
2752        asm.sub_ri(Reg::Rsp, stack_pad);
2753    }
2754    {
2755        // Load resident slots in slot order for determinism. GP-resident slots
2756        // load via `mov`; XMM-resident (float) slots via `movsd`.
2757        for s in 0..=max_slot as u16 {
2758            // LS1: under linear scan, load a resident slot at entry iff it is
2759            // ENTRY-LIVE (live range starts at op 0 — an input/loop-carried value
2760            // read before written) OR it has a register to ITSELF (non-shared, so
2761            // the load can't clobber a co-tenant; loading a write-first temp is
2762            // harmless — its def overwrites it, and the entry value preserves a
2763            // conditionally-written slot on the paths it isn't written). Skip only
2764            // a SHARED write-first temp: loading it would clobber the entry-live or
2765            // earlier member sharing its one register.
2766            if linscan_active {
2767                let shared = ls_shared.get(s as usize).copied().unwrap_or(false);
2768                let entry_live = matches!(ls_range.get(s as usize).copied().flatten(), Some((0, _)));
2769                if shared && !entry_live {
2770                    continue;
2771                }
2772            }
2773            match loc.get(s as usize) {
2774                Some(Loc::Reg(r)) => asm.mov_rm(*r, BASE, (s as i32) * 8),
2775                Some(Loc::Xmm(x)) => asm.movsd_rm(*x, BASE, (s as i32) * 8),
2776                _ => {}
2777            }
2778        }
2779    }
2780
2781    let status_addr = status
2782        .as_ref()
2783        .map(|c| c.as_ref() as *const AtomicI64 as i64)
2784        .unwrap_or(0);
2785
2786    // The self-call ENTRY CELL: an `Arc<AtomicI64>` whose address is baked into
2787    // every call site and which is written with `chain.base()` after mapping.
2788    // Needed for BOTH the classic direct self-call and the mode-B precise call
2789    // (both are REAL SysV calls through this very chain's entry).
2790    let entry_cell: Option<std::sync::Arc<AtomicI64>> =
2791        (has_self_call || has_precise_call).then(|| std::sync::Arc::new(AtomicI64::new(0)));
2792    let entry_addr = entry_cell
2793        .as_ref()
2794        .map(|c| c.as_ref() as *const AtomicI64 as i64)
2795        .unwrap_or(0);
2796
2797    let self_call = propagate_label.map(|pl| SelfCall { propagate_label: pl });
2798    let precise_ctx = precise.map(|codes| Precise { codes, code_blocks: &code_blocks });
2799
2800    let mut g = Gen {
2801        asm,
2802        loc: loc.clone(),
2803        op_labels: &op_labels,
2804        deopt_label,
2805        stack_pad,
2806        self_call,
2807        call_gp: &call_gp,
2808        call_xmm: &call_xmm,
2809        precise: precise_ctx,
2810        off_im1_reg,
2811        off_scaled_reg,
2812        off_cache: None,
2813        hoists: &hoists,
2814        active_hoists: Vec::new(),
2815    };
2816
2817    // Jump-target indices: an op reached by some transfer's `target` is a control
2818    // join, so the scaled-index cache must be DROPPED on entry to it (a stale
2819    // reserved register from the fall-through predecessor must not be reused when
2820    // control arrives from the branch). Computed once; consulted in the loop.
2821    let mut is_jump_target = vec![false; ops.len()];
2822    for op in ops {
2823        if let Some(t) = op_target(op) {
2824            if let Some(slot) = is_jump_target.get_mut(t) {
2825                *slot = true;
2826            }
2827        }
2828    }
2829
2830    // LOOP-INVARIANT CONSTANT HOIST (Wave 28). A `LoadConst` whose `dst` is the
2831    // span's sole writer and lands in a GP REGISTER is materialized ONCE in the
2832    // loop pre-header instead of every iteration. `const_hoist_by_op[i] =
2833    // Some(head)` marks the op `i` to (a) SKIP in the body and (b) materialize at
2834    // `head`'s pre-header.
2835    //
2836    // FLOAT (XMM-resident) consts are EXCLUDED, and the exclusion is empirical,
2837    // not incidental. mandelbrot's hot inner loop re-materializes `2.0`/`4.0`
2838    // every iteration through a GP→XMM bridge; hoisting them into resident XMM
2839    // registers held across the loop is bit-identical but measured ~8% SLOWER (n
2840    // =2000: 480ms vs 443ms, reproducible across independent processes; GP-only
2841    // hoisting is at parity). The XMM register file is SATURATED (14/14 resident
2842    // in mandelbrot), and holding an invariant float in an XMM register that the
2843    // float dependency chain repeatedly reads serializes worse than the
2844    // per-iteration re-materialization, whose independent `movabs;movq` the
2845    // out-of-order engine overlaps with the float chain — the copy-and-patch /
2846    // contiguous backend is throughput-bound here, so removing the "redundant"
2847    // float loads loses free ILP. Frame-resident dsts are also filtered (no
2848    // register to keep resident — the body would still touch the frame). The
2849    // structural detection is purely over the op stream; residency is decided
2850    // here against `loc`.
2851    let mut const_hoist_by_op: Vec<Option<usize>> = vec![None; ops.len()];
2852    let mut const_hoist_at_head: Vec<Vec<usize>> = vec![Vec::new(); ops.len()];
2853    if const_hoist_enabled() {
2854        for h in loop_invariant_const_hoists(ops) {
2855            if matches!(loc[h.dst as usize], Loc::Reg(_)) {
2856                const_hoist_by_op[h.op] = Some(h.head);
2857                const_hoist_at_head[h.head].push(h.op);
2858            }
2859        }
2860    }
2861
2862    for (idx, op) in ops.iter().enumerate() {
2863        // LOOP PRE-HEADER for any hoist whose loop head is THIS op: load the
2864        // invariant ptr (and len) into the reserved callee-saved register(s) ONCE,
2865        // BEFORE binding the head's label. The loop's back-edge targets the bound
2866        // label, so it SKIPS this load (the register stays live across iterations);
2867        // only the entry fall-through executes it. The entry-discipline guard in
2868        // `loop_invariant_array_hoists` ensures the head is entered ONLY via this
2869        // fall-through, so the register is always loaded before the first access.
2870        for k in 0..g.hoists.len() {
2871            let e = g.hoists[k];
2872            if e.plan.head == idx {
2873                g.load(e.ptr_reg, e.plan.ptr_slot);
2874                if let Some(lr) = e.len_reg {
2875                    g.load(lr, e.plan.len_slot);
2876                }
2877            }
2878        }
2879        // LOOP PRE-HEADER for any loop-invariant CONSTANT whose head is THIS op:
2880        // materialize the raw bits ONCE into the dst register, BEFORE binding the
2881        // head's label, so the back-edge (which jumps to the bound label) skips it
2882        // and the register stays resident across iterations. Mirrors the body
2883        // `LoadConst` emit exactly (a float dst bridges GP→XMM via `movq`), so the
2884        // register holds the same bits the per-iteration load would have.
2885        for ci in 0..const_hoist_at_head[idx].len() {
2886            let op_i = const_hoist_at_head[idx][ci];
2887            if let MicroOp::LoadConst { dst, value } = ops[op_i] {
2888                g.asm.mov_ri(S0, value);
2889                if let Loc::Xmm(_) = g.loc[dst as usize] {
2890                    g.asm.movq_xr(FS0, S0);
2891                    g.fstore(dst, FS0);
2892                } else {
2893                    g.store(dst, S0);
2894                }
2895            }
2896        }
2897        g.asm.bind(op_labels[idx]);
2898        // The hoists ACTIVE at this op: those whose span `[head, back]` contains
2899        // `idx`. Inside the span `emit_arr_addr` reads the hoisted register;
2900        // outside it falls back to the per-access frame reload (the register may
2901        // hold a stale value from a different loop or a since-rewritten slot).
2902        g.active_hoists.clear();
2903        for k in 0..g.hoists.len() {
2904            let p = g.hoists[k].plan;
2905            if (p.head..=p.back).contains(&idx) {
2906                g.active_hoists.push(k);
2907            }
2908        }
2909        // Drop the scaled-index cache at a control join: control could reach this
2910        // op from a branch where the reserved register holds a different (or no)
2911        // index, so the next array access here must recompute.
2912        if is_jump_target[idx] {
2913            g.invalidate_off_cache();
2914        }
2915        // A hoisted loop-invariant `LoadConst`: the pre-header already
2916        // materialized its register, which stays resident across the back-edge —
2917        // SKIP the per-iteration re-materialization (the body label is still bound
2918        // above so any transfer to it lands here correctly). The post-match
2919        // off-cache invalidation below is still honored: a `LoadConst` is never a
2920        // SysV call, and `invalidate_off_cache_if_writes_idx` recomputes a cache
2921        // keyed off this slot — harmless here (the invariant value is unchanged).
2922        let hoisted_const = const_hoist_by_op[idx].is_some();
2923        if !hoisted_const {
2924            match op {
2925            MicroOp::CallSelf { dst, args_start, limit_slot, frame_size, .. } => {
2926                emit_self_call(
2927                    &mut g, idx, *dst, *args_start, None, *limit_slot, *frame_size, status_addr,
2928                    depth_addr, entry_addr,
2929                );
2930            }
2931            MicroOp::CallSelfCopy {
2932                dst, args_start, src_start, arg_count, limit_slot, frame_size, ..
2933            } => {
2934                emit_self_call(
2935                    &mut g, idx, *dst, *args_start, Some((*src_start, *arg_count)), *limit_slot,
2936                    *frame_size, status_addr, depth_addr, entry_addr,
2937                );
2938            }
2939            // The mode-B precise self-`Call`: the disjoint callee window is
2940            // placed at `args_start == frame_size` (the adapter's invariant), so
2941            // the callee's frame size — for the arena bound and the limit plant —
2942            // is exactly `args_start`. The per-op code (precise) drives the
2943            // guard exits.
2944            MicroOp::Call { dst, args_start, limit_slot, .. } => {
2945                let code = g
2946                    .precise
2947                    .as_ref()
2948                    .map(|p| p.codes[idx])
2949                    .expect("a precise Call requires precise codes");
2950                emit_precise_call(
2951                    &mut g, idx, *dst, *args_start, *limit_slot, *args_start as i64, status_addr,
2952                    depth_addr, entry_addr, code,
2953                );
2954            }
2955            _ => emit_op(&mut g, op, idx, &written, &used_callee)?,
2956            }
2957        }
2958        // A SysV call (self-call, precise call, or a list/string-mutation helper)
2959        // clobbers the caller-saved reserved register(s) during the call, so the
2960        // cache cannot survive it. A write to the cached index slot makes the held
2961        // `im1` stale. Either drops the cache so a later access recomputes.
2962        if is_sysv_call(op) {
2963            g.invalidate_off_cache();
2964        } else {
2965            g.invalidate_off_cache_if_writes_idx(op);
2966        }
2967        // LS1 SPILL-AT-END: when a SHARED-register slot's range ends, store it to
2968        // its frame home and set it `Frame`, freeing the register for the next
2969        // member. Only the Completed exit flush reads these cells (a classic Deopt
2970        // DISCARDS the private frame and replays from the VM's own registers —
2971        // machine.rs — so no per-write write-through is needed). A slot in a
2972        // register all to itself needs nothing: the register always holds exactly
2973        // its value, so the exit flush reads it correctly. The reuse start is
2974        // strictly after this end (`linscan_assign`), so the register still holds
2975        // THIS slot now — never a successor's value.
2976        if linscan_active {
2977            for s in 0..=max_slot {
2978                if ls_shared[s] && matches!(ls_range[s], Some((_, e)) if e == idx) {
2979                    match g.loc[s] {
2980                        Loc::Reg(r) => g.asm.mov_mr(BASE, (s as i32) * 8, r),
2981                        Loc::Xmm(x) => g.asm.movsd_mr(BASE, (s as i32) * 8, x),
2982                        Loc::Frame => {}
2983                    }
2984                    g.loc[s] = Loc::Frame;
2985                }
2986            }
2987        }
2988    }
2989
2990    // Deopt epilogue (only if some op can side-exit): store 1 to the status
2991    // cell, flush resident-written slots, restore callee-saved regs, return 0.
2992    if let Some(dl) = deopt_label {
2993        g.asm.bind(dl);
2994        // mov rax, status_addr; mov qword [rax], 1
2995        g.asm.mov_ri(S0, status_addr);
2996        g.asm.mov_ri(S1, 1);
2997        g.asm.mov_mr(S0, 0, S1);
2998        emit_flush_and_return(&mut g, &written, &used_callee, None);
2999    }
3000
3001    // Self-call deopt-PROPAGATION epilogue: the status cell ALREADY holds the
3002    // inner exit code (5/9/precise/1), so flush and return WITHOUT touching it.
3003    if let Some(pl) = propagate_label {
3004        g.asm.bind(pl);
3005        emit_flush_and_return(&mut g, &written, &used_callee, None);
3006    }
3007
3008    // PRECISE (mode-B) deopt epilogue + per-code tag-staging blocks. Each block
3009    // stages its tag in S1, then falls through (or jumps) to the shared epilogue,
3010    // which combines the live depth into the high 32 bits, writes the status
3011    // cell, flushes every resident-written slot, and returns — bit-identical to
3012    // `logos_stencil_deopt_at` (`status = (pc<<2 | 3) | (depth << 32)`).
3013    if let Some(ep) = precise_epilogue {
3014        // The blocks (deterministic order by code value for a stable layout),
3015        // each `mov S1, code; jmp epilogue`.
3016        let mut codes_sorted: Vec<(i64, LabelId)> =
3017            code_blocks.iter().map(|(&c, &l)| (c, l)).collect();
3018        codes_sorted.sort_by_key(|(c, _)| *c);
3019        for (c, lbl) in codes_sorted {
3020            g.asm.bind(lbl);
3021            g.asm.mov_ri(S1, c);
3022            g.asm.jmp(ep);
3023        }
3024        // The shared epilogue: S1 = tag; rax = *depth; rax <<= 32; rax |= S1;
3025        // *status = rax; flush; return.
3026        g.asm.bind(ep);
3027        g.asm.mov_ri(S0, depth_addr);
3028        g.asm.mov_rm(S0, S0, 0); // S0 = *depth
3029        g.asm.shl_ri(S0, 32);
3030        g.asm.or_rr(S0, S1); // S0 = (depth << 32) | tag
3031        g.asm.mov_ri(SC, status_addr);
3032        g.asm.mov_mr(SC, 0, S0); // *status = S0
3033        emit_flush_and_return(&mut g, &written, &used_callee, None);
3034    }
3035
3036    let code = g.asm.resolve();
3037    let chain = JitChain::from_code(&code, ops.len()).ok()?;
3038
3039    // Patch the entry cell with the mapped base, so every self-call reaches this
3040    // very function. An unpatched (0) cell would deopt (matching the stencil's
3041    // unpatched-entry guard), but we patch it here, before the chain ever runs.
3042    if let Some(cell) = entry_cell {
3043        cell.store(chain.base() as i64, std::sync::atomic::Ordering::SeqCst);
3044        return Some(CompiledChain::from_chain_keepalive(chain, status, vec![cell]));
3045    }
3046    Some(CompiledChain::from_chain(chain, status))
3047}
3048
3049/// The destination slot an op writes, if any.
3050fn dest_of(op: &MicroOp) -> Option<Slot> {
3051    match *op {
3052        MicroOp::Move { dst, .. }
3053        | MicroOp::LoadConst { dst, .. }
3054        | MicroOp::Add { dst, .. }
3055        | MicroOp::Sub { dst, .. }
3056        | MicroOp::Mul { dst, .. }
3057        | MicroOp::Lt { dst, .. }
3058        | MicroOp::Gt { dst, .. }
3059        | MicroOp::LtEq { dst, .. }
3060        | MicroOp::GtEq { dst, .. }
3061        | MicroOp::Eq { dst, .. }
3062        | MicroOp::Neq { dst, .. }
3063        | MicroOp::BitAnd { dst, .. }
3064        | MicroOp::BitOr { dst, .. }
3065        | MicroOp::BitXor { dst, .. }
3066        | MicroOp::Shl { dst, .. }
3067        | MicroOp::Shr { dst, .. }
3068        | MicroOp::NotInt { dst, .. }
3069        | MicroOp::NotBool { dst, .. }
3070        | MicroOp::Div { dst, .. }
3071        | MicroOp::Mod { dst, .. }
3072        | MicroOp::ArrLoad { dst, .. }
3073        | MicroOp::DivPow2 { dst, .. }
3074        | MicroOp::MagicDivU { dst, .. }
3075        | MicroOp::AddF { dst, .. }
3076        | MicroOp::SubF { dst, .. }
3077        | MicroOp::MulF { dst, .. }
3078        | MicroOp::DivF { dst, .. }
3079        | MicroOp::SqrtF { dst, .. }
3080        | MicroOp::IntToFloat { dst, .. }
3081        | MicroOp::FmaF { dst, .. }
3082        | MicroOp::LtF { dst, .. }
3083        | MicroOp::GtF { dst, .. }
3084        | MicroOp::LtEqF { dst, .. }
3085        | MicroOp::GtEqF { dst, .. }
3086        | MicroOp::EqF { dst, .. }
3087        | MicroOp::NeqF { dst, .. }
3088        // A self-call writes its result into `dst` (after a successful return).
3089        | MicroOp::CallSelf { dst, .. }
3090        | MicroOp::CallSelfCopy { dst, .. }
3091        // The mode-B precise self-call also stores its result handle into `dst`.
3092        | MicroOp::Call { dst, .. } => Some(dst),
3093        // `ArrStore` writes the BUFFER (through the pinned pointer), not a frame
3094        // slot, so it has no `dest_of` — its effect is outside the frame.
3095        _ => None,
3096    }
3097}
3098
3099/// Flush every resident-written slot back to the frame, restore callee-saved
3100/// registers, and `ret`. When `ret_slot` is `Some`, the return value (slot's
3101/// value) is loaded into `rax` AFTER flushing; when `None` (deopt), `rax` is
3102/// already the status-write scratch and the returned value is irrelevant (the
3103/// status cell signals the side-exit).
3104fn emit_flush_and_return(
3105    g: &mut Gen,
3106    written: &[bool],
3107    used_callee: &[Reg],
3108    ret_slot: Option<Slot>,
3109) {
3110    // Flush resident-written slots in slot order. GP slots flush via `mov`,
3111    // XMM (float) slots via `movsd` — both leave the frame consistent with the
3112    // tree-walker's full frame state (so the region can resume / deopt).
3113    for s in 0..g.loc.len() {
3114        if written[s] {
3115            match g.loc[s] {
3116                Loc::Reg(r) => g.asm.mov_mr(BASE, (s as i32) * 8, r),
3117                Loc::Xmm(x) => g.asm.movsd_mr(BASE, (s as i32) * 8, x),
3118                Loc::Frame => {}
3119            }
3120        }
3121    }
3122    if let Some(rs) = ret_slot {
3123        // Load the return value bits into rax from its (now frame-consistent)
3124        // home. A float return slot lives in an XMM reg — bit-copy via `movq`;
3125        // the chain ABI returns the raw i64 bits either way.
3126        match g.loc[rs as usize] {
3127            Loc::Reg(r) => g.asm.mov_rr(S0, r),
3128            Loc::Xmm(x) => g.asm.movq_rx(S0, x),
3129            Loc::Frame => g.asm.mov_rm(S0, BASE, (rs as i32) * 8),
3130        }
3131    }
3132    // Undo the prologue's stack-alignment pad (if any) before restoring regs.
3133    if g.stack_pad != 0 {
3134        g.asm.add_ri(Reg::Rsp, g.stack_pad);
3135    }
3136    // Restore callee-saved regs in reverse push order, then base.
3137    for &r in used_callee.iter().rev() {
3138        g.asm.pop(r);
3139    }
3140    g.asm.pop(BASE);
3141    g.asm.ret();
3142}
3143
3144/// Lower a DIRECT self-call (`CallSelf` / `CallSelfCopy`) — a REAL SysV call to
3145/// THIS chain's own entry. Bit-identical to `logos_stencil_call_self`(`_copy`):
3146///
3147/// 1. entry-cell load + zero guard (status = 1, the unpatched-entry marker);
3148/// 2. live-depth guard `depth >= MAX` (status = 5);
3149/// 3. arena-bound guard `callee_base + frame_size*8 > arena_end` (status = 9),
3150///    where `arena_end = frame[limit_slot]` and `callee_base = BASE + args*8`;
3151/// 4. (`CallSelfCopy`) stage `arg_count` scalar args from `src_start..` into the
3152///    callee window `callee_base[0..]` — read from wherever the source slots
3153///    live (register or frame), so a register-resident arg is staged correctly;
3154/// 5. plant `arena_end` into the callee's own limit slot
3155///    (`callee_base[frame_size-1]`);
3156/// 6. `*depth += 1`; spill caller-saved residents; `call [entry]`;
3157/// 7. `*depth -= 1`; if `*status != 0` → PROPAGATE (bare flush+return, the inner
3158///    exit code is already in the cell); else reload residents and store the
3159///    result into `dst`.
3160///
3161/// Every side exit writes its marker (or leaves the propagated code) and jumps
3162/// to the self-call PROPAGATION epilogue, which flushes the frame and returns
3163/// WITHOUT overwriting the status cell.
3164#[allow(clippy::too_many_arguments)]
3165fn emit_self_call(
3166    g: &mut Gen,
3167    idx: usize,
3168    dst: Slot,
3169    args_start: Slot,
3170    copy: Option<(Slot, u16)>,
3171    limit_slot: Slot,
3172    frame_size: i64,
3173    status_addr: i64,
3174    depth_addr: i64,
3175    entry_addr: i64,
3176) {
3177    let propagate = g.self_call.as_ref().expect("self-call without SelfCall context").propagate_label;
3178
3179    // A small helper: on a failed guard, write `marker` to the status cell, then
3180    // jump to the propagation epilogue (which flushes + returns, no further
3181    // status write). Emitted as an out-of-line block reached by a jcc.
3182    let entry_zero = g.asm.new_label();
3183    let depth_bad = g.asm.new_label();
3184    let arena_bad = g.asm.new_label();
3185
3186    // ---- 1. entry-cell load + zero guard ----
3187    // S0 = *entry_cell (the patched base, or 0 if somehow unpatched).
3188    g.asm.mov_ri(S0, entry_addr);
3189    g.asm.mov_rm(S0, S0, 0);
3190    g.asm.test_rr(S0, S0);
3191    g.asm.jcc(Cond::Eq, entry_zero);
3192
3193    // ---- 2. depth guard: *depth >= MAX ----
3194    // S1 = *depth.
3195    g.asm.mov_ri(S1, depth_addr);
3196    g.asm.mov_rm(S1, S1, 0);
3197    g.asm.cmp_ri(S1, SELF_CALL_DEPTH_LIMIT as i32);
3198    g.asm.jcc(Cond::Ge, depth_bad);
3199
3200    // ---- 3. arena-bound guard ----
3201    // SC = callee_base = BASE + args_start*8.
3202    g.asm.mov_rr(SC, BASE);
3203    g.asm.add_ri(SC, (args_start as i32) * 8);
3204    // S0 = arena_end = frame[limit_slot].
3205    g.asm.mov_rm(S0, BASE, (limit_slot as i32) * 8);
3206    // S1 = callee_base + frame_size*8.
3207    g.asm.mov_rr(S1, SC);
3208    g.asm.add_ri(S1, (frame_size as i32) * 8);
3209    g.asm.cmp_rr(S1, S0); // (callee_base + fs*8) - arena_end
3210    g.asm.jcc(Cond::Gt, arena_bad); // > arena_end → OOB
3211
3212    // ---- 4. argument staging (CallSelfCopy) ----
3213    // SC holds callee_base; S0 holds arena_end (kept for step 5). Stage each
3214    // source arg through S1, reading the source slot from wherever it lives.
3215    if let Some((src_start, arg_count)) = copy {
3216        for j in 0..arg_count {
3217            g.load(S1, src_start + j); // S1 = frame-or-reg[src_start+j]
3218            g.asm.mov_mr(SC, (j as i32) * 8, S1); // callee_base[j] = arg
3219        }
3220    }
3221
3222    // ---- 5. plant arena_end into the callee's own limit slot ----
3223    g.asm.mov_mr(SC, ((frame_size - 1) as i32) * 8, S0); // callee_base[fs-1] = arena_end
3224
3225    // ---- 6a. *depth += 1 ----
3226    g.asm.mov_ri(SC, depth_addr);
3227    g.asm.mov_rm(S0, SC, 0);
3228    g.asm.add_ri(S0, 1);
3229    g.asm.mov_mr(SC, 0, S0);
3230
3231    // ---- 6b. spill caller-saved residents to their frame slots ----
3232    g.spill_volatiles_at(idx);
3233
3234    // ---- 6c. set up the SysV call args and CALL ----
3235    // rdi = callee_base = BASE + args_start*8 (recompute — SC was reused above).
3236    g.asm.mov_rr(Reg::Rdi, BASE);
3237    g.asm.add_ri(Reg::Rdi, (args_start as i32) * 8);
3238    // rsi = sp = 0 (the contiguous backend never reads the operand stack).
3239    g.asm.mov_ri(Reg::Rsi, 0);
3240    // rdx/rcx/r8/r9 = 0 (threaded register args enter zeroed; the callee's
3241    // prologue reloads its pinned registers from its frame).
3242    g.asm.mov_ri(Reg::Rdx, 0);
3243    g.asm.mov_ri(Reg::Rcx, 0);
3244    g.asm.mov_ri(Reg::R8, 0);
3245    g.asm.mov_ri(Reg::R9, 0);
3246    // call [entry_cell]: rax = *entry_cell; call rax.
3247    g.asm.mov_ri(S0, entry_addr);
3248    g.asm.mov_rm(S0, S0, 0);
3249    g.asm.call_r(S0); // result -> rax (S0)
3250
3251    // ---- 7a. *depth -= 1 (uses rcx; rax holds the result) ----
3252    g.asm.mov_ri(SC, depth_addr);
3253    g.asm.mov_rm(S1, SC, 0);
3254    g.asm.sub_ri(S1, 1);
3255    g.asm.mov_mr(SC, 0, S1);
3256
3257    // ---- 7b. status check: if *status != 0 propagate (rax irrelevant) ----
3258    g.asm.mov_ri(S1, status_addr);
3259    g.asm.mov_rm(S1, S1, 0);
3260    g.asm.test_rr(S1, S1);
3261    g.asm.jcc(Cond::Ne, propagate);
3262
3263    // ---- 7c. reload caller-saved residents (their pre-call loop-carried
3264    // values, spilled in 6b — the callee never touches the caller's frame
3265    // slots). Reloads write the resident register directly (no scratch), so
3266    // rax (the result) survives. Only the LIVE-AFTER subset is reloaded (a
3267    // resident dead after the call is read by no later op). ----
3268    g.reload_volatiles_at(idx);
3269
3270    // ---- 7d. store the result into dst (AFTER reloading, so a volatile-
3271    // resident dst gets the result, not its stale reloaded value). ----
3272    g.store(dst, S0);
3273
3274    // ---- out-of-line guard-failure blocks (write marker, then propagate) ----
3275    let after = g.asm.new_label();
3276    g.asm.jmp(after);
3277
3278    g.asm.bind(entry_zero);
3279    emit_marker_then_propagate(g, status_addr, 1, propagate);
3280    g.asm.bind(depth_bad);
3281    emit_marker_then_propagate(g, status_addr, 5, propagate);
3282    g.asm.bind(arena_bad);
3283    emit_marker_then_propagate(g, status_addr, 9, propagate);
3284
3285    g.asm.bind(after);
3286}
3287
3288/// Write `marker` to the status cell, then jump to the self-call propagation
3289/// epilogue (bare flush + return). Used by the pre-call self-call guards
3290/// (unpatched entry = 1, depth limit = 5, arena bound = 9) — the same distinct
3291/// markers `logos_stencil_call_self` writes.
3292fn emit_marker_then_propagate(g: &mut Gen, status_addr: i64, marker: i64, propagate: LabelId) {
3293    g.asm.mov_ri(S0, status_addr);
3294    g.asm.mov_ri(S1, marker);
3295    g.asm.mov_mr(S0, 0, S1);
3296    g.asm.jmp(propagate);
3297}
3298
3299/// Lower a mode-B PRECISE self-`Call` (a REAL SysV call through the program's
3300/// entry table) — bit-identical to `logos_stencil_call_precise`:
3301///
3302/// 1. entry-cell load + zero guard (precise-tag exit);
3303/// 2. live-depth guard `depth >= MAX` (precise-tag exit);
3304/// 3. arena-bound guard `callee_base + (regc+3)*8 > arena_end` (precise-tag exit),
3305///    where `arena_end = frame[limit_slot]`, `callee_base = BASE + args_start*8`,
3306///    `regc = frame_size - 3` (the published table regcount);
3307/// 4. plant `arena_end` into the callee's own limit slot
3308///    (`callee_base[regc+2] = callee_base[frame_size-1]`);
3309/// 5. `*depth += 1`; spill caller-saved residents; `call [entry]`;
3310/// 6. `*depth -= 1`; if `*status != 0` → PRECISE PROPAGATE (bare flush+return,
3311///    the inner exit code — already a precise tag — stays in the cell); else
3312///    reload residents and store the result handle into `dst`.
3313///
3314/// Unlike the classic self-call, the args are PRE-STAGED by `Move` micros into
3315/// the disjoint callee window (`args_start = frame_size`) before this op, and the
3316/// plant window/resume/dst slots were written by the preceding `LoadConst`
3317/// micros (all frame-resident), so the precise-deopt walk can chain frames. On
3318/// ANY guard failure the precise epilogue writes `code | (*depth << 32)`.
3319#[allow(clippy::too_many_arguments)]
3320fn emit_precise_call(
3321    g: &mut Gen,
3322    idx: usize,
3323    dst: Slot,
3324    args_start: Slot,
3325    limit_slot: Slot,
3326    frame_size: i64,
3327    status_addr: i64,
3328    depth_addr: i64,
3329    entry_addr: i64,
3330    code: i64,
3331) {
3332    let propagate = g
3333        .self_call
3334        .as_ref()
3335        .expect("precise call without a propagate epilogue")
3336        .propagate_label;
3337    let fail = g
3338        .precise
3339        .as_ref()
3340        .map(|p| p.code_blocks[&code])
3341        .expect("precise call without a code block");
3342
3343    // The published table regcount is frame_size - 3; the callee frame spans
3344    // (regc + 3) slots, exactly the stencil's `regc.wrapping_add(3)`.
3345    let bound_slots = frame_size; // == (frame_size - 3) + 3.
3346
3347    // ---- 1. entry-cell load + zero guard ----
3348    g.asm.mov_ri(S0, entry_addr);
3349    g.asm.mov_rm(S0, S0, 0);
3350    g.asm.test_rr(S0, S0);
3351    g.asm.jcc(Cond::Eq, fail);
3352
3353    // ---- 2. depth guard: *depth >= MAX ----
3354    g.asm.mov_ri(S1, depth_addr);
3355    g.asm.mov_rm(S1, S1, 0);
3356    g.asm.cmp_ri(S1, SELF_CALL_DEPTH_LIMIT as i32);
3357    g.asm.jcc(Cond::Ge, fail);
3358
3359    // ---- 3. arena-bound guard ----
3360    // SC = callee_base = BASE + args_start*8.
3361    g.asm.mov_rr(SC, BASE);
3362    g.asm.add_ri(SC, (args_start as i32) * 8);
3363    // S0 = arena_end = frame[limit_slot].
3364    g.asm.mov_rm(S0, BASE, (limit_slot as i32) * 8);
3365    // S1 = callee_base + bound_slots*8.
3366    g.asm.mov_rr(S1, SC);
3367    g.asm.add_ri(S1, (bound_slots as i32) * 8);
3368    g.asm.cmp_rr(S1, S0);
3369    g.asm.jcc(Cond::Gt, fail);
3370
3371    // ---- 4. plant arena_end into the callee's own limit slot ----
3372    g.asm.mov_mr(SC, ((frame_size - 1) as i32) * 8, S0); // callee_base[fs-1] = arena_end
3373
3374    // ---- 5a. *depth += 1 ----
3375    g.asm.mov_ri(SC, depth_addr);
3376    g.asm.mov_rm(S0, SC, 0);
3377    g.asm.add_ri(S0, 1);
3378    g.asm.mov_mr(SC, 0, S0);
3379
3380    // ---- 5b. spill the LIVE-AFTER caller-saved residents ----
3381    // A resident dead after this precise call is read by no later op AND is not
3382    // observed by a precise resume at this call's PC (the resume reads live-IN
3383    // slots, a subset of these live-after residents plus the frame-resident
3384    // args/limit) — so eliding its spill/reload is bit-identical.
3385    g.spill_volatiles_at(idx);
3386
3387    // ---- 5c. SysV call args + CALL ----
3388    g.asm.mov_rr(Reg::Rdi, BASE);
3389    g.asm.add_ri(Reg::Rdi, (args_start as i32) * 8);
3390    g.asm.mov_ri(Reg::Rsi, 0);
3391    g.asm.mov_ri(Reg::Rdx, 0);
3392    g.asm.mov_ri(Reg::Rcx, 0);
3393    g.asm.mov_ri(Reg::R8, 0);
3394    g.asm.mov_ri(Reg::R9, 0);
3395    g.asm.mov_ri(S0, entry_addr);
3396    g.asm.mov_rm(S0, S0, 0);
3397    g.asm.call_r(S0); // result -> rax (S0)
3398
3399    // ---- 6a. *depth -= 1 (rcx scratch; rax holds the result) ----
3400    g.asm.mov_ri(SC, depth_addr);
3401    g.asm.mov_rm(S1, SC, 0);
3402    g.asm.sub_ri(S1, 1);
3403    g.asm.mov_mr(SC, 0, S1);
3404
3405    // ---- 6b. status check: if *status != 0 propagate (rax irrelevant) ----
3406    g.asm.mov_ri(S1, status_addr);
3407    g.asm.mov_rm(S1, S1, 0);
3408    g.asm.test_rr(S1, S1);
3409    g.asm.jcc(Cond::Ne, propagate);
3410
3411    // ---- 6c. reload the LIVE-AFTER caller-saved residents ----
3412    g.reload_volatiles_at(idx);
3413
3414    // ---- 6d. store the result handle into dst ----
3415    g.store(dst, S0);
3416}
3417
3418/// Lower one supported op.
3419fn emit_op(
3420    g: &mut Gen,
3421    op: &MicroOp,
3422    idx: usize,
3423    written: &[bool],
3424    used_callee: &[Reg],
3425) -> Option<()> {
3426    match *op {
3427        MicroOp::LoadConst { dst, value } => {
3428            // Materialize the raw bits in a GP scratch; if the dst is XMM-
3429            // resident (a float const like `0.0`), bridge through `movq`.
3430            g.asm.mov_ri(S0, value);
3431            if let Loc::Xmm(_) = g.loc[dst as usize] {
3432                g.asm.movq_xr(FS0, S0);
3433                g.fstore(dst, FS0);
3434            } else {
3435                g.store(dst, S0);
3436            }
3437        }
3438        MicroOp::Move { dst, src } => {
3439            // A raw-bits copy. If EITHER end is XMM-resident, route through an
3440            // XMM scratch (fload/fstore bit-preserve across all locations);
3441            // otherwise the GP path (a direct reg move when both resident).
3442            if matches!(g.loc[dst as usize], Loc::Xmm(_))
3443                || matches!(g.loc[src as usize], Loc::Xmm(_))
3444            {
3445                let x = g.foperand(src, FS0);
3446                g.fstore(dst, x);
3447            } else {
3448                let r = g.operand(src, S0);
3449                g.store(dst, r);
3450            }
3451        }
3452        MicroOp::Add { dst, lhs, rhs } => emit_commutative(g, dst, lhs, rhs, AsmBinop::Add, idx),
3453        MicroOp::Sub { dst, lhs, rhs } => emit_sub(g, dst, lhs, rhs, idx),
3454        MicroOp::Mul { dst, lhs, rhs } => emit_commutative(g, dst, lhs, rhs, AsmBinop::Mul, idx),
3455        MicroOp::BitAnd { dst, lhs, rhs } => emit_commutative(g, dst, lhs, rhs, AsmBinop::And, idx),
3456        MicroOp::BitOr { dst, lhs, rhs } => emit_commutative(g, dst, lhs, rhs, AsmBinop::Or, idx),
3457        MicroOp::BitXor { dst, lhs, rhs } => emit_commutative(g, dst, lhs, rhs, AsmBinop::Xor, idx),
3458        MicroOp::Lt { dst, lhs, rhs } => emit_compare(g, dst, lhs, rhs, Cond::Lt),
3459        MicroOp::Gt { dst, lhs, rhs } => emit_compare(g, dst, lhs, rhs, Cond::Gt),
3460        MicroOp::LtEq { dst, lhs, rhs } => emit_compare(g, dst, lhs, rhs, Cond::Le),
3461        MicroOp::GtEq { dst, lhs, rhs } => emit_compare(g, dst, lhs, rhs, Cond::Ge),
3462        MicroOp::Eq { dst, lhs, rhs } => emit_compare(g, dst, lhs, rhs, Cond::Eq),
3463        MicroOp::Neq { dst, lhs, rhs } => emit_compare(g, dst, lhs, rhs, Cond::Ne),
3464        MicroOp::NotInt { dst, src } => {
3465            g.load(S0, src);
3466            g.asm.not_r(S0);
3467            g.store(dst, S0);
3468        }
3469        MicroOp::NotBool { dst, src } => {
3470            // dst = src ^ 1
3471            g.load(S0, src);
3472            g.asm.mov_ri(S1, 1);
3473            g.asm.xor_rr(S0, S1);
3474            g.store(dst, S0);
3475        }
3476        MicroOp::Shl { dst, lhs, rhs } => emit_shift(g, dst, lhs, rhs, ShiftKind::Left),
3477        MicroOp::Shr { dst, lhs, rhs } => emit_shift(g, dst, lhs, rhs, ShiftKind::Right),
3478        MicroOp::DivPow2 { dst, lhs, k } => emit_div_pow2(g, dst, lhs, k),
3479        MicroOp::MagicDivU { dst, lhs, magic, more, mul_back } => {
3480            emit_magic_div(g, dst, lhs, magic, more, mul_back)
3481        }
3482        MicroOp::Div { dst, lhs, rhs } => {
3483            let dl = g.checked_exit(idx).expect("Div without a deopt label");
3484            emit_div_mod(g, dst, lhs, rhs, true, dl);
3485        }
3486        MicroOp::Mod { dst, lhs, rhs } => {
3487            let dl = g.checked_exit(idx).expect("Mod without a deopt label");
3488            emit_div_mod(g, dst, lhs, rhs, false, dl);
3489        }
3490        MicroOp::Branch { cmp, lhs, rhs, target } => {
3491            // Transfer to `target` when cmp(lhs, rhs) is FALSE; fall through
3492            // when TRUE. So jump on the NEGATED condition.
3493            let a = g.operand(lhs, S0);
3494            let b = g.operand(rhs, S1);
3495            g.asm.cmp_rr(a, b);
3496            let neg = cond_of(cmp.negated());
3497            g.asm.jcc(neg, g.op_labels[target]);
3498        }
3499        MicroOp::Jump { target } => {
3500            if target != idx + 1 {
3501                g.asm.jmp(g.op_labels[target]);
3502            }
3503        }
3504        MicroOp::JumpIfFalse { cond, target } => {
3505            let r = g.operand(cond, S0);
3506            g.asm.test_rr(r, r);
3507            g.asm.jcc(Cond::Eq, g.op_labels[target]); // ZF set => value == 0
3508        }
3509        MicroOp::JumpIfTrue { cond, target } => {
3510            let r = g.operand(cond, S0);
3511            g.asm.test_rr(r, r);
3512            g.asm.jcc(Cond::Ne, g.op_labels[target]); // ZF clear => value != 0
3513        }
3514        MicroOp::ArrLoad { dst, idx: idx_slot, ptr_slot, len_slot, byte: false, narrow32: true, checked } => {
3515            let dl = g.checked_exit(idx);
3516            emit_arr_load_i32(g, dst, idx_slot, ptr_slot, len_slot, checked, dl);
3517        }
3518        MicroOp::ArrStore { src, idx: idx_slot, ptr_slot, len_slot, byte: false, narrow32: true, checked } => {
3519            let dl = g.checked_exit(idx);
3520            emit_arr_store_i32(g, src, idx_slot, ptr_slot, len_slot, checked, dl);
3521        }
3522        MicroOp::ArrLoad { dst, idx: idx_slot, ptr_slot, len_slot, byte: false, narrow32: false, checked } => {
3523            // The exit label is only consulted for a CHECKED access; an unchecked
3524            // load needs none (and a region with no checked op has no label).
3525            let dl = g.checked_exit(idx);
3526            emit_arr_load(g, dst, idx_slot, ptr_slot, len_slot, checked, dl);
3527        }
3528        MicroOp::ArrStore { src, idx: idx_slot, ptr_slot, len_slot, byte: false, narrow32: false, checked } => {
3529            let dl = g.checked_exit(idx);
3530            emit_arr_store(g, src, idx_slot, ptr_slot, len_slot, checked, dl);
3531        }
3532        MicroOp::ArrLoad { dst, idx: idx_slot, ptr_slot, len_slot, byte: true, narrow32: _, checked } => {
3533            let dl = g.checked_exit(idx);
3534            emit_arr_load_byte(g, dst, idx_slot, ptr_slot, len_slot, checked, dl);
3535        }
3536        MicroOp::ArrStore { src, idx: idx_slot, ptr_slot, len_slot, byte: true, narrow32: _, checked } => {
3537            let dl = g.checked_exit(idx);
3538            emit_arr_store_byte(g, src, idx_slot, ptr_slot, len_slot, checked, dl);
3539        }
3540        // ---- LIST MUTATION (wave 13) — helper calls into the JIT runtime. ----
3541        MicroOp::ArrPush { src, vec_slot, ptr_slot, len_slot, helper_addr, byte, narrow32 } => {
3542            emit_list_push(g, idx, src, vec_slot, ptr_slot, len_slot, helper_addr, byte, narrow32);
3543        }
3544        MicroOp::ListClear { vec_slot, ptr_slot, len_slot, helper_addr } => {
3545            emit_list_clear(g, idx, vec_slot, ptr_slot, len_slot, helper_addr);
3546        }
3547        MicroOp::StrAppend { text_handle_slot, src, helper_addr } => {
3548            emit_str_append(g, idx, text_handle_slot, src, helper_addr);
3549        }
3550        // ---- MODE-B list machinery (precise path). The fresh-list-RETURN
3551        // recursion shape (mergesort): `NewList` allocates a registry-owned fresh
3552        // buffer and plants its pin triple, `ArrPush` (above) appends with a
3553        // realloc refresh, and `ListTriple` refreshes a triple from a live handle
3554        // (a self-call's returned list). ----
3555        MicroOp::NewList { vec_slot, ptr_slot, len_slot, helper_addr } => {
3556            emit_new_list(g, idx, vec_slot, ptr_slot, len_slot, helper_addr);
3557        }
3558        MicroOp::ListTriple { handle_slot, vec_slot, ptr_slot, len_slot, helper_addr } => {
3559            emit_list_triple(g, idx, handle_slot, vec_slot, ptr_slot, len_slot, helper_addr);
3560        }
3561        // ---- FLOAT (f64 / XMM) ops. IEEE; no FMA; no reassociation. ----
3562        MicroOp::AddF { dst, lhs, rhs } => emit_fbinop(g, dst, lhs, rhs, FBinop::Add),
3563        MicroOp::SubF { dst, lhs, rhs } => emit_fbinop(g, dst, lhs, rhs, FBinop::Sub),
3564        MicroOp::MulF { dst, lhs, rhs } => emit_fbinop(g, dst, lhs, rhs, FBinop::Mul),
3565        MicroOp::DivF { dst, lhs, rhs } => {
3566            let dl = g.checked_exit(idx).expect("DivF without a deopt label");
3567            emit_fdiv(g, dst, lhs, rhs, dl);
3568        }
3569        MicroOp::SqrtF { dst, src } => {
3570            let s = g.foperand(src, FS1);
3571            g.asm.sqrtsd_rr(FS0, s);
3572            g.fstore(dst, FS0);
3573        }
3574        MicroOp::IntToFloat { dst, src } => {
3575            // cvtsi2sd needs a GP source; materialize the int into S0.
3576            let r = g.operand(src, S0);
3577            g.asm.cvtsi2sd(FS0, r);
3578            g.fstore(dst, FS0);
3579        }
3580        MicroOp::FmaF { dst, a, b, c } => {
3581            // TWO IEEE roundings (mulsd THEN addsd), bit-identical to the
3582            // reference `(a*b) + c`. NEVER a fused `vfmadd` (one rounding).
3583            g.fload(FS0, a); // FS0 = a
3584            let bv = g.foperand(b, FS1);
3585            g.asm.mulsd_rr(FS0, bv); // FS0 = a*b (rounded once)
3586            let cv = g.foperand(c, FS1);
3587            g.asm.addsd_rr(FS0, cv); // FS0 = (a*b) + c (rounded again)
3588            g.fstore(dst, FS0);
3589        }
3590        MicroOp::LtF { dst, lhs, rhs } => emit_fcompare(g, dst, lhs, rhs, FCmp::Lt),
3591        MicroOp::GtF { dst, lhs, rhs } => emit_fcompare(g, dst, lhs, rhs, FCmp::Gt),
3592        MicroOp::LtEqF { dst, lhs, rhs } => emit_fcompare(g, dst, lhs, rhs, FCmp::Le),
3593        MicroOp::GtEqF { dst, lhs, rhs } => emit_fcompare(g, dst, lhs, rhs, FCmp::Ge),
3594        MicroOp::EqF { dst, lhs, rhs } => emit_feq(g, dst, lhs, rhs, false),
3595        MicroOp::NeqF { dst, lhs, rhs } => emit_feq(g, dst, lhs, rhs, true),
3596        MicroOp::BranchF { cmp, lhs, rhs, target } => {
3597            emit_branchf(g, cmp, lhs, rhs, g.op_labels[target]);
3598        }
3599        MicroOp::Return { src } => {
3600            emit_flush_and_return(g, written, used_callee, Some(src));
3601        }
3602        _ => return None,
3603    }
3604    Some(())
3605}
3606
3607/// Compute the 1-based array element address into [`S0`], side-exiting to the
3608/// deopt epilogue on a CHECKED out-of-bounds index — bit-identical to the
3609/// `ST_ARRLD`/`ST_ARRLDB`/`ST_ARRST`/`ST_ARRSTB` stencils: `im1 = idx - 1`
3610/// (wrapping); a checked access exits when `(im1 as u64) >= (len as u64)` (the
3611/// unsigned compare catches both the 0/negative index — `im1` wraps huge — and
3612/// the over-length case); `addr = ptr + im1 * stride`, where `stride` is 8 for
3613/// 8-byte int/float elements (`byte == false`) and 1 for 1-byte `Seq of Bool`
3614/// elements (`byte == true`). After this returns the address is in `S0` and the
3615/// length/pointer scratch (`S1`) is dead.
3616fn emit_arr_addr(g: &mut Gen, idx: Slot, ptr_slot: Slot, len_slot: Slot, checked: bool, byte: bool, dl: Option<LabelId>) {
3617    // SCALED-INDEX CSE FAST PATH. When the reserved register already holds the
3618    // 0-based index `im1 = frame[idx] - 1` for THIS index slot at THIS element
3619    // stride (a hit established by an earlier access in this straight-line run),
3620    // skip the index reload + decrement. The bounds check still runs PER ACCESS
3621    // (each array has its own length) against the cached TRUE `im1`, so an
3622    // out-of-bounds index — including the wrapped huge value a 0/negative index
3623    // produces — side-exits identically; only the `idx - 1` (and, with the second
3624    // reserved register, the `* 8` scaling) arithmetic is shared.
3625    if let (Some(im1_reg), Some(state)) = (g.off_im1_reg, g.off_cache) {
3626        if state.idx == idx && state.byte == byte {
3627            if checked {
3628                let dl = dl.expect("checked array op without a deopt label");
3629                // The invariant length: a hoisted len register (loop pre-header
3630                // load) when active, else the per-access frame reload.
3631                let len = g.hoisted_len(ptr_slot).unwrap_or_else(|| g.operand(len_slot, S1));
3632                g.asm.cmp_rr(im1_reg, len); // im1 - len
3633                g.asm.jcc(Cond::AeU, dl); // (im1 as u64) >= (len as u64) → OOB
3634            }
3635            // addr = ptr + im1 * stride, reusing the cached index. A byte stride
3636            // is `im1` itself; an 8-byte stride reuses the cached scaled offset
3637            // (second register) or re-shifts the cached `im1` (one register).
3638            if byte {
3639                g.asm.mov_rr(S0, im1_reg);
3640            } else if let Some(scaled_reg) = g.off_scaled_reg {
3641                g.asm.mov_rr(S0, scaled_reg); // S0 = im1 * 8 (cached)
3642            } else {
3643                g.asm.mov_rr(S0, im1_reg);
3644                g.asm.shl_ri(S0, 3); // im1 * 8
3645            }
3646            // The invariant base pointer: the hoisted ptr register when active,
3647            // else the per-access frame reload.
3648            let ptr = g.hoisted_ptr(ptr_slot).unwrap_or_else(|| g.operand(ptr_slot, S1));
3649            g.asm.add_rr(S0, ptr); // S0 = ptr + im1*stride
3650            return;
3651        }
3652    }
3653
3654    // COMPUTE PATH (cache miss or CSE disabled): the literal per-access lowering,
3655    // additionally populating the reserved register(s) so the next access in the
3656    // run hits the fast path above. Bit-identical to the stencil tier.
3657    // S0 = idx; S0 -= 1  → im1.
3658    g.load(S0, idx);
3659    g.asm.sub_ri(S0, 1);
3660    // Cache the freshly computed `im1` (the 0-based index) for reuse.
3661    if let Some(im1_reg) = g.off_im1_reg {
3662        g.asm.mov_rr(im1_reg, S0);
3663        g.off_cache = Some(OffCacheState { idx, byte });
3664    }
3665    if checked {
3666        let dl = dl.expect("checked array op without a deopt label");
3667        let len = g.hoisted_len(ptr_slot).unwrap_or_else(|| g.operand(len_slot, S1));
3668        g.asm.cmp_rr(S0, len); // im1 - len
3669        g.asm.jcc(Cond::AeU, dl); // (im1 as u64) >= (len as u64) → OOB
3670    }
3671    // addr = ptr + im1 * stride. Byte (Bool) elements are 1-byte: no scaling.
3672    if !byte {
3673        g.asm.shl_ri(S0, 3); // im1 * 8 (8-byte i64/f64 elements)
3674        // Cache the scaled offset too (second register), so a reused 8-byte
3675        // access skips the shift as well.
3676        if g.off_im1_reg.is_some() {
3677            if let Some(scaled_reg) = g.off_scaled_reg {
3678                g.asm.mov_rr(scaled_reg, S0);
3679            }
3680        }
3681    }
3682    let ptr = g.hoisted_ptr(ptr_slot).unwrap_or_else(|| g.operand(ptr_slot, S1));
3683    g.asm.add_rr(S0, ptr); // S0 = ptr + im1*stride
3684}
3685
3686/// `frame[dst] = buffer[frame[idx] - 1]` (8-byte int element). The loaded value
3687/// goes through scratch `S1` so a resident `dst` is written with a register move.
3688/// A CHECKED out-of-bounds index jumps to `dl` (the classic deopt epilogue or, in
3689/// the precise path, this op's tag-staging block).
3690fn emit_arr_load(g: &mut Gen, dst: Slot, idx: Slot, ptr_slot: Slot, len_slot: Slot, checked: bool, dl: Option<LabelId>) {
3691    emit_arr_addr(g, idx, ptr_slot, len_slot, checked, false, dl);
3692    g.asm.mov_rm(S1, S0, 0); // S1 = *addr
3693    g.store(dst, S1);
3694}
3695
3696/// `frame[dst] = buffer[frame[idx] - 1] as i64` over 1-BYTE (`Seq of Bool`)
3697/// elements — bit-identical to `logos_stencil_arrldb`: a zero-extended 1-byte
3698/// load (`movzx`), so the loaded `u8` (0..=255, in practice 0/1) widens to a
3699/// NON-NEGATIVE i64. The loaded value goes through scratch `S1` so a resident
3700/// `dst` is written with a register move; OOB side-exit is identical to the
3701/// 8-byte path (same `im1`/length unsigned guard).
3702fn emit_arr_load_byte(g: &mut Gen, dst: Slot, idx: Slot, ptr_slot: Slot, len_slot: Slot, checked: bool, dl: Option<LabelId>) {
3703    emit_arr_addr(g, idx, ptr_slot, len_slot, checked, true, dl);
3704    g.asm.movzx_rm8(S1, S0, 0); // S1 = *(u8*)addr (zero-extended)
3705    g.store(dst, S1);
3706}
3707
3708/// `buffer[frame[idx] - 1] = frame[src]` (8 raw bytes). The stored value is the
3709/// slot's raw bits, so a FLOAT-resident (XMM) source is bit-copied to a GP
3710/// register via `movq` first — the buffer holds the same 8 bytes either way. A
3711/// CHECKED store side-exits BEFORE the write (to `dl`), so an out-of-bounds store
3712/// leaves the buffer untouched (the reference and stencil contract).
3713fn emit_arr_store(g: &mut Gen, src: Slot, idx: Slot, ptr_slot: Slot, len_slot: Slot, checked: bool, dl: Option<LabelId>) {
3714    emit_arr_addr(g, idx, ptr_slot, len_slot, checked, false, dl);
3715    // The address is in S0 and the pointer scratch S1 is dead. Materialize the
3716    // source bits into a GP reg that cannot collide with the address: SC (rcx)
3717    // for a GP/frame source, or S1 (the dead pointer scratch) for an XMM source.
3718    let v = match g.loc[src as usize] {
3719        Loc::Xmm(x) => {
3720            g.asm.movq_rx(S1, x);
3721            S1
3722        }
3723        _ => g.operand(src, SC),
3724    };
3725    g.asm.mov_mr(S0, 0, v); // *addr = value
3726}
3727
3728/// `buffer[frame[idx] - 1] = (frame[src] != 0) as u8` over 1-BYTE (`Seq of Bool`)
3729/// elements — bit-identical to `logos_stencil_arrstb`: the stored byte is the
3730/// BOOLEAN NORMALIZATION of the value (1 when nonzero, else 0), NOT its raw low
3731/// byte. A CHECKED store side-exits BEFORE the write (to `dl`), so an
3732/// out-of-bounds store leaves the buffer untouched.
3733///
3734/// The normalization is `test v, v; setne S1l` — `S1` (rdx) is the dead pointer
3735/// scratch after the address is in `S0`, and its low byte `dl` is REX-free
3736/// addressable. Only that low byte is then stored. An XMM-resident source is
3737/// bit-copied to `SC` (rcx) first so its full 64 bits feed the zero test (a
3738/// nonzero f64 bit pattern — incl. `-0.0` and NaN — stores 1, matching `!= 0`).
3739fn emit_arr_store_byte(g: &mut Gen, src: Slot, idx: Slot, ptr_slot: Slot, len_slot: Slot, checked: bool, dl: Option<LabelId>) {
3740    emit_arr_addr(g, idx, ptr_slot, len_slot, checked, true, dl);
3741    // Materialize the source value into a GP reg distinct from the address (S0).
3742    // SC (rcx) for a GP/frame source; for an XMM source, bit-copy to SC.
3743    let v = match g.loc[src as usize] {
3744        Loc::Xmm(x) => {
3745            g.asm.movq_rx(SC, x);
3746            SC
3747        }
3748        _ => g.operand(src, SC),
3749    };
3750    // (v != 0) → S1's low byte via test + setne; store that one byte.
3751    g.asm.test_rr(v, v);
3752    g.asm.setcc8(Cond::Ne, S1);
3753    g.asm.mov_mr8(S0, 0, S1); // *(u8*)addr = (v != 0) as u8
3754}
3755
3756/// Address machinery for a 4-byte (`IntsI32`) element: `S0 = ptr + (idx-1)*4`,
3757/// with the same unsigned over-length OOB guard as [`emit_arr_addr`]. A literal
3758/// per-access lowering (no scaled-index CSE — the half-width lane is rare and
3759/// the index reuse is the dominant cost, already covered by the loop's other
3760/// accesses); after this the address is in `S0` and the scratch `S1` is dead.
3761fn emit_arr_addr_i32(g: &mut Gen, idx: Slot, ptr_slot: Slot, len_slot: Slot, checked: bool, dl: Option<LabelId>) {
3762    g.load(S0, idx);
3763    g.asm.sub_ri(S0, 1); // im1 = idx - 1
3764    if checked {
3765        let dl = dl.expect("checked array op without a deopt label");
3766        let len = g.hoisted_len(ptr_slot).unwrap_or_else(|| g.operand(len_slot, S1));
3767        g.asm.cmp_rr(S0, len); // im1 - len
3768        g.asm.jcc(Cond::AeU, dl); // (im1 as u64) >= (len as u64) → OOB
3769    }
3770    g.asm.shl_ri(S0, 2); // im1 * 4 (4-byte i32 elements)
3771    let ptr = g.hoisted_ptr(ptr_slot).unwrap_or_else(|| g.operand(ptr_slot, S1));
3772    g.asm.add_rr(S0, ptr); // S0 = ptr + im1*4
3773}
3774
3775/// `frame[dst] = buffer[frame[idx] - 1] as i64` over 4-byte SIGN-EXTENDED
3776/// (`IntsI32`) elements — bit-identical to `logos_stencil_arrld_i32`: a
3777/// `movsxd` widening the stored `i32` to a full i64. Through scratch `S1` so a
3778/// resident `dst` is written with a register move; OOB side-exit identical to
3779/// the 8-byte path. A half-width access drops the scaled-index CSE cache (its
3780/// stride differs from any cached 8-byte run).
3781fn emit_arr_load_i32(g: &mut Gen, dst: Slot, idx: Slot, ptr_slot: Slot, len_slot: Slot, checked: bool, dl: Option<LabelId>) {
3782    g.invalidate_off_cache();
3783    emit_arr_addr_i32(g, idx, ptr_slot, len_slot, checked, dl);
3784    g.asm.movsxd_rm(S1, S0, 0); // S1 = *(i32*)addr (sign-extended)
3785    g.store(dst, S1);
3786}
3787
3788/// `buffer[frame[idx] - 1] = frame[src] as i32` over 4-byte (`IntsI32`)
3789/// elements — bit-identical to `logos_stencil_arrst_i32`: a truncating 4-byte
3790/// store (lossless under the narrowing proof). A CHECKED store side-exits
3791/// BEFORE the write. An XMM-resident source is bit-copied to a GP reg first.
3792fn emit_arr_store_i32(g: &mut Gen, src: Slot, idx: Slot, ptr_slot: Slot, len_slot: Slot, checked: bool, dl: Option<LabelId>) {
3793    g.invalidate_off_cache();
3794    emit_arr_addr_i32(g, idx, ptr_slot, len_slot, checked, dl);
3795    let v = match g.loc[src as usize] {
3796        Loc::Xmm(x) => {
3797            g.asm.movq_rx(S1, x);
3798            S1
3799        }
3800        _ => g.operand(src, SC),
3801    };
3802    g.asm.mov_mr32(S0, 0, v); // *(i32*)addr = value as i32 (low 4 bytes)
3803}
3804
3805/// Lower a pinned-list PUSH (`ArrPush`) with an INLINE FAST PATH plus a cold
3806/// realloc helper call — the V8-style array-push lowering.
3807///
3808/// The common case (the appended element fits the current capacity) runs ENTIRELY
3809/// in scratch registers, with NO SysV call and NO caller-saved spill/reload:
3810///
3811/// ```text
3812///     vec = frame[vec_slot]            // the live *mut Vec
3813///     len = frame[len_slot]            // the mirrored length
3814///     if (len as u64) < (*(vec+cap_off) as u64):    // spare capacity?
3815///         ptr = frame[ptr_slot]
3816///         ptr[len] = value             // store (8-byte raw, or (v!=0) byte)
3817///         len += 1
3818///         frame[len_slot] = len        // refresh the mirror
3819///         *(vec+len_off) = len         //   AND the real Vec::len field
3820///         goto join
3821///     // else fall through to the cold realloc path:
3822///     <spill caller-saved>; helper(frame, vec, ptr, len, value); <reload>
3823///   join:
3824/// ```
3825///
3826/// SOUNDNESS — bit-identical to `logos_rt_push_*` (and thus to `Vec::push` and the
3827/// tree-walker) in every case:
3828/// * The capacity test uses the SAME `cap`/`len` the helper would (`(*vec).len() <
3829///   (*vec).capacity()` is exactly `Vec::push`'s "no grow" condition), read from
3830///   the LIVE `Vec` at PROBED field offsets ([`vec_layout`]) — never a hardcoded
3831///   layout — so it is correct on this exact binary.
3832/// * On the fast path the buffer does NOT move, so `frame[ptr_slot]` stays valid
3833///   (the helper would not refresh it either) and the store lands at the identical
3834///   address `ptr + len*stride` the helper's `(*vec).push` writes.
3835/// * BOTH the mirrored `frame[len_slot]` AND the real `Vec::len` field are bumped,
3836///   so a later non-fast access (a subsequent realloc helper push, a deopt that
3837///   materializes/keeps the live `Rc<Vec>`, or the `Vec`'s eventual drop) observes
3838///   the appended element exactly as after `Vec::push`.
3839/// * The cold path is the ORIGINAL helper-call lowering verbatim: on a realloc
3840///   (`len == cap`) the helper reallocates and refreshes `frame[ptr_slot]` /
3841///   `frame[len_slot]` (and the real `Vec` fields), so the next access reads the
3842///   fresh pointer — the realloc-coherence contract is unchanged.
3843/// * The fast path touches ONLY the reserved scratch registers `S0`/`S1`/`SC`
3844///   (`rax`/`rdx`/`rcx`), which are NEVER assigned to a resident slot, so it
3845///   clobbers no loop-carried value and needs no spill/reload. The cold path keeps
3846///   the full caller-saved spill discipline.
3847/// * The vec/ptr/len triple is forced FRAME-resident (`force_frame_set`), so both
3848///   paths read/write the same frame cells and the helper's view agrees.
3849fn emit_list_push(
3850    g: &mut Gen,
3851    idx: usize,
3852    src: Slot,
3853    vec_slot: Slot,
3854    ptr_slot: Slot,
3855    len_slot: Slot,
3856    helper_addr: i64,
3857    byte: bool,
3858    narrow32: bool,
3859) {
3860    // Kill-switch / A-B toggle: fall back to the always-call helper lowering.
3861    if !inline_push_enabled() {
3862        g.spill_volatiles_at(idx);
3863        g.load(Reg::R8, src);
3864        g.asm.mov_rr(Reg::Rdi, BASE);
3865        g.asm.mov_ri(Reg::Rsi, vec_slot as i64);
3866        g.asm.mov_ri(Reg::Rdx, ptr_slot as i64);
3867        g.asm.mov_ri(Reg::Rcx, len_slot as i64);
3868        g.asm.mov_ri(S0, helper_addr);
3869        g.asm.call_r(S0);
3870        g.reload_volatiles_at(idx);
3871        return;
3872    }
3873
3874    let layout = vec_layout();
3875    let cold = g.asm.new_label();
3876    let join = g.asm.new_label();
3877
3878    // ---- FAST PATH ---- (scratch-only; no spill, no call when len < cap)
3879    // S0 = vec handle (kept live for the real-len writeback); S1 = len; SC = cap.
3880    g.asm.mov_rm(S0, BASE, (vec_slot as i32) * 8);
3881    g.asm.mov_rm(S1, BASE, (len_slot as i32) * 8);
3882    g.asm.mov_rm(SC, S0, layout.cap_off); // SC = (*vec).capacity
3883    g.asm.cmp_rr(S1, SC); // len - cap
3884    g.asm.jcc(Cond::AeU, cold); // (len as u64) >= (cap as u64) -> realloc (cold)
3885
3886    // Materialize the pushed value into SC (cap is dead). An XMM-resident
3887    // float-list value is bit-copied via movq — the raw 8 bytes travel to the
3888    // buffer exactly as the helper's `f64::from_bits(...).push(...)` round-trip.
3889    match g.loc[src as usize] {
3890        Loc::Xmm(x) => g.asm.movq_rx(SC, x),
3891        _ => {
3892            let v = g.operand(src, SC);
3893            if v != SC {
3894                g.asm.mov_rr(SC, v);
3895            }
3896        }
3897    }
3898    // addr = ptr + len*stride, computed into S0 (vec dead now; reloaded below for
3899    // the real-len writeback). S1 still holds `len`.
3900    g.asm.mov_rm(S0, BASE, (ptr_slot as i32) * 8); // S0 = ptr
3901    if narrow32 {
3902        g.asm.shl_ri(S1, 2); // len * 4 (4-byte i32 elements)
3903    } else if !byte {
3904        g.asm.shl_ri(S1, 3); // len * 8 (8-byte i64/f64 elements)
3905    }
3906    g.asm.add_rr(S0, S1); // S0 = ptr + len*stride  (S1 now dead)
3907    if byte {
3908        // 1-byte (`Seq of Bool`) store: the BOOLEAN NORMALIZATION (v != 0) as u8,
3909        // matching `logos_rt_push_bool`'s `(*vec).push(value != 0)`.
3910        g.asm.test_rr(SC, SC);
3911        g.asm.setcc8(Cond::Ne, S1);
3912        g.asm.mov_mr8(S0, 0, S1); // *(u8*)addr = (value != 0) as u8
3913    } else if narrow32 {
3914        // 4-byte (`IntsI32`) store: TRUNCATE the value to its low 4 bytes
3915        // (lossless under the narrowing proof), matching `logos_rt_push_i32`.
3916        g.asm.mov_mr32(S0, 0, SC); // *(i32*)addr = value as i32
3917    } else {
3918        g.asm.mov_mr(S0, 0, SC); // *addr = value (8 raw bytes)
3919    }
3920    // Bump the length: new_len = old_len + 1. Refresh BOTH the mirrored frame
3921    // slot AND the real `Vec::len` field so every later view is coherent.
3922    g.asm.mov_rm(S1, BASE, (len_slot as i32) * 8); // reload old len
3923    g.asm.add_ri(S1, 1); // new_len
3924    g.asm.mov_mr(BASE, (len_slot as i32) * 8, S1); // frame[len_slot] = new_len
3925    g.asm.mov_rm(S0, BASE, (vec_slot as i32) * 8); // reload vec handle
3926    g.asm.mov_mr(S0, layout.len_off, S1); // (*vec).len = new_len
3927    g.asm.jmp(join);
3928
3929    // ---- COLD PATH ---- (realloc: the original helper-call lowering verbatim).
3930    g.asm.bind(cold);
3931    // Spill the LIVE-AFTER caller-saved residents to their frame slots FIRST (the
3932    // helper clobbers them). This must precede touching any arg register: the arg
3933    // registers (rdi/rsi/rdx/rcx/r8) are themselves in the caller-saved pool, so a
3934    // live resident may sit in one of them. Spilling reads each resident's
3935    // register (still intact) into the frame; only AFTER that is it safe to
3936    // overwrite those registers with call args.
3937    g.spill_volatiles_at(idx);
3938    // Materialize the pushed value into the value-arg register (r8) AFTER the
3939    // spill (so no resident's value is lost). XMM-resident floats bit-copy.
3940    g.load(Reg::R8, src);
3941    g.asm.mov_rr(Reg::Rdi, BASE);
3942    g.asm.mov_ri(Reg::Rsi, vec_slot as i64);
3943    g.asm.mov_ri(Reg::Rdx, ptr_slot as i64);
3944    g.asm.mov_ri(Reg::Rcx, len_slot as i64);
3945    g.asm.mov_ri(S0, helper_addr);
3946    g.asm.call_r(S0);
3947    g.reload_volatiles_at(idx);
3948
3949    g.asm.bind(join);
3950}
3951
3952/// Lower a pinned-list in-place CLEAR (`ListClear`) as a SysV call to
3953/// `logos_rt_clear_*(frame, vec_slot, ptr_slot, len_slot)`: truncate the
3954/// SOLE-OWNED buffer to empty (keep its capacity) and refresh
3955/// `frame[ptr_slot]`/`frame[len_slot]` (length → 0). The buffer-reuse
3956/// alias-safety is established UPSTREAM in the micro-op lowering (a
3957/// `Op::NewEmptyList` whose handle escapes via a live `Move` declines to emit
3958/// `ListClear`), so a `ListClear` reaching here is provably unaliased. Same
3959/// frame-resident-triple + caller-saved spill discipline as the push, minus the
3960/// value argument.
3961fn emit_list_clear(g: &mut Gen, idx: usize, vec_slot: Slot, ptr_slot: Slot, len_slot: Slot, helper_addr: i64) {
3962    g.spill_volatiles_at(idx);
3963    g.asm.mov_rr(Reg::Rdi, BASE);
3964    g.asm.mov_ri(Reg::Rsi, vec_slot as i64);
3965    g.asm.mov_ri(Reg::Rdx, ptr_slot as i64);
3966    g.asm.mov_ri(Reg::Rcx, len_slot as i64);
3967    g.asm.mov_ri(S0, helper_addr);
3968    g.asm.call_r(S0);
3969    g.reload_volatiles_at(idx);
3970}
3971
3972/// Lower a fresh-list ALLOCATION (`NewList`, mode B) as a SysV call to
3973/// `logos_rt_alloc_list_i64(frame, vec_slot, ptr_slot, len_slot)`: the helper
3974/// `Box`-allocates a fresh empty `Vec`, REGISTERS it in the thread-local
3975/// allocation registry (so a deopt drains it and a `Return` detaches it), and
3976/// plants the pin triple — writing `frame[vec_slot]` (the live `*mut Vec`),
3977/// `frame[ptr_slot]` (its buffer pointer) and `frame[len_slot]` (0). The triple
3978/// is forced FRAME-resident (so the helper's frame writes are the single source
3979/// of truth and every later push/load reads the fresh pointer/length); the call
3980/// takes no value argument. Same caller-saved spill discipline as the push/clear
3981/// helpers — bit-identical to the per-piece tier's `ST_ALLOCLIST` stencil.
3982fn emit_new_list(g: &mut Gen, idx: usize, vec_slot: Slot, ptr_slot: Slot, len_slot: Slot, helper_addr: i64) {
3983    g.spill_volatiles_at(idx);
3984    g.asm.mov_rr(Reg::Rdi, BASE);
3985    g.asm.mov_ri(Reg::Rsi, vec_slot as i64);
3986    g.asm.mov_ri(Reg::Rdx, ptr_slot as i64);
3987    g.asm.mov_ri(Reg::Rcx, len_slot as i64);
3988    g.asm.mov_ri(S0, helper_addr);
3989    g.asm.call_r(S0);
3990    g.reload_volatiles_at(idx);
3991}
3992
3993/// Lower a triple-plant (`ListTriple`, mode B) as a SysV call to
3994/// `logos_rt_list_triple(frame, handle_slot, vec_slot, ptr_slot, len_slot)`: read
3995/// `frame[handle_slot]` (a live `*mut Vec`) and refresh the pin triple. The
3996/// handle and triple slots are all forced FRAME-resident (so the helper's frame
3997/// view is the source of truth). Same caller-saved spill discipline; the handle
3998/// slot rides the fifth SysV arg (`r8`).
3999fn emit_list_triple(
4000    g: &mut Gen,
4001    idx: usize,
4002    handle_slot: Slot,
4003    vec_slot: Slot,
4004    ptr_slot: Slot,
4005    len_slot: Slot,
4006    helper_addr: i64,
4007) {
4008    g.spill_volatiles_at(idx);
4009    g.asm.mov_rr(Reg::Rdi, BASE);
4010    g.asm.mov_ri(Reg::Rsi, handle_slot as i64);
4011    g.asm.mov_ri(Reg::Rdx, vec_slot as i64);
4012    g.asm.mov_ri(Reg::Rcx, ptr_slot as i64);
4013    g.asm.mov_ri(Reg::R8, len_slot as i64);
4014    g.asm.mov_ri(S0, helper_addr);
4015    g.asm.call_r(S0);
4016    g.reload_volatiles_at(idx);
4017}
4018
4019/// Lower a pinned MUTABLE-Text append (`StrAppend`) as a SysV call to the runtime
4020/// helper `logos_rt_str_append(frame, handle_slot, src, src_len)`: the helper
4021/// reads `frame[handle_slot]` (a live `*mut Value` to the VM accumulator cell)
4022/// and grows the accumulator with EXACTLY the VM's `add_assign` (in-place when
4023/// sole-owned, copy-on-write otherwise) semantics. The handle slot is forced
4024/// FRAME-resident (the helper indexes it). The source is either a 1-char frame
4025/// BYTE — read AFTER the spill into the value-arg register `rdx`, with `rcx` =
4026/// `-1` flagging the byte form — or a baked constant byte slice (`rdx` = the
4027/// `'static` pointer, `rcx` = its length). Same caller-saved spill discipline as
4028/// the list-push helper.
4029fn emit_str_append(
4030    g: &mut Gen,
4031    idx: usize,
4032    handle_slot: Slot,
4033    src: crate::jit::StrSrc,
4034    helper_addr: i64,
4035) {
4036    // Spill the LIVE-AFTER caller-saved residents FIRST (the helper clobbers
4037    // them; the arg registers are themselves caller-saved). The byte-form value
4038    // is read AFTER the spill (so no resident is lost) but BEFORE the arg
4039    // registers overwrite the register the source might still occupy.
4040    g.spill_volatiles_at(idx);
4041    match src {
4042        crate::jit::StrSrc::Byte(s) => {
4043            g.load(Reg::Rdx, s);
4044            g.asm.mov_ri(Reg::Rcx, -1);
4045        }
4046        crate::jit::StrSrc::Const { ptr, len } => {
4047            g.asm.mov_ri(Reg::Rdx, ptr);
4048            g.asm.mov_ri(Reg::Rcx, len);
4049        }
4050    }
4051    g.asm.mov_rr(Reg::Rdi, BASE);
4052    g.asm.mov_ri(Reg::Rsi, handle_slot as i64);
4053    g.asm.mov_ri(S0, helper_addr);
4054    g.asm.call_r(S0);
4055    g.reload_volatiles_at(idx);
4056}
4057
4058#[derive(Clone, Copy)]
4059enum AsmBinop {
4060    Add,
4061    Mul,
4062    And,
4063    Or,
4064    Xor,
4065}
4066
4067/// `dst = lhs OP rhs` for a commutative op. Compute in S0 to avoid clobbering a
4068/// resident operand that may be read again later. Integer `add`/`imul` SIDE-EXIT
4069/// on signed overflow (`jo` → this op's deopt label) BEFORE the store, so the
4070/// exact tier recomputes and promotes to BigInt; bitwise ops cannot overflow.
4071fn emit_commutative(g: &mut Gen, dst: Slot, lhs: Slot, rhs: Slot, op: AsmBinop, idx: usize) {
4072    g.load(S0, lhs);
4073    let b = g.operand(rhs, S1);
4074    match op {
4075        AsmBinop::Add => g.asm.add_rr(S0, b),
4076        AsmBinop::Mul => g.asm.imul_rr(S0, b),
4077        AsmBinop::And => g.asm.and_rr(S0, b),
4078        AsmBinop::Or => g.asm.or_rr(S0, b),
4079        AsmBinop::Xor => g.asm.xor_rr(S0, b),
4080    }
4081    if matches!(op, AsmBinop::Add | AsmBinop::Mul) {
4082        if let Some(dl) = g.checked_exit(idx) {
4083            g.asm.jcc(Cond::Overflow, dl);
4084        }
4085    }
4086    g.store(dst, S0);
4087}
4088
4089/// `dst = lhs - rhs` (non-commutative). lhs into S0, then `sub S0, rhs`; `jo` →
4090/// deopt on signed overflow (before the store) so the exact tier promotes.
4091fn emit_sub(g: &mut Gen, dst: Slot, lhs: Slot, rhs: Slot, idx: usize) {
4092    g.load(S0, lhs);
4093    let b = g.operand(rhs, S1);
4094    g.asm.sub_rr(S0, b);
4095    if let Some(dl) = g.checked_exit(idx) {
4096        g.asm.jcc(Cond::Overflow, dl);
4097    }
4098    g.store(dst, S0);
4099}
4100
4101/// `dst = (lhs CMP rhs) as i64` — `cmp` then `setcc`/`movzx`.
4102fn emit_compare(g: &mut Gen, dst: Slot, lhs: Slot, rhs: Slot, cond: Cond) {
4103    let a = g.operand(lhs, S0);
4104    let b = g.operand(rhs, S1);
4105    g.asm.cmp_rr(a, b);
4106    g.asm.setcc_movzx(cond, S0);
4107    g.store(dst, S0);
4108}
4109
4110#[derive(Clone, Copy)]
4111enum FBinop {
4112    Add,
4113    Sub,
4114    Mul,
4115}
4116
4117/// `dst = lhs OP rhs` for an f64 arithmetic op (`addsd`/`subsd`/`mulsd`).
4118///
4119/// The instruction order is ALWAYS the source order — `OP target, second`
4120/// computes `target OP second` — so a non-commutative `subsd` keeps `lhs - rhs`
4121/// and the result is bit-identical to the reference regardless of which fast
4122/// path fires. No reassociation, no FMA.
4123///
4124/// When `dst` is XMM-resident we compute IN PLACE in `dst`, avoiding the
4125/// `fload`→FS0 + `fstore`→dst scratch round-trip (two extra `movsd`s) that the
4126/// scratch path pays every iteration on a loop-carried accumulator (nbody's
4127/// dx/dy/dz/dist, mandelbrot's zr/zi):
4128/// - `dst == lhs` (in-place `x = x OP rhs`): just `OP dst, rhs` — no moves;
4129/// - `dst != lhs` and `dst != rhs`: `movsd dst, lhs; OP dst, rhs` — the result
4130///   lands in `dst` directly (one move, no store).
4131/// The remaining case (`dst` resident, `dst == rhs`, `dst != lhs`) cannot write
4132/// `dst` first without clobbering `rhs`, so it falls through to FS0 staging.
4133fn emit_fbinop(g: &mut Gen, dst: Slot, lhs: Slot, rhs: Slot, op: FBinop) {
4134    let apply = |asm: &mut Asm, target: Xmm, second: Xmm| match op {
4135        FBinop::Add => asm.addsd_rr(target, second),
4136        FBinop::Sub => asm.subsd_rr(target, second),
4137        FBinop::Mul => asm.mulsd_rr(target, second),
4138    };
4139    if let Loc::Xmm(d) = g.loc[dst as usize] {
4140        // Resolve rhs to a concrete register first (its own resident XMM, or
4141        // FS1 if frame-resident). FS1 is scratch and never a slot's residence,
4142        // so a frame-resident rhs never collides with `d`.
4143        let b = g.foperand(rhs, FS1);
4144        if g.loc[lhs as usize] == Loc::Xmm(d) {
4145            // In-place accumulate: dst and lhs ARE the same register.
4146            apply(&mut g.asm, d, b);
4147            return;
4148        }
4149        if d != b {
4150            // Materialize lhs into dst, then op against rhs — result in dst.
4151            g.fload(d, lhs);
4152            apply(&mut g.asm, d, b);
4153            return;
4154        }
4155        // d == b (dst aliases rhs, dst != lhs): writing dst first would lose
4156        // rhs; fall through to the scratch path, which reads rhs (b == d) before
4157        // overwriting dst.
4158        g.fload(FS0, lhs);
4159        apply(&mut g.asm, FS0, b);
4160        g.fstore(dst, FS0);
4161        return;
4162    }
4163    g.fload(FS0, lhs);
4164    let b = g.foperand(rhs, FS1);
4165    apply(&mut g.asm, FS0, b);
4166    g.fstore(dst, FS0);
4167}
4168
4169/// `dst = lhs / rhs` (f64), side-EXITING to the deopt epilogue when the divisor
4170/// is `0.0` — bit-identical to the reference (`b == 0.0 -> None`). The IEEE
4171/// `0.0 == -0.0` makes BOTH zeros trip it: the divisor bits are compared to 0.0
4172/// via `ucomisd`, and `ucomisd -0.0, 0.0` sets ZF (equal), so `je deopt` fires
4173/// for `-0.0` too. NaN divisor is NOT zero (ucomisd sets PF, ZF — but `je` on a
4174/// NaN-vs-0 compare: NaN is unordered → ZF=1 → would wrongly deopt). Guard the
4175/// NaN case: only deopt when the unordered flag is CLEAR (an ordered equality).
4176fn emit_fdiv(g: &mut Gen, dst: Slot, lhs: Slot, rhs: Slot, dl: LabelId) {
4177    // Zero-divisor side-exit: compare rhs to 0.0. Build 0.0 in FS1 (xorps would
4178    // need a new encoder; instead movq from a zeroed GP reg).
4179    let b = g.foperand(rhs, FS1);
4180    // FS0 = 0.0 via movq from a zeroed GP scratch.
4181    g.asm.mov_ri(S0, 0);
4182    g.asm.movq_xr(FS0, S0);
4183    g.asm.ucomisd_rr(b, FS0); // compare rhs, 0.0
4184    // An ORDERED equality (rhs == 0.0, incl -0.0) sets ZF=1, PF=0. A NaN divisor
4185    // is unordered: ZF=1, PF=1 — must NOT deopt (NaN/x is a valid IEEE result).
4186    // So skip the deopt when PF=1 (unordered), then `je` on the ordered equal.
4187    let not_zero = g.asm.new_label();
4188    g.asm.jcc(Cond::ParityEven, not_zero); // unordered (NaN) -> not a zero divisor
4189    g.asm.jcc(Cond::Eq, dl); // ordered equal to 0.0 -> side-exit
4190    g.asm.bind(not_zero);
4191    // Normal divide: FS0 = lhs; FS0 /= rhs.
4192    g.fload(FS0, lhs);
4193    let b2 = g.foperand(rhs, FS1);
4194    g.asm.divsd_rr(FS0, b2);
4195    g.fstore(dst, FS0);
4196}
4197
4198#[derive(Clone, Copy)]
4199enum FCmp {
4200    Lt,
4201    Gt,
4202    Le,
4203    Ge,
4204}
4205
4206/// `dst = (lhs CMP rhs) as i64` for an f64 ORDERING compare, exact IEEE: a NaN
4207/// operand makes the result FALSE (0). Implemented via `ucomisd` + an UNSIGNED
4208/// setcc — for `>`/`>=` directly (`a` then `b`), for `<`/`<=` by SWAPPING the
4209/// operands (so `a < b` ≡ `b > a`). The unordered (NaN) case sets ZF=CF=1, which
4210/// the `seta`/`setae` family reads as FALSE, matching the reference.
4211fn emit_fcompare(g: &mut Gen, dst: Slot, lhs: Slot, rhs: Slot, cmp: FCmp) {
4212    // Decide operand order + the unsigned condition.
4213    let (a, b, cond) = match cmp {
4214        FCmp::Gt => (lhs, rhs, Cond::AU),  // a > b   : ucomisd a,b ; seta
4215        FCmp::Ge => (lhs, rhs, Cond::AeU), // a >= b  : ucomisd a,b ; setae
4216        FCmp::Lt => (rhs, lhs, Cond::AU),  // a < b ≡ b > a : ucomisd b,a ; seta
4217        FCmp::Le => (rhs, lhs, Cond::AeU), // a <= b ≡ b >= a : ucomisd b,a ; setae
4218    };
4219    let xa = g.foperand(a, FS0);
4220    let xb = g.foperand(b, FS1);
4221    g.asm.ucomisd_rr(xa, xb);
4222    g.asm.setcc_movzx(cond, S0);
4223    g.store(dst, S0);
4224}
4225
4226/// `dst = (lhs == rhs) as i64` (when `neg` is false) or `(lhs != rhs)` (`neg`
4227/// true) — IEEE float equality via `ucomisd`: ordered-equal ⟺ ZF=1 && PF=0
4228/// (PF=1 ⟺ unordered, i.e. a NaN operand, which must yield false for `==`
4229/// and true for `!=`) — bit-identical to the reference's `a == b`.
4230fn emit_feq(g: &mut Gen, dst: Slot, lhs: Slot, rhs: Slot, neg: bool) {
4231    let xa = g.foperand(lhs, FS0);
4232    let xb = g.foperand(rhs, FS1);
4233    g.asm.ucomisd_rr(xa, xb);
4234    if neg {
4235        // neq = ZF==0 || PF==1 (`setne` | `setp`).
4236        g.asm.setcc_movzx(Cond::Ne, S0);
4237        g.asm.setcc_movzx(Cond::ParityEven, S1);
4238        g.asm.or_rr(S0, S1);
4239    } else {
4240        // eq = ZF==1 && PF==0 (`sete` & `setnp`).
4241        g.asm.setcc_movzx(Cond::Eq, S0);
4242        g.asm.setcc_movzx(Cond::ParityOdd, S1);
4243        g.asm.and_rr(S0, S1);
4244    }
4245    g.store(dst, S0);
4246}
4247
4248/// Fused f64 compare-and-branch: transfer to `target` when `cmp(lhs, rhs)` is
4249/// FALSE — which, under IEEE, includes every NaN-unordered comparison (the
4250/// reference's `BranchF`). Ordering compares use `ucomisd` + the negated
4251/// unsigned jcc; epsilon (Eq/NotEq) reuses the `emit_feq` value then tests it.
4252fn emit_branchf(g: &mut Gen, cmp: Cmp, lhs: Slot, rhs: Slot, target: LabelId) {
4253    match cmp {
4254        Cmp::Lt | Cmp::Gt | Cmp::LtEq | Cmp::GtEq => {
4255            // For each, pick (a, b) and the "FALSE" unsigned jcc. The branch is
4256            // taken when the comparison is FALSE; NaN (unordered: CF=ZF=1) must
4257            // TAKE the false branch.
4258            //   a > b  true = seta(CF=0&&ZF=0) ; false = jbe(CF=1||ZF=1)
4259            //   a >= b true = setae(CF=0)       ; false = jb (CF=1)
4260            let (a, b, false_cond) = match cmp {
4261                Cmp::Gt => (lhs, rhs, Cond::BeU),  // !(a>b)
4262                Cmp::GtEq => (lhs, rhs, Cond::BU), // !(a>=b)
4263                Cmp::Lt => (rhs, lhs, Cond::BeU),  // a<b ≡ b>a ; !(b>a)
4264                Cmp::LtEq => (rhs, lhs, Cond::BU), // a<=b ≡ b>=a ; !(b>=a)
4265                _ => unreachable!(),
4266            };
4267            let xa = g.foperand(a, FS0);
4268            let xb = g.foperand(b, FS1);
4269            g.asm.ucomisd_rr(xa, xb);
4270            g.asm.jcc(false_cond, target);
4271        }
4272        Cmp::Eq | Cmp::NotEq => {
4273            // IEEE (in)equality; branch when the comparison is FALSE.
4274            let xa = g.foperand(lhs, FS0);
4275            let xb = g.foperand(rhs, FS1);
4276            g.asm.ucomisd_rr(xa, xb);
4277            if matches!(cmp, Cmp::Eq) {
4278                // truth = ordered-equal; FALSE = ZF==0 || PF==1 — two direct
4279                // jumps, and a NaN (PF=1) takes the false branch.
4280                g.asm.jcc(Cond::Ne, target);
4281                g.asm.jcc(Cond::ParityEven, target);
4282                return;
4283            }
4284            // truth = (a != b); FALSE = ordered-equal (ZF==1 && PF==0).
4285            // Materialize the IEEE neq exactly like `emit_feq`, then branch
4286            // on zero.
4287            g.asm.setcc_movzx(Cond::Ne, S0);
4288            g.asm.setcc_movzx(Cond::ParityEven, S1);
4289            g.asm.or_rr(S0, S1);
4290            g.asm.test_rr(S0, S0);
4291            g.asm.jcc(Cond::Eq, target); // truth == 0 → jump (false branch)
4292        }
4293    }
4294}
4295
4296#[derive(Clone, Copy)]
4297enum ShiftKind {
4298    Left,
4299    Right,
4300}
4301
4302/// `dst = lhs <</>> rhs` with the kernel's shift spec: the count is the low
4303/// bits of `rhs` (x86 masks the count mod 64 for 64-bit operands, matching
4304/// `wrapping_shl`/`wrapping_shr`'s `as u32 % 64`). The value goes in S0, the
4305/// count in `cl` (rcx).
4306fn emit_shift(g: &mut Gen, dst: Slot, lhs: Slot, rhs: Slot, kind: ShiftKind) {
4307    g.load(S0, lhs);
4308    g.load(SC, rhs);
4309    match kind {
4310        ShiftKind::Left => g.asm.shl_cl(S0),
4311        ShiftKind::Right => g.asm.sar_cl(S0),
4312    }
4313    g.store(dst, S0);
4314}
4315
4316/// `dst = lhs / 2^k` (signed, round toward zero) via the sign-correcting shift,
4317/// bit-exact with `reference_eval`'s `DivPow2`:
4318/// `(x + ((x >> 63) & ((1<<k)-1))) >> k`.
4319fn emit_div_pow2(g: &mut Gen, dst: Slot, lhs: Slot, k: u32) {
4320    g.load(S0, lhs); // x
4321    // S1 = x >> 63 (sign mask, arithmetic).
4322    g.asm.mov_rr(S1, S0);
4323    g.asm.mov_ri(SC, 63);
4324    g.asm.sar_cl(S1);
4325    // S1 &= (1<<k) - 1
4326    let mask = (1i64 << k) - 1;
4327    g.asm.mov_ri(SC, mask);
4328    g.asm.and_rr(S1, SC);
4329    // S0 = x + S1
4330    g.asm.add_rr(S0, S1);
4331    // S0 >>= k (arithmetic)
4332    g.asm.mov_ri(SC, k as i64);
4333    g.asm.sar_cl(S0);
4334    g.store(dst, S0);
4335}
4336
4337/// `dst = lhs / c` (`mul_back == 0`) or `dst = lhs % c` (`mul_back == c`) via the
4338/// Granlund–Montgomery / libdivide UNSIGNED magic reciprocal — bit-exact with
4339/// `reference_eval`'s `MagicDivU` and the VM's `magic_eval`. Emitted only for a
4340/// proven non-negative dividend, so the unsigned arithmetic equals the signed
4341/// truncating result.
4342///
4343/// Register choreography: RAX/RDX/RCX (S0/S1/SC) are reserved scratch — no slot
4344/// is ever resident there, so `mul`'s implicit RDX:RAX clobber is safe. The
4345/// dividend's home slot (`lhs`) is never written, so it is RELOADED whenever the
4346/// add-fixup or the remainder needs `x` again.
4347///
4348/// Quotient: `q_hi = mulhi_u(magic, x)`; with the add-marker path
4349/// `q = ((x - q_hi) >> 1 + q_hi) >> shift`, else `q = q_hi >> shift`. The
4350/// quotient lands in S0 (RAX). Remainder: `r = x - q*c` (S0 ← x - q*c).
4351fn emit_magic_div(g: &mut Gen, dst: Slot, lhs: Slot, magic: u64, more: u8, mul_back: i64) {
4352    const SHIFT_MASK: u8 = 0x3F;
4353    const ADD_MARKER: u8 = 0x40;
4354    const SHIFT_PATH: u8 = 0x80;
4355    let shift = (more & SHIFT_MASK) as u8;
4356
4357    if more & SHIFT_PATH != 0 {
4358        // Pure power-of-two path (never emitted by the compiler for the non-pow2
4359        // gate, but executed identically here for completeness): q = x >> shift,
4360        // LOGICAL — sound because x is non-negative.
4361        g.load(S0, lhs);
4362        if shift != 0 {
4363            g.asm.shr_ri(S0, shift);
4364        }
4365        finish_magic(g, dst, lhs, mul_back);
4366        return;
4367    }
4368
4369    // q_hi = high 64 bits of the UNSIGNED product magic * x.
4370    g.asm.mov_ri(SC, magic as i64); // RCX = magic
4371    g.load(S0, lhs); // RAX = x
4372    g.asm.mul_r(SC); // RDX:RAX = x * magic; RDX = q_hi
4373
4374    if more & ADD_MARKER != 0 {
4375        // q = ((x - q_hi) >> 1).wrapping_add(q_hi) >> shift.
4376        g.load(SC, lhs); // RCX = x (reload; lhs home is untouched)
4377        g.asm.sub_rr(SC, S1); // RCX = x - q_hi
4378        g.asm.shr_ri(SC, 1); // RCX = (x - q_hi) >> 1  (logical)
4379        g.asm.add_rr(SC, S1); // RCX = t
4380        if shift != 0 {
4381            g.asm.shr_ri(SC, shift); // RCX = q
4382        }
4383        g.asm.mov_rr(S0, SC); // RAX = q
4384    } else {
4385        // q = q_hi >> shift.
4386        if shift != 0 {
4387            g.asm.shr_ri(S1, shift); // RDX = q
4388        }
4389        g.asm.mov_rr(S0, S1); // RAX = q
4390    }
4391    finish_magic(g, dst, lhs, mul_back);
4392}
4393
4394/// Common tail: the quotient is in S0 (RAX). For division store it; for modulo
4395/// compute `r = x - q*c` (`mul_back == c`) and store that.
4396fn finish_magic(g: &mut Gen, dst: Slot, lhs: Slot, mul_back: i64) {
4397    if mul_back == 0 {
4398        g.store(dst, S0); // quotient
4399    } else {
4400        // r = x - q*c.  RAX holds q.
4401        g.asm.mov_ri(SC, mul_back); // RCX = c
4402        g.asm.imul_rr(S0, SC); // RAX = q * c (low 64 bits suffice)
4403        g.load(SC, lhs); // RCX = x (reload)
4404        g.asm.sub_rr(SC, S0); // RCX = x - q*c
4405        g.store(dst, SC); // remainder
4406    }
4407}
4408
4409/// `dst = lhs / rhs` (quotient) or `lhs % rhs` (remainder), with a zero-divisor
4410/// SIDE-EXIT before any effect and the kernel's `MIN op -1` overflow handling
4411/// (no `#DE` trap): if `rhs == -1` then quotient = `0 - lhs` (== MIN for MIN)
4412/// and remainder = 0.
4413fn emit_div_mod(g: &mut Gen, dst: Slot, lhs: Slot, rhs: Slot, is_div: bool, dl: LabelId) {
4414    // Load divisor into S1 (rdx is overwritten by cqo, so keep divisor in a
4415    // register that survives; use SC=rcx for the divisor instead).
4416    g.load(SC, rhs); // divisor in rcx
4417    // Zero check: side-exit if rcx == 0.
4418    g.asm.test_rr(SC, SC);
4419    g.asm.jcc(Cond::Eq, dl);
4420    // Overflow guard: if divisor == -1, avoid #DE. quotient = -lhs, rem = 0.
4421    let neg_one = g.asm.new_label();
4422    let done = g.asm.new_label();
4423    g.asm.mov_ri(S1, -1);
4424    g.asm.cmp_rr(SC, S1); // compare divisor with -1
4425    g.asm.jcc(Cond::Eq, neg_one);
4426    // Normal path: rax = lhs; cqo; idiv rcx.
4427    g.load(S0, lhs);
4428    g.asm.cqo();
4429    g.asm.idiv_r(SC); // quotient -> rax, remainder -> rdx
4430    if is_div {
4431        // result already in rax (S0)
4432    } else {
4433        g.asm.mov_rr(S0, S1); // remainder rdx -> rax
4434    }
4435    g.asm.jmp(done);
4436    // divisor == -1 path.
4437    g.asm.bind(neg_one);
4438    if is_div {
4439        // quotient = -lhs — EXCEPT `i64::MIN`, whose negation overflows:
4440        // side-exit so the exact tier recomputes (and promotes to BigInt).
4441        g.load(S0, lhs);
4442        g.asm.mov_ri(S1, i64::MIN);
4443        g.asm.cmp_rr(S0, S1);
4444        g.asm.jcc(Cond::Eq, dl);
4445        g.asm.neg_r(S0);
4446    } else {
4447        // remainder = 0
4448        g.asm.mov_ri(S0, 0);
4449    }
4450    g.asm.bind(done);
4451    g.store(dst, S0);
4452}
4453
4454#[cfg(test)]
4455mod tests {
4456    use super::*;
4457    use crate::jit::{reference_eval, ChainOutcome};
4458    use std::sync::atomic::Ordering;
4459    use std::sync::Arc;
4460
4461    fn run(ops: &[MicroOp], frame: &[i64]) -> (ChainOutcome, Vec<i64>) {
4462        let status = Arc::new(AtomicI64::new(0));
4463        let chain = compile_region_regalloc(ops, Some(status)).expect("supported region compiles");
4464        let mut f = frame.to_vec();
4465        let out = chain.run_with_frame(&mut f);
4466        (out, f)
4467    }
4468
4469    #[test]
4470    fn live_intervals_span_first_to_last_touch() {
4471        // LoadConst s0; Move s1<-s0; Return s1.
4472        let ops = vec![
4473            MicroOp::LoadConst { dst: 0, value: 7 },
4474            MicroOp::Move { dst: 1, src: 0 },
4475            MicroOp::Return { src: 1 },
4476        ];
4477        let iv = live_intervals(&ops, 1);
4478        assert_eq!(iv, vec![
4479            LiveInterval { slot: 0, start: 0, end: 1 }, // defined @0, last read @1
4480            LiveInterval { slot: 1, start: 1, end: 2 }, // defined @1, returned @2
4481        ]);
4482    }
4483
4484    #[test]
4485    fn linscan_reuses_a_register_across_disjoint_ranges() {
4486        // Five float temporaries with strictly DISJOINT ranges. The whole-region
4487        // per-slot model would give each its own home (spilling 4 of 5 with a
4488        // single register); linear scan reuses the one register for all → 0 spill.
4489        let class = vec![Class::Float; 5];
4490        let iv: Vec<LiveInterval> = (0..5)
4491            .map(|s| LiveInterval { slot: s, start: (s as usize) * 2, end: (s as usize) * 2 + 1 })
4492            .collect();
4493        assert_eq!(linscan_spills(&iv, &class, Class::Float, 1), 0, "disjoint ranges share 1 reg");
4494        assert_eq!(linscan_spills(&iv, &class, Class::Float, 0), 5, "0 regs spills all");
4495    }
4496
4497    #[test]
4498    fn linscan_spills_only_the_overlap_excess() {
4499        // Three fully-overlapping float intervals need 3 registers; with 2 exactly
4500        // one spills, with 3 none. This is the pressure the per-slot model can't
4501        // see — it would spill by rank, not by overlap.
4502        let class = vec![Class::Float; 3];
4503        let iv: Vec<LiveInterval> = (0..3).map(|s| LiveInterval { slot: s, start: 0, end: 9 }).collect();
4504        assert_eq!(linscan_spills(&iv, &class, Class::Float, 3), 0);
4505        assert_eq!(linscan_spills(&iv, &class, Class::Float, 2), 1);
4506        assert_eq!(linscan_spills(&iv, &class, Class::Float, 1), 2);
4507    }
4508
4509    /// A float accumulation loop: `acc` is loop-carried; `x` and `y` are disjoint
4510    /// within-iteration float temps. Returns (ops, max_slot). slots: 0=i 1=N
4511    /// 2=acc(f) 3=x(f) 4=y(f) 5=one.
4512    fn float_loop_two_temps() -> (Vec<MicroOp>, usize) {
4513        let ops = vec![
4514            MicroOp::Branch { cmp: Cmp::Lt, lhs: 0, rhs: 1, target: 8 }, // loop guard (ctrl + target@8)
4515            MicroOp::IntToFloat { dst: 3, src: 0 },                      // x = (f)i
4516            MicroOp::AddF { dst: 2, lhs: 2, rhs: 3 },                    // acc += x   (x dead after)
4517            MicroOp::IntToFloat { dst: 4, src: 0 },                      // y = (f)i
4518            MicroOp::AddF { dst: 2, lhs: 2, rhs: 4 },                    // acc += y   (y dead after)
4519            MicroOp::LoadConst { dst: 5, value: 1 },
4520            MicroOp::Add { dst: 0, lhs: 0, rhs: 5 }, // i += 1
4521            MicroOp::Jump { target: 0 },             // back-edge (ctrl + target@0)
4522            MicroOp::Return { src: 2 },
4523        ];
4524        (ops, 5)
4525    }
4526
4527    #[test]
4528    fn live_ranges_are_loop_aware() {
4529        let (ops, max_slot) = float_loop_two_temps();
4530        let r = live_ranges(&ops, max_slot);
4531        // acc (loop-carried) spans the whole loop, returned at op 8.
4532        assert_eq!(r[2], Some((0, 8)), "carried acc must span the loop");
4533        // x / y are within-iteration temps with disjoint ranges.
4534        assert_eq!(r[3], Some((1, 2)));
4535        assert_eq!(r[4], Some((3, 4)));
4536    }
4537
4538    #[test]
4539    fn linscan_prefers_fresh_then_reuses_under_pressure() {
4540        let (ops, max_slot) = float_loop_two_temps();
4541        let class = classify_slots(&ops, max_slot);
4542        let ranges = live_ranges(&ops, max_slot);
4543        // AMPLE XMM pool: disjoint temps x and y each get their OWN register — no
4544        // reuse, hence no spill-at-end overhead when the region already fits.
4545        let ample = linscan_assign(&ops, max_slot, &class, &CALLER_SAVED, &FLOAT_REGS);
4546        assert_ne!(ample[3], Loc::Frame, "temp must get a register");
4547        assert_ne!(ample[3], ample[4], "with registers to spare, disjoint temps do NOT share");
4548        // PRESSURE: only acc + one scratch XMM. acc (loop-carried) overlaps both
4549        // temps and keeps its register; x and y must then SHARE the single scratch.
4550        let tight = linscan_assign(&ops, max_slot, &class, &CALLER_SAVED, &[Xmm::Xmm0, Xmm::Xmm1]);
4551        assert_eq!(tight[3], tight[4], "under pressure, disjoint gap-clear temps reuse one register");
4552        assert_ne!(tight[2], tight[3], "carried acc cannot share a live temp's register");
4553        // INVARIANT (both regimes): no two slots with OVERLAPPING ranges share a register.
4554        for assign in [&ample, &tight] {
4555            for a in 0..=max_slot {
4556                for b in (a + 1)..=max_slot {
4557                    if let (Some((sa, ea)), Some((sb, eb))) = (ranges[a], ranges[b]) {
4558                        let overlap = sa <= eb && sb <= ea;
4559                        if overlap && assign[a] != Loc::Frame {
4560                            assert_ne!(assign[a], assign[b], "overlapping slots {a},{b} share a register");
4561                        }
4562                    }
4563                }
4564            }
4565        }
4566    }
4567
4568    #[test]
4569    fn linscan_no_reuse_across_a_control_point() {
4570        // Two disjoint int temps separated by a jump TARGET: reuse would cross a
4571        // control join, so they must NOT share a register.
4572        // 0:LoadConst t0  1:Add a=t0+t0 (t0 dead@1)  2:Jump 4  3:(unreached)  4:(target) LoadConst t1  5:Add b=t1+t1  6:Return b
4573        let ops = vec![
4574            MicroOp::LoadConst { dst: 0, value: 1 }, // t0 @0
4575            MicroOp::Add { dst: 1, lhs: 0, rhs: 0 }, // a=t0+t0 ; t0 dead after @1
4576            MicroOp::Jump { target: 4 },             // ctrl; target 4 is a jump target
4577            MicroOp::LoadConst { dst: 9, value: 0 }, // filler (unreached) keeps idx 3 occupied
4578            MicroOp::LoadConst { dst: 2, value: 2 }, // t1 @4 (a jump target — control join)
4579            MicroOp::Add { dst: 3, lhs: 2, rhs: 2 }, // b=t1+t1
4580            MicroOp::Return { src: 3 },
4581        ];
4582        let max_slot = 9;
4583        let class = classify_slots(&ops, max_slot);
4584        let assign = linscan_assign(&ops, max_slot, &class, &CALLER_SAVED, &FLOAT_REGS);
4585        // t0 (range ~[0,1]) freed before t1 (range ~[4,5]) but op 4 is a jump
4586        // target in the gap → t1 must take a FRESH register, not t0's.
4587        assert_ne!(assign[0], assign[2], "reuse must not cross a control point");
4588    }
4589
4590    #[test]
4591    fn straightline_arith_matches_reference() {
4592        let ops = vec![
4593            MicroOp::Add { dst: 3, lhs: 0, rhs: 1 },
4594            MicroOp::Mul { dst: 4, lhs: 3, rhs: 2 },
4595            MicroOp::Sub { dst: 5, lhs: 4, rhs: 0 },
4596            MicroOp::Return { src: 5 },
4597        ];
4598        let frame = vec![7i64, 11, 13, 0, 0, 0];
4599        let expected = reference_eval(&ops, &mut frame.clone(), 1000).unwrap();
4600        let (out, _) = run(&ops, &frame);
4601        assert_eq!(out, ChainOutcome::Return(expected));
4602    }
4603
4604    #[test]
4605    fn counting_loop_matches_reference() {
4606        // sum 0..N via a branch-back loop.
4607        let ops = vec![
4608            MicroOp::Branch { cmp: Cmp::Lt, lhs: 0, rhs: 2, target: 5 }, // while i<N
4609            MicroOp::Add { dst: 1, lhs: 1, rhs: 0 },                     // acc += i
4610            MicroOp::LoadConst { dst: 3, value: 1 },
4611            MicroOp::Add { dst: 0, lhs: 0, rhs: 3 }, // i += 1
4612            MicroOp::Jump { target: 0 },
4613            MicroOp::Return { src: 1 },
4614        ];
4615        let frame = vec![0i64, 0, 1000, 0];
4616        let expected = reference_eval(&ops, &mut frame.clone(), 10_000_000).unwrap();
4617        let (out, _) = run(&ops, &frame);
4618        assert_eq!(out, ChainOutcome::Return(expected));
4619        assert_eq!(out, ChainOutcome::Return(499_500));
4620    }
4621
4622    #[test]
4623    fn div_by_zero_side_exits() {
4624        let status = Arc::new(AtomicI64::new(0));
4625        let ops = vec![
4626            MicroOp::Div { dst: 2, lhs: 0, rhs: 1 },
4627            MicroOp::Return { src: 2 },
4628        ];
4629        let chain = compile_region_regalloc(&ops, Some(status)).unwrap();
4630        let mut frame = vec![100i64, 0, 0];
4631        let out = chain.run_with_frame(&mut frame);
4632        assert!(out.is_deopt(), "div by zero must side-exit, got {out:?}");
4633    }
4634
4635    #[test]
4636    fn div_min_neg_one_side_exits() {
4637        // Exact arithmetic: the overflowing quotient i64::MIN / -1 side-exits
4638        // in BOTH the reference and the machine code (deopt → promotion),
4639        // never wraps — and it must not trap the process.
4640        let ops = vec![
4641            MicroOp::Div { dst: 2, lhs: 0, rhs: 1 },
4642            MicroOp::Return { src: 2 },
4643        ];
4644        let frame = vec![i64::MIN, -1, 0];
4645        assert!(
4646            reference_eval(&ops, &mut frame.clone(), 1000).is_none(),
4647            "reference must side-exit MIN / -1"
4648        );
4649        let (out, _) = run(&ops, &frame);
4650        assert!(out.is_deopt(), "machine code must side-exit MIN / -1, got {out:?}");
4651    }
4652
4653    #[test]
4654    fn mod_min_neg_one_zero() {
4655        let ops = vec![
4656            MicroOp::Mod { dst: 2, lhs: 0, rhs: 1 },
4657            MicroOp::Return { src: 2 },
4658        ];
4659        let frame = vec![i64::MIN, -1, 0];
4660        let expected = reference_eval(&ops, &mut frame.clone(), 1000).unwrap();
4661        let (out, _) = run(&ops, &frame);
4662        assert_eq!(out, ChainOutcome::Return(expected)); // MIN % -1 == 0
4663        assert_eq!(out, ChainOutcome::Return(0));
4664    }
4665
4666    #[test]
4667    fn unsupported_op_returns_none() {
4668        // A `MapGet` (hash-map probe via a runtime helper) is outside the
4669        // supported subset, so the region declines and the caller falls back to
4670        // the per-piece stencil tier. (Byte / `Seq of Bool` array loads/stores
4671        // ARE now supported — see `byte_array_*` — so they no longer decline.)
4672        let ops = vec![
4673            MicroOp::MapGet { dst: 1, key: 0, map_slot: 2, helper_addr: 0 },
4674            MicroOp::Return { src: 1 },
4675        ];
4676        assert!(compile_region_regalloc(&ops, None).is_none());
4677    }
4678
4679    // =================================================================
4680    // WAVE 25: the INLINED `ArrPush` fast path. Each push lowers to an
4681    // INLINE `len < cap ? buffer[len++] = v` test that calls the runtime
4682    // helper ONLY on the (cold) realloc boundary. These forge-level tests
4683    // drive a REAL `Vec` through the compiled region with an instrumented
4684    // helper, proving (1) bit-identical contents to `Vec::push` across the
4685    // fast path AND the realloc cold path, and (2) that the per-iteration
4686    // call vanished on the fast path (the helper's call counter).
4687    // =================================================================
4688
4689    /// Call counter for the instrumented test push helper: a fast-path push
4690    /// (`len < cap`) must NOT touch the helper, so this counts only the realloc
4691    /// cold-path entries.
4692    static TEST_PUSH_CALLS: AtomicI64 = AtomicI64::new(0);
4693
4694    /// A drop-in stand-in for `logos_rt_push_i64` with the SAME SysV ABI and the
4695    /// SAME `Vec::push` + ptr/len-refresh semantics, plus a call counter. The
4696    /// inline fast path must reproduce its effect EXACTLY when `len < cap`.
4697    ///
4698    /// # Safety
4699    /// `frame` slots must hold a live `*mut Vec<i64>` (vec_slot) and the mirrored
4700    /// ptr/len cells, exactly as the region pin convention requires.
4701    unsafe extern "C" fn test_push_i64(
4702        frame: *mut i64,
4703        vec_slot: i64,
4704        ptr_slot: i64,
4705        len_slot: i64,
4706        value: i64,
4707    ) {
4708        TEST_PUSH_CALLS.fetch_add(1, Ordering::Relaxed);
4709        let vec = *frame.add(vec_slot as usize) as *mut Vec<i64>;
4710        (*vec).push(value);
4711        *frame.add(ptr_slot as usize) = (*vec).as_mut_ptr() as i64;
4712        *frame.add(len_slot as usize) = (*vec).len() as i64;
4713    }
4714
4715    /// Build a region that pushes `i` for `i` in `[0, n)` into a pinned Int list,
4716    /// then returns the final length. Frame layout:
4717    ///   0 = i (induction), 1 = n (bound), 2 = const 1,
4718    ///   8 = vec handle, 9 = buffer ptr, 10 = length.
4719    fn push_loop_ops(helper_addr: i64) -> Vec<MicroOp> {
4720        vec![
4721            MicroOp::Branch { cmp: Cmp::Lt, lhs: 0, rhs: 1, target: 5 }, // while i < n
4722            MicroOp::ArrPush {
4723                src: 0,
4724                vec_slot: 8,
4725                ptr_slot: 9,
4726                len_slot: 10,
4727                helper_addr,
4728                byte: false,
4729                narrow32: false,
4730            },
4731            MicroOp::LoadConst { dst: 2, value: 1 },
4732            MicroOp::Add { dst: 0, lhs: 0, rhs: 2 }, // i += 1
4733            MicroOp::Jump { target: 0 },
4734            MicroOp::Return { src: 10 }, // final length
4735        ]
4736    }
4737
4738    /// RED: pushes that ALL fit the pre-allocated capacity touch the runtime
4739    /// helper ZERO times — every push took the inline fast path — and the buffer
4740    /// holds exactly `Vec::push`'s result (same contents, same final pointer,
4741    /// since no realloc moved it).
4742    #[test]
4743    fn inline_push_fast_path_never_calls_helper_within_capacity() {
4744        let n = 5000i64;
4745        let mut vec: Vec<i64> = Vec::with_capacity(n as usize);
4746        let vec_ptr = &mut vec as *mut Vec<i64>;
4747        let buf_before = vec.as_ptr() as i64;
4748
4749        let ops = push_loop_ops(test_push_i64 as usize as i64);
4750        let status = Arc::new(AtomicI64::new(0));
4751        let chain = compile_region_regalloc(&ops, Some(status)).expect("push region compiles");
4752
4753        // frame[8] = vec handle, [9] = buffer ptr, [10] = length (all 0 initially).
4754        let mut frame = vec![0i64; 11];
4755        frame[1] = n;
4756        frame[8] = vec_ptr as i64;
4757        frame[9] = buf_before;
4758        frame[10] = 0;
4759
4760        TEST_PUSH_CALLS.store(0, Ordering::Relaxed);
4761        let out = chain.run_with_frame(&mut frame);
4762
4763        assert_eq!(out, ChainOutcome::Return(n), "final length must be n");
4764        assert_eq!(
4765            TEST_PUSH_CALLS.load(Ordering::Relaxed),
4766            0,
4767            "every push fit within capacity — the inline fast path must call the helper ZERO times"
4768        );
4769        // Bit-identical to Vec::push: contents and (no-realloc) pointer.
4770        assert_eq!(vec.len(), n as usize, "real Vec length");
4771        assert_eq!(vec.as_ptr() as i64, buf_before, "no realloc moved the buffer");
4772        for (i, &v) in vec.iter().enumerate() {
4773            assert_eq!(v, i as i64, "buffer[{i}] must be the pushed value");
4774        }
4775        // The frame's mirrored len/ptr stay coherent with the real Vec.
4776        assert_eq!(frame[10], n, "mirrored length");
4777        assert_eq!(frame[9], buf_before, "mirrored pointer unchanged (no realloc)");
4778    }
4779
4780    /// RED: pushes that GROW past the initial capacity stay bit-identical to
4781    /// `Vec::push` — the cold helper path reallocates and refreshes ptr/len, and
4782    /// the inline path bumps len in place between reallocs. The helper is called
4783    /// only on the realloc boundaries (far fewer than `n` times), proving the
4784    /// per-iteration call is gone.
4785    #[test]
4786    fn inline_push_realloc_cold_path_matches_vec_push() {
4787        let n = 4000i64;
4788        // Start with a TINY capacity so the cold realloc path fires repeatedly.
4789        let mut vec: Vec<i64> = Vec::with_capacity(1);
4790        let vec_ptr = &mut vec as *mut Vec<i64>;
4791
4792        // The reference: a plain Vec::push of the same sequence from cap 1.
4793        let mut reference: Vec<i64> = Vec::with_capacity(1);
4794        for i in 0..n {
4795            reference.push(i);
4796        }
4797
4798        let ops = push_loop_ops(test_push_i64 as usize as i64);
4799        let status = Arc::new(AtomicI64::new(0));
4800        let chain = compile_region_regalloc(&ops, Some(status)).expect("push region compiles");
4801
4802        let mut frame = vec![0i64; 11];
4803        frame[1] = n;
4804        frame[8] = vec_ptr as i64;
4805        frame[9] = vec.as_ptr() as i64;
4806        frame[10] = 0;
4807
4808        TEST_PUSH_CALLS.store(0, Ordering::Relaxed);
4809        let out = chain.run_with_frame(&mut frame);
4810
4811        assert_eq!(out, ChainOutcome::Return(n), "final length must be n");
4812        assert_eq!(vec, reference, "compiled push must be bit-identical to Vec::push");
4813        assert_eq!(frame[10], n, "mirrored length matches");
4814        assert_eq!(
4815            frame[9],
4816            vec.as_ptr() as i64,
4817            "mirrored pointer matches the (post-realloc) buffer"
4818        );
4819        let calls = TEST_PUSH_CALLS.load(Ordering::Relaxed);
4820        // Geometric growth from cap 1: O(log n) reallocs, vastly fewer than n.
4821        assert!(
4822            calls < n / 2,
4823            "the helper must fire only on realloc boundaries (got {calls} of {n} pushes)"
4824        );
4825        assert!(calls >= 1, "at least one realloc must have occurred");
4826    }
4827
4828    /// RED: a `Seq of Bool` (1-byte element) push stores the BOOLEAN
4829    /// NORMALIZATION `(v != 0) as u8` inline — bit-identical to
4830    /// `logos_rt_push_bool`'s `(*vec).push(value != 0)`.
4831    #[test]
4832    fn inline_push_byte_normalizes_like_helper() {
4833        unsafe extern "C" fn test_push_bool(
4834            frame: *mut i64,
4835            vec_slot: i64,
4836            ptr_slot: i64,
4837            len_slot: i64,
4838            value: i64,
4839        ) {
4840            let vec = *frame.add(vec_slot as usize) as *mut Vec<bool>;
4841            (*vec).push(value != 0);
4842            *frame.add(ptr_slot as usize) = (*vec).as_mut_ptr() as i64;
4843            *frame.add(len_slot as usize) = (*vec).len() as i64;
4844        }
4845
4846        let n = 300i64;
4847        let mut vec: Vec<bool> = Vec::with_capacity(n as usize);
4848        let vec_ptr = &mut vec as *mut Vec<bool>;
4849        let buf_before = vec.as_ptr() as i64;
4850
4851        // Push `i % 3` (0, 1, 2, 0, 1, 2, …): zero stores false, nonzero true.
4852        let ops = vec![
4853            MicroOp::Branch { cmp: Cmp::Lt, lhs: 0, rhs: 1, target: 7 }, // while i < n
4854            MicroOp::LoadConst { dst: 3, value: 3 },
4855            MicroOp::Mod { dst: 4, lhs: 0, rhs: 3 }, // v = i % 3
4856            MicroOp::ArrPush {
4857                src: 4,
4858                vec_slot: 8,
4859                ptr_slot: 9,
4860                len_slot: 10,
4861                helper_addr: test_push_bool as usize as i64,
4862                byte: true,
4863                narrow32: false,
4864            },
4865            MicroOp::LoadConst { dst: 2, value: 1 },
4866            MicroOp::Add { dst: 0, lhs: 0, rhs: 2 }, // i += 1
4867            MicroOp::Jump { target: 0 },
4868            MicroOp::Return { src: 10 },
4869        ];
4870        let status = Arc::new(AtomicI64::new(0));
4871        let chain = compile_region_regalloc(&ops, Some(status)).expect("byte push region compiles");
4872
4873        let mut frame = vec![0i64; 11];
4874        frame[1] = n;
4875        frame[8] = vec_ptr as i64;
4876        frame[9] = buf_before;
4877        frame[10] = 0;
4878        let out = chain.run_with_frame(&mut frame);
4879
4880        assert_eq!(out, ChainOutcome::Return(n));
4881        let reference: Vec<bool> = (0..n).map(|i| (i % 3) != 0).collect();
4882        assert_eq!(vec, reference, "byte push must normalize (v != 0) like the helper");
4883        assert_eq!(vec.as_ptr() as i64, buf_before, "no realloc within capacity");
4884    }
4885
4886    /// RED: an XMM-resident (float-list) push bit-copies the value to the buffer
4887    /// inline — the raw 8 bytes travel identically to the helper's
4888    /// `f64::from_bits` round-trip.
4889    #[test]
4890    fn inline_push_float_value_bitcopies() {
4891        unsafe extern "C" fn test_push_f64(
4892            frame: *mut i64,
4893            vec_slot: i64,
4894            ptr_slot: i64,
4895            len_slot: i64,
4896            value: i64,
4897        ) {
4898            let vec = *frame.add(vec_slot as usize) as *mut Vec<f64>;
4899            (*vec).push(f64::from_bits(value as u64));
4900            *frame.add(ptr_slot as usize) = (*vec).as_mut_ptr() as i64;
4901            *frame.add(len_slot as usize) = (*vec).len() as i64;
4902        }
4903
4904        let n = 200i64;
4905        let mut vec: Vec<f64> = Vec::with_capacity(n as usize);
4906        let vec_ptr = &mut vec as *mut Vec<f64>;
4907        let buf_before = vec.as_ptr() as i64;
4908
4909        // src is a FLOAT slot produced by IntToFloat (lands in XMM): push i as f64.
4910        let ops = vec![
4911            MicroOp::Branch { cmp: Cmp::Lt, lhs: 0, rhs: 1, target: 6 }, // while i < n
4912            MicroOp::IntToFloat { dst: 4, src: 0 }, // v = i as f64 (XMM-resident)
4913            MicroOp::ArrPush {
4914                src: 4,
4915                vec_slot: 8,
4916                ptr_slot: 9,
4917                len_slot: 10,
4918                helper_addr: test_push_f64 as usize as i64,
4919                byte: false,
4920                narrow32: false,
4921            },
4922            MicroOp::LoadConst { dst: 2, value: 1 },
4923            MicroOp::Add { dst: 0, lhs: 0, rhs: 2 }, // i += 1
4924            MicroOp::Jump { target: 0 },
4925            MicroOp::Return { src: 10 },
4926        ];
4927        let status = Arc::new(AtomicI64::new(0));
4928        let chain = compile_region_regalloc(&ops, Some(status)).expect("float push region compiles");
4929
4930        let mut frame = vec![0i64; 11];
4931        frame[1] = n;
4932        frame[8] = vec_ptr as i64;
4933        frame[9] = buf_before;
4934        frame[10] = 0;
4935        let out = chain.run_with_frame(&mut frame);
4936
4937        assert_eq!(out, ChainOutcome::Return(n));
4938        let reference: Vec<f64> = (0..n).map(|i| i as f64).collect();
4939        assert_eq!(vec, reference, "float push must bit-copy the value like the helper");
4940        assert_eq!(vec.as_ptr() as i64, buf_before, "no realloc within capacity");
4941    }
4942
4943    fn lcg(state: &mut u64) -> u64 {
4944        *state = state
4945            .wrapping_mul(6364136223846793005)
4946            .wrapping_add(1442695040888963407);
4947        *state >> 33
4948    }
4949
4950    /// Exhaustive differential: every random straight-line program over the
4951    /// supported families must equal the (location-independent) reference, at
4952    /// EVERY slot count — small (all resident) and large (forces spills).
4953    #[test]
4954    fn random_straightline_matches_reference_all_pressures() {
4955        for slots in [3u16, 6, 10, 20] {
4956            for seed in 1..=80u64 {
4957                let mut s = seed.wrapping_mul(2654435761);
4958                let len = 8 + (lcg(&mut s) % 24) as usize;
4959                let mut ops = Vec::with_capacity(len + 1);
4960                for _ in 0..len {
4961                    let dst = (lcg(&mut s) % slots as u64) as u16;
4962                    let lhs = (lcg(&mut s) % slots as u64) as u16;
4963                    let rhs = (lcg(&mut s) % slots as u64) as u16;
4964                    let op = match lcg(&mut s) % 13 {
4965                        0 => MicroOp::Add { dst, lhs, rhs },
4966                        1 => MicroOp::Sub { dst, lhs, rhs },
4967                        2 => MicroOp::Mul { dst, lhs, rhs },
4968                        3 => MicroOp::BitAnd { dst, lhs, rhs },
4969                        4 => MicroOp::BitOr { dst, lhs, rhs },
4970                        5 => MicroOp::BitXor { dst, lhs, rhs },
4971                        6 => MicroOp::Lt { dst, lhs, rhs },
4972                        7 => MicroOp::Gt { dst, lhs, rhs },
4973                        8 => MicroOp::LtEq { dst, lhs, rhs },
4974                        9 => MicroOp::Eq { dst, lhs, rhs },
4975                        10 => MicroOp::Neq { dst, lhs, rhs },
4976                        11 => MicroOp::Move { dst, src: lhs },
4977                        _ => MicroOp::LoadConst {
4978                            dst,
4979                            value: (lcg(&mut s) as i64).wrapping_sub(1 << 40),
4980                        },
4981                    };
4982                    ops.push(op);
4983                }
4984                let ret = (lcg(&mut s) % slots as u64) as u16;
4985                ops.push(MicroOp::Return { src: ret });
4986
4987                let mut frame = vec![0i64; slots as usize];
4988                for (i, f) in frame.iter_mut().enumerate() {
4989                    *f = (i as i64 + 1).wrapping_mul(1_000_003) - 7;
4990                }
4991                // Exact integer math: `reference_eval` returns None on i64 overflow,
4992                // which the native chain matches with a side-exit (deopt).
4993                let expected = reference_eval(&ops, &mut frame.clone(), 100_000);
4994                let (out, post) = run(&ops, &frame);
4995                match expected {
4996                    Some(e) => {
4997                        assert_eq!(
4998                            out,
4999                            ChainOutcome::Return(e),
5000                            "slots={slots} seed={seed}: return diverged"
5001                        );
5002                        // The full frame must also match the reference's full frame.
5003                        let mut ref_frame = frame.clone();
5004                        reference_eval(&ops, &mut ref_frame, 100_000);
5005                        assert_eq!(post, ref_frame, "slots={slots} seed={seed}: frame diverged");
5006                    }
5007                    None => assert!(
5008                        out.is_deopt(),
5009                        "slots={slots} seed={seed}: overflow must deopt, got {out:?}"
5010                    ),
5011                }
5012            }
5013        }
5014    }
5015
5016    /// Shifts and unary ops bit-exact against the reference, at spill pressure.
5017    #[test]
5018    fn shifts_and_unary_match_reference() {
5019        let ops = vec![
5020            MicroOp::Shl { dst: 3, lhs: 0, rhs: 1 },
5021            MicroOp::Shr { dst: 4, lhs: 3, rhs: 2 },
5022            MicroOp::NotInt { dst: 5, src: 4 },
5023            MicroOp::NotBool { dst: 6, src: 2 },
5024            MicroOp::DivPow2 { dst: 7, lhs: 0, k: 3 },
5025            MicroOp::Add { dst: 8, lhs: 5, rhs: 7 },
5026            MicroOp::Return { src: 8 },
5027        ];
5028        for &a in &[1i64, -1, 1024, i64::MIN, -255] {
5029            let frame = vec![a, 5, 2, 0, 0, 0, 0, 0, 0];
5030            let expected = reference_eval(&ops, &mut frame.clone(), 1000).unwrap();
5031            let (out, _) = run(&ops, &frame);
5032            assert_eq!(out, ChainOutcome::Return(expected), "a={a}");
5033        }
5034    }
5035
5036    /// The Granlund–Montgomery magic constants for an unsigned divisor `d`
5037    /// (test-local; mirrors the canonical `LogosDivU64::new` the compiler ships).
5038    fn magic_parts(d: u64) -> (u64, u8) {
5039        if d & (d - 1) == 0 {
5040            return (0, (d.trailing_zeros() as u8) | 0x80);
5041        }
5042        let l = 63 - d.leading_zeros();
5043        let numer = (1u128 << l) << 64;
5044        let m = (numer / d as u128) as u64;
5045        let rem = (numer % d as u128) as u64;
5046        if d - rem < (1u64 << l) {
5047            (m + 1, l as u8)
5048        } else {
5049            let twice = rem.wrapping_mul(2);
5050            let bump = (twice >= d || twice < rem) as u64;
5051            (m.wrapping_mul(2).wrapping_add(bump) + 1, (l as u8) | 0x40)
5052        }
5053    }
5054
5055    /// `MagicDivU` (the W24 constant-divisor magic reciprocal) is bit-exact with
5056    /// `reference_eval` — which itself is bit-exact with the kernel's
5057    /// `wrapping_div`/`wrapping_rem` for the non-negative dividend it gates on —
5058    /// for both the quotient and the remainder, across several divisors (the
5059    /// 64-bit-magic and 65-bit add-marker paths) and a boundary value grid,
5060    /// emitted into a frame with enough live slots to force spill pressure on
5061    /// the dst/lhs.
5062    #[test]
5063    fn magic_div_matches_reference_div_and_mod() {
5064        for &c in &[3i64, 7, 100, 1000, 1_000_000_007, 65521] {
5065            let (magic, more) = magic_parts(c as u64);
5066            // Two ops (div into 1, mod into 2) plus padding slots 3..16 so the
5067            // allocator must spill — exercises both register- and frame-resident
5068            // dst/lhs in emit_magic_div.
5069            let ops = vec![
5070                MicroOp::MagicDivU { dst: 1, lhs: 0, magic, more, mul_back: 0 },
5071                MicroOp::MagicDivU { dst: 2, lhs: 0, magic, more, mul_back: c },
5072                MicroOp::Add { dst: 3, lhs: 1, rhs: 2 },
5073                MicroOp::Return { src: 3 },
5074            ];
5075            for n in [
5076                0i64, 1, c - 1, c, c + 1, 2 * c + 3, 123_456_789, i64::MAX, i64::MAX - 1,
5077                (i64::MAX / c) * c, (i64::MAX / c).saturating_sub(1) * c,
5078            ] {
5079                assert!(n >= 0);
5080                let frame = vec![n, 0, 0, 0];
5081                let expected = reference_eval(&ops, &mut frame.clone(), 1000).unwrap();
5082                let (out, _) = run(&ops, &frame);
5083                assert_eq!(out, ChainOutcome::Return(expected), "c={c} n={n}");
5084                // And the reference itself equals the kernel for these.
5085                assert_eq!(
5086                    expected,
5087                    n.wrapping_div(c).wrapping_add(n.wrapping_rem(c)),
5088                    "reference vs kernel c={c} n={n}"
5089                );
5090            }
5091        }
5092    }
5093
5094    /// A nested loop with many live slots (forces some spills) — the polynomial
5095    /// accumulation the ceiling harness times — matches the reference exactly.
5096    #[test]
5097    fn poly_loop_matches_reference_under_spill() {
5098        // s = s*A + i*B - C across i in 0..N, with A,B,C,N,i,s in distinct slots.
5099        let ops = vec![
5100            MicroOp::Branch { cmp: Cmp::Lt, lhs: 0, rhs: 1, target: 11 }, // i<N
5101            MicroOp::Mul { dst: 7, lhs: 2, rhs: 3 },                       // t = s*A
5102            MicroOp::Mul { dst: 8, lhs: 0, rhs: 4 },                       // u = i*B
5103            MicroOp::Add { dst: 7, lhs: 7, rhs: 8 },                       // t += u
5104            MicroOp::Sub { dst: 2, lhs: 7, rhs: 5 },                       // s = t - C
5105            MicroOp::BitXor { dst: 2, lhs: 2, rhs: 6 },                    // s ^= MASK
5106            MicroOp::Add { dst: 9, lhs: 2, rhs: 0 },                       // (extra live)
5107            MicroOp::Mul { dst: 2, lhs: 2, rhs: 3 },                       // s *= A again
5108            MicroOp::LoadConst { dst: 10, value: 1 },
5109            MicroOp::Add { dst: 0, lhs: 0, rhs: 10 }, // i += 1
5110            MicroOp::Jump { target: 0 },
5111            MicroOp::Return { src: 2 },
5112        ];
5113        // slots: 0=i 1=N 2=s 3=A 4=B 5=C 6=MASK 7=t 8=u 9=extra 10=one
5114        let frame = vec![0i64, 5000, 1, 6364136223846793005u64 as i64, 1442695040888963407u64 as i64, 12345, 0x5DEECE66D, 0, 0, 0, 0];
5115        // These LCG-sized multipliers overflow i64 on the first `s*A`. Under exact
5116        // integer math the reference returns None and the native chain side-exits
5117        // (deopt) — the two must agree that this leaves the i64 fast path.
5118        let expected = reference_eval(&ops, &mut frame.clone(), 100_000_000);
5119        let (out, _) = run(&ops, &frame);
5120        match expected {
5121            Some(e) => assert_eq!(out, ChainOutcome::Return(e)),
5122            None => assert!(out.is_deopt(), "overflow must deopt, got {out:?}"),
5123        }
5124    }
5125
5126    /// `loop_depths` (the spill-weight heuristic's loop detector): a back-edge
5127    /// `[target, idx]` raises the depth of every op in its span by one, and
5128    /// nesting accumulates. Straight-line code stays depth 0.
5129    #[test]
5130    fn loop_depths_counts_back_edge_nesting() {
5131        // No back-edge: all depth 0.
5132        let flat = vec![
5133            MicroOp::Add { dst: 1, lhs: 0, rhs: 0 },
5134            MicroOp::Return { src: 1 },
5135        ];
5136        assert_eq!(loop_depths(&flat), vec![0, 0]);
5137
5138        // One loop: a Jump at idx 3 back to 1 wraps ops 1..=3 (depth 1); the
5139        // guard at 0 and the tail at 4 stay depth 0.
5140        let single = vec![
5141            MicroOp::Branch { cmp: Cmp::Lt, lhs: 0, rhs: 1, target: 4 }, // 0
5142            MicroOp::Add { dst: 2, lhs: 2, rhs: 0 },                     // 1
5143            MicroOp::Add { dst: 0, lhs: 0, rhs: 3 },                     // 2
5144            MicroOp::Jump { target: 1 },                                 // 3
5145            MicroOp::Return { src: 2 },                                  // 4
5146        ];
5147        assert_eq!(loop_depths(&single), vec![0, 1, 1, 1, 0]);
5148
5149        // Nested: inner back-edge 3->2 (depth 2 over ops 2..=3) sits inside the
5150        // outer back-edge 5->1 (depth 1 over ops 1..=5).
5151        let nested = vec![
5152            MicroOp::Branch { cmp: Cmp::Lt, lhs: 0, rhs: 1, target: 6 }, // 0
5153            MicroOp::Move { dst: 4, src: 0 },                            // 1 outer
5154            MicroOp::Add { dst: 2, lhs: 2, rhs: 4 },                     // 2 inner
5155            MicroOp::Jump { target: 2 },                                 // 3 inner back-edge
5156            MicroOp::Add { dst: 0, lhs: 0, rhs: 3 },                     // 4 outer
5157            MicroOp::Jump { target: 1 },                                 // 5 outer back-edge
5158            MicroOp::Return { src: 2 },                                  // 6
5159        ];
5160        assert_eq!(loop_depths(&nested), vec![0, 1, 2, 2, 1, 1, 0]);
5161    }
5162
5163    /// A loop-carried slot referenced ONCE per iteration but living inside a
5164    /// loop must out-rank a colder slot referenced MANY times only in
5165    /// straight-line setup, so it wins a register. We assert this through the
5166    /// public `loc` assignment indirectly: build a region where slot `H` (hot,
5167    /// loop-carried, few raw refs) and slot `C` (cold, setup-only, many raw refs)
5168    /// compete for the SOLE remaining int register, and confirm the loop weight
5169    /// flips the ranking so the result is still bit-identical to the reference
5170    /// (the heuristic only moves residency, never correctness).
5171    #[test]
5172    fn loop_weight_keeps_carried_slot_resident_and_exact() {
5173        // Setup touches `C` (slot 5) four times in straight-line code; the loop
5174        // touches `H` (slot 2, the accumulator) once per iteration. Raw counts
5175        // would rank C above H; the loop weight (10^1 per loop ref) flips it.
5176        let ops = vec![
5177            MicroOp::Add { dst: 5, lhs: 0, rhs: 1 },  // 0 C = a+b   (setup)
5178            MicroOp::Add { dst: 5, lhs: 5, rhs: 0 },  // 1 C += a    (setup)
5179            MicroOp::Add { dst: 5, lhs: 5, rhs: 1 },  // 2 C += b    (setup)
5180            MicroOp::Add { dst: 2, lhs: 2, rhs: 5 },  // 3 seed acc with C (setup)
5181            MicroOp::Branch { cmp: Cmp::Lt, lhs: 3, rhs: 4, target: 8 }, // 4 while i<N
5182            MicroOp::Add { dst: 2, lhs: 2, rhs: 3 },  // 5 acc += i  (HOT, depth 1)
5183            MicroOp::Add { dst: 3, lhs: 3, rhs: 6 },  // 6 i += 1    (HOT, depth 1)
5184            MicroOp::Jump { target: 4 },              // 7 back-edge
5185            MicroOp::Return { src: 2 },               // 8
5186        ];
5187        // slots: 0=a 1=b 2=acc(H) 3=i 4=N 5=C(cold) 6=one
5188        let frame = vec![3i64, 4, 0, 0, 100_000, 0, 1];
5189        let depths = loop_depths(&ops);
5190        assert_eq!(depths[5], 1, "the accumulator update is inside the loop");
5191        let expected = reference_eval(&ops, &mut frame.clone(), 10_000_000).unwrap();
5192        let (out, post) = run(&ops, &frame);
5193        assert_eq!(out, ChainOutcome::Return(expected), "loop-weighted ranking must stay bit-identical");
5194        let mut ref_frame = frame.clone();
5195        reference_eval(&ops, &mut ref_frame, 10_000_000).unwrap();
5196        assert_eq!(post, ref_frame, "frame diverged under loop-weighted ranking");
5197    }
5198
5199    /// Fix 1 (call-weight tie-break): two slots with the SAME primary loop-weight
5200    /// must be ordered by call-survival — the slot LIVE ACROSS a SysV call ranks
5201    /// first, so it wins a callee-saved register (paying no per-call spill/reload)
5202    /// while the call-dead slot of equal weight takes a caller-saved one. The
5203    /// call-weight is a SECONDARY key: it breaks the tie WITHOUT changing the
5204    /// primary weights (so it can never evict a hotter loop slot).
5205    #[test]
5206    fn call_weight_breaks_tie_toward_live_across_call_slot() {
5207        // `survivor` (slot 1) and `transient` (slot 2) are each defined once and
5208        // read once — identical primary loop-weight. `survivor` is read AFTER the
5209        // call (op 3) → live across it; `transient` is consumed BEFORE the call
5210        // and never read after → dead across it. Only the secondary key differs.
5211        // 0: survivor = a + one    (def slot 1)
5212        // 1: transient = a + one   (def slot 2)
5213        // 2: sink = transient + a  (last read of transient, def slot 8 — BEFORE call)
5214        // 3: ListClear             (the SysV call, op 3)
5215        // 4: r = survivor + one    (reads slot 1 AFTER the call, def slot 4)
5216        // 5: Return r
5217        let ops = vec![
5218            MicroOp::Add { dst: 1, lhs: 0, rhs: 3 },
5219            MicroOp::Add { dst: 2, lhs: 0, rhs: 3 },
5220            MicroOp::Add { dst: 8, lhs: 2, rhs: 0 },
5221            MicroOp::ListClear { vec_slot: 5, ptr_slot: 6, len_slot: 7, helper_addr: 0 },
5222            MicroOp::Add { dst: 4, lhs: 1, rhs: 3 },
5223            MicroOp::Return { src: 4 },
5224        ];
5225        let max_slot = max_slot_of(&ops);
5226        let la = liveness_after(&ops, max_slot);
5227        assert!(la[3][1], "survivor must be live across the call");
5228        assert!(!la[3][2], "transient must be dead across the call");
5229        // PRIMARY weights must be equal (both: 1 def-ref + 1 use-ref outside any
5230        // loop). Slot 1: def@0 + use@4 = 2 refs. Slot 2: def@1 + use@2 = 2 refs.
5231        let (order, _) = rank_slots(&ops, 16, 4, 4, Some(&la));
5232        let pos = |slot: Slot| order.iter().position(|&(s, _)| s == slot).unwrap();
5233        let key = |slot: Slot| order.iter().find(|&&(s, _)| s == slot).unwrap().1;
5234        assert_eq!(
5235            key(1),
5236            key(2),
5237            "survivor and transient must share the SAME primary weight (got {} vs {})",
5238            key(1),
5239            key(2)
5240        );
5241        assert!(
5242            pos(1) < pos(2),
5243            "on equal primary weight the call-survivor must rank ahead of the call-dead slot"
5244        );
5245    }
5246
5247    /// Fix 1 soundness: the call-survival key NEVER changes the primary
5248    /// loop-weighted ordering of two slots with DIFFERENT primary weights, so it
5249    /// cannot evict a hotter loop slot. A loop-hot, call-DEAD slot must still
5250    /// out-rank a cold, call-SURVIVING slot.
5251    #[test]
5252    fn call_weight_never_outranks_a_hotter_loop_slot() {
5253        // `hot` (slot 2) is referenced every iteration of a loop (high primary
5254        // weight) but is consumed before the call → dead across it. `cold` (slot
5255        // 1) is set once outside the loop and read once after the call → live
5256        // across the call but cold. The loop-hot slot must still win.
5257        let ops = vec![
5258            MicroOp::Add { dst: 1, lhs: 0, rhs: 3 },                      // 0 cold = a+one (setup)
5259            MicroOp::Branch { cmp: Cmp::Lt, lhs: 4, rhs: 5, target: 6 },  // 1 while i<N
5260            MicroOp::Add { dst: 2, lhs: 2, rhs: 4 },                      // 2 hot += i (HOT)
5261            MicroOp::Add { dst: 4, lhs: 4, rhs: 3 },                      // 3 i += one (HOT)
5262            MicroOp::ListClear { vec_slot: 7, ptr_slot: 8, len_slot: 9, helper_addr: 0 }, // 4 call
5263            MicroOp::Jump { target: 1 },                                  // 5 back-edge
5264            MicroOp::Add { dst: 10, lhs: 1, rhs: 2 },                     // 6 use cold (live across) + hot
5265        ];
5266        // Append a Return so the region is well-formed.
5267        let mut ops = ops;
5268        ops.push(MicroOp::Return { src: 10 });
5269        let max_slot = max_slot_of(&ops);
5270        let la = liveness_after(&ops, max_slot);
5271        let (order, _) = rank_slots(&ops, 16, 4, 4, Some(&la));
5272        let pos = |slot: Slot| order.iter().position(|&(s, _)| s == slot).unwrap();
5273        assert!(
5274            pos(2) < pos(1),
5275            "the loop-hot slot must out-rank the cold call-survivor (primary key dominates)"
5276        );
5277    }
5278
5279    /// Fix 1 soundness: the call bonus is INERT for a call-free region. With
5280    /// `live_after = None` the ranking is exactly the pre-Fix-1 loop-weighted
5281    /// ranking — no bonus exists because there is no call to survive.
5282    #[test]
5283    fn call_weight_is_inert_without_a_call() {
5284        let ops = vec![
5285            MicroOp::Add { dst: 3, lhs: 0, rhs: 1 },
5286            MicroOp::Mul { dst: 4, lhs: 3, rhs: 2 },
5287            MicroOp::Sub { dst: 5, lhs: 4, rhs: 0 },
5288            MicroOp::Return { src: 5 },
5289        ];
5290        // With no liveness (call-free) and with an EMPTY all-dead liveness, the
5291        // bonus contributes nothing, so the two rankings are identical.
5292        let (no_call, _) = rank_slots(&ops, 16, 4, 4, None);
5293        let max_slot = max_slot_of(&ops);
5294        let dead = vec![vec![false; max_slot + 1]; ops.len()];
5295        let (dead_la, _) = rank_slots(&ops, 16, 4, 4, Some(&dead));
5296        assert_eq!(no_call, dead_la, "the call bonus must vanish with no live-across-call slot");
5297    }
5298
5299    /// Fix 1 (call-weight) must not perturb the SELF-CALL window forcing or the
5300    /// `max_slot` extent: a region with a `CallSelf` still compiles, and the
5301    /// ranking ranks the call-surviving non-window slot ahead of a transient. The
5302    /// runnable end-to-end (bit-identical + tiers) lives in the integration suite
5303    /// (`jit_regalloc::call_weight_*`) where a real recursive program is built.
5304    #[test]
5305    fn call_weight_self_call_region_compiles_and_ranks_survivor() {
5306        // A function-shaped region: guard, a self-call, an accumulate that reads a
5307        // value live across the call, return. (Window slots are forced frame-
5308        // resident regardless; this only checks the ranking + that it compiles.)
5309        let ops = vec![
5310            MicroOp::Branch { cmp: Cmp::Lt, lhs: 0, rhs: 1, target: 5 }, // 0 if !(i<N) -> ret
5311            MicroOp::CallSelf { dst: 3, args_start: 8, depth_addr: 0, status_addr: 0, limit_slot: 2, frame_size: 4 }, // 1 call
5312            MicroOp::Add { dst: 4, lhs: 4, rhs: 3 }, // 2 acc += call result (acc live across? no — defined here)
5313            MicroOp::Add { dst: 0, lhs: 0, rhs: 4 }, // 3 i += acc (reads acc 4 → live after the call)
5314            MicroOp::Jump { target: 0 },             // 4 back-edge
5315            MicroOp::Return { src: 4 },              // 5
5316        ];
5317        let max_slot = max_slot_of(&ops);
5318        assert!(max_slot >= 11, "self-call window extent (8+4-1=11) must be covered, got {max_slot}");
5319        let la = liveness_after(&ops, max_slot);
5320        // slot 4 (acc) is read by op 3 after the call at op 1 → live across it.
5321        assert!(la[1][4], "acc must be live across the self-call");
5322        let (order, _) = rank_slots(&ops, 16, 4, 4, Some(&la));
5323        let key = |slot: Slot| order.iter().find(|&&(s, _)| s == slot).map(|&(_, k)| k).unwrap_or(0);
5324        // slot 4 (call-survivor, loop-carried) must out-rank the bound slot 1
5325        // (referenced only at the cold guard, not across the call).
5326        assert!(key(4) > key(1), "call-surviving loop slot must out-rank a cold bound");
5327    }
5328
5329    /// Wave 16 (call-site spill liveness): `liveness_after` is a sound backward
5330    /// dataflow. A slot WRITTEN before an op and READ only before it (never on any
5331    /// path AFTER) is DEAD after that op; a slot read by a LATER op (including a
5332    /// loop back-edge) is LIVE after every op on the path to that read.
5333    #[test]
5334    fn liveness_after_is_a_sound_backward_dataflow() {
5335        // 0: t = a + b      (defines t=2)
5336        // 1: dead = a * a   (defines dead=3, never read again)
5337        // 2: r = t - dead   (reads t and dead, defines r=4)
5338        // 3: Return r
5339        let ops = vec![
5340            MicroOp::Add { dst: 2, lhs: 0, rhs: 1 },
5341            MicroOp::Mul { dst: 3, lhs: 0, rhs: 0 },
5342            MicroOp::Sub { dst: 4, lhs: 2, rhs: 3 },
5343            MicroOp::Return { src: 4 },
5344        ];
5345        let la = liveness_after(&ops, 4);
5346        // After op 1 (`dead = a*a`), `dead` (slot 3) is LIVE (read by op 2) and so
5347        // is `t` (slot 2, read by op 2).
5348        assert!(la[1][3], "dead must be live right after it is defined (read by op 2)");
5349        assert!(la[1][2], "t must still be live after op 1 (read by op 2)");
5350        // After op 2 (`r = t - dead`), NEITHER `t` nor `dead` is read again — both DEAD.
5351        assert!(!la[2][2], "t is dead after its last read (op 2)");
5352        assert!(!la[2][3], "dead is dead after its last read (op 2)");
5353        // `r` (slot 4) is live after op 2 (the Return reads it).
5354        assert!(la[2][4], "r is live after op 2 (Return reads it)");
5355        // After the Return, nothing is live.
5356        assert!(la[3].iter().all(|&b| !b), "no slot is live after Return");
5357    }
5358
5359    /// A loop-carried slot read across a back-edge stays LIVE after every op in
5360    /// the loop body (the backward dataflow must reach a fixpoint over the
5361    /// back-edge, not just the forward pass).
5362    #[test]
5363    fn liveness_after_propagates_across_loop_back_edge() {
5364        // counting loop: 0=i 1=acc 2=N 3=one
5365        let ops = vec![
5366            MicroOp::Branch { cmp: Cmp::Lt, lhs: 0, rhs: 2, target: 5 }, // 0 while i<N
5367            MicroOp::Add { dst: 1, lhs: 1, rhs: 0 },                     // 1 acc += i
5368            MicroOp::LoadConst { dst: 3, value: 1 },                     // 2 one = 1
5369            MicroOp::Add { dst: 0, lhs: 0, rhs: 3 },                     // 3 i += 1
5370            MicroOp::Jump { target: 0 },                                 // 4 back-edge
5371            MicroOp::Return { src: 1 },                                  // 5
5372        ];
5373        let la = liveness_after(&ops, 3);
5374        // `i` (slot 0) is read by the guard (op 0), `acc += i` (op 1), and `i += 1`
5375        // (op 3); via the back-edge it must be live after op 3 (the increment),
5376        // after op 1, and after the guard's true edge — i.e. live around the loop.
5377        assert!(la[3][0], "i must be live after the increment (back-edge re-reads it)");
5378        assert!(la[1][0], "i must be live after acc += i (used again at op 3)");
5379        // `acc` (slot 1) is read by op 1 and the final Return → live around the loop.
5380        assert!(la[3][1], "acc must be live after op 3 (read next iteration / return)");
5381        assert!(la[1][1], "acc must be live after its own update");
5382    }
5383
5384    /// The spill-elision keep-rule: a caller-saved resident is elided around a
5385    /// call ONLY when it is READ-ONLY (never written → never flushed at exit) AND
5386    /// dead after the call. A WRITTEN slot is kept even when dead-after-call
5387    /// (its last value is flushed at every region exit), and a read-only slot
5388    /// read after the call is kept (the reload feeds that read). This mirrors the
5389    /// `keep` closure in `compile_impl` and is the soundness contract the
5390    /// `*_matches_stencil` post-frame differentials enforce.
5391    #[test]
5392    fn spill_elision_keeps_written_and_live_drops_readonly_dead() {
5393        // Slot 5 = `cold`: a frame input READ only at op 0 (never written, never
5394        //   read after the call) → read-only + dead-after-call → ELIDED.
5395        // Slot 8 = `hot`: WRITTEN at op 1, dead after the call (op 3 re-reads it,
5396        //   so it is actually live-after too — but the WRITTEN rule alone keeps
5397        //   it; flushed at every exit). Slot 1 = the literal-`1` operand: read
5398        //   only (never written) but READ after the call (op 3) → kept via live.
5399        // 0: t0 = cold + 1   (reads slot 5 `cold`, defines slot 7)
5400        // 1: hot = hot + 1   (WRITTEN slot 8)
5401        // 2: ListClear       (the call op, index 2)
5402        // 3: keep = hot + 1  (reads slot 8 `hot` and slot 1 `1`, defines slot 9)
5403        // 4: Return keep
5404        let ops = vec![
5405            MicroOp::Add { dst: 7, lhs: 5, rhs: 1 },          // 0 t0 = cold + 1
5406            MicroOp::Add { dst: 8, lhs: 8, rhs: 1 },          // 1 hot += 1 (written)
5407            MicroOp::ListClear { vec_slot: 2, ptr_slot: 3, len_slot: 4, helper_addr: 0 }, // 2 call
5408            MicroOp::Add { dst: 9, lhs: 8, rhs: 1 },          // 3 keep = hot + 1
5409            MicroOp::Return { src: 9 },                       // 4
5410        ];
5411        let max_slot = 9;
5412        let la = liveness_after(&ops, max_slot);
5413        let mut written = vec![false; max_slot + 1];
5414        for op in &ops {
5415            if let Some(d) = dest_of(op) {
5416                written[d as usize] = true;
5417            }
5418        }
5419        let keep = |s: Slot| {
5420            written.get(s as usize).copied().unwrap_or(true)
5421                || la[2].get(s as usize).copied().unwrap_or(true)
5422        };
5423        // `cold` (slot 5): read-only (only ever an operand) and NOT read after the
5424        // call (op 3 reads `hot`/`1`, not `cold`) → ELIDED.
5425        assert!(!written[5], "cold is read-only");
5426        assert!(!la[2][5], "cold is dead after the call");
5427        assert!(!keep(5), "a read-only, dead-after-call resident must be elided");
5428        // `hot` (slot 8): WRITTEN → kept regardless (flushed at every exit). It is
5429        // also live-after here, but the WRITTEN rule alone must keep it.
5430        assert!(written[8], "hot is written");
5431        assert!(keep(8), "a written resident must be kept (flushed at exit)");
5432        // `frame[1]` (the literal `1` operand): read-only but read AFTER the call
5433        // (op 3 reads it) → kept via the live-after rule.
5434        assert!(!written[1], "the constant operand is read-only");
5435        assert!(la[2][1], "the constant operand is read after the call");
5436        assert!(keep(1), "a read-only resident read after the call must be kept");
5437    }
5438
5439    // =================================================================
5440    // WAVE 21: precise REGION backend (in-place SetIndex + reallocating
5441    // ArrPush). The fannkuch / graph_bfs worklist shape — a precise region
5442    // that does NOT have the mode-B function prologue, so `mode_b_rc` is
5443    // inferred only for FUNCTIONS, never regions.
5444    // =================================================================
5445
5446    /// The precise REGION path compiles a push+SetIndex stream (a reallocating
5447    /// `ArrPush` beside an in-place checked `ArrStore`) into ONE contiguous
5448    /// register-allocated chain — even though the stream has NO mode-B function
5449    /// prologue (`LoadConst {-1}`). The per-piece precise stencil tier no longer
5450    /// monopolizes this shape.
5451    #[test]
5452    fn precise_region_with_push_and_setindex_compiles() {
5453        // 0: i < n?  -> exit (op 6)
5454        // 1: ArrStore arr[i] = v  (in-place, CHECKED — the precise side exit)
5455        // 2: ArrPush v -> q       (reallocates q; helper refreshes ptr/len)
5456        // 3: i += 1
5457        // 4: Jump head
5458        // 5: (exit) Return i
5459        let ops = vec![
5460            MicroOp::Branch { cmp: Cmp::Lt, lhs: 0, rhs: 1, target: 5 },
5461            MicroOp::ArrStore { src: 2, idx: 0, ptr_slot: 6, len_slot: 7, byte: false, narrow32: false, checked: true },
5462            MicroOp::ArrPush { src: 2, vec_slot: 8, ptr_slot: 9, len_slot: 10, helper_addr: 0, byte: false, narrow32: false },
5463            MicroOp::Add { dst: 0, lhs: 0, rhs: 3 },
5464            MicroOp::Jump { target: 0 },
5465            MicroOp::Return { src: 0 },
5466        ];
5467        // Per-op precise resume codes (parallel to `ops`): the checked store and
5468        // the push carry a precise tag `(pc << 2) | 3`; the rest stay plain `1`.
5469        let codes: Vec<i64> = ops
5470            .iter()
5471            .enumerate()
5472            .map(|(i, op)| match op {
5473                MicroOp::ArrStore { checked: true, .. } | MicroOp::ArrPush { .. } => {
5474                    ((i as i64) << 2) | 3
5475                }
5476                _ => 1,
5477            })
5478            .collect();
5479        let status = Arc::new(AtomicI64::new(0));
5480        let chain = compile_region_regalloc_precise(&ops, Some(status), 0, &codes);
5481        assert!(
5482            chain.is_some(),
5483            "a precise push+SetIndex REGION (no mode-B prologue) must compile through \
5484             the contiguous regalloc backend"
5485        );
5486    }
5487
5488    /// The precise REGION path DECLINES when the per-op `deopt_codes` length does
5489    /// not match the op stream — a malformed call falls back to the per-piece tier
5490    /// rather than mis-indexing the resume table.
5491    #[test]
5492    fn precise_region_declines_on_codes_length_mismatch() {
5493        let ops = vec![
5494            MicroOp::ArrStore { src: 2, idx: 0, ptr_slot: 6, len_slot: 7, byte: false, narrow32: false, checked: true },
5495            MicroOp::Return { src: 0 },
5496        ];
5497        let codes = vec![1i64]; // one short
5498        let status = Arc::new(AtomicI64::new(0));
5499        assert!(
5500            compile_region_regalloc_precise(&ops, Some(status), 0, &codes).is_none(),
5501            "a deopt-codes length mismatch must decline"
5502        );
5503    }
5504
5505    /// The precise REGION path requires the shared status channel (every checked
5506    /// op's side exit stores its tagged resume value through it); a `None` status
5507    /// declines.
5508    #[test]
5509    fn precise_region_declines_without_status() {
5510        let ops = vec![
5511            MicroOp::ArrStore { src: 2, idx: 0, ptr_slot: 6, len_slot: 7, byte: false, narrow32: false, checked: true },
5512            MicroOp::Return { src: 0 },
5513        ];
5514        let codes = vec![3i64, 1];
5515        assert!(
5516            compile_region_regalloc_precise(&ops, None, 0, &codes).is_none(),
5517            "a precise region with no status channel must decline"
5518        );
5519    }
5520
5521    /// A precise region carrying a CROSS-FUNCTION `Call` is unsupported by the
5522    /// region gate (`supported`) and must decline — the JIT adapter already
5523    /// disqualifies push-beside-call regions, and the backend is the second line
5524    /// of defense (a call frame would need the full function-precise walk).
5525    #[test]
5526    fn precise_region_declines_on_cross_call() {
5527        let ops = vec![
5528            MicroOp::Call {
5529                dst: 1,
5530                args_start: 4,
5531                table_addr: 0,
5532                depth_addr: 0,
5533                status_addr: 0,
5534                limit_slot: 3,
5535                depth_limit: SELF_CALL_DEPTH_LIMIT,
5536            },
5537            MicroOp::Return { src: 1 },
5538        ];
5539        let codes = vec![3i64, 1];
5540        let status = Arc::new(AtomicI64::new(0));
5541        assert!(
5542            compile_region_regalloc_precise(&ops, Some(status), 0, &codes).is_none(),
5543            "a precise REGION with a cross-function Call must decline (region gate)"
5544        );
5545    }
5546}