Skip to main content

polydat_core/compile/jit/
codegen.rs

1// Copyright 2024-2026 Jonathan Shook
2// SPDX-License-Identifier: Apache-2.0
3
4//! JIT codegen: Cranelift IR generation, operation classification, and
5//! extern "C" runtime helpers called from JIT-compiled code.
6//!
7//! `JitOp` classifies each DAG node into an inline IR pattern or an
8//! extern call. `compile_jit_impl` lowers a slice of `(JitOp, inputs,
9//! outputs)` steps into a single native function via Cranelift.
10//! The four `compile_jit_*` constructors wrap the result in the
11//! appropriate kernel struct from `kernels`.
12
13use std::collections::HashMap;
14use std::mem;
15
16use cranelift_codegen::ir::{self, AbiParam, InstBuilder, types};
17use cranelift_codegen::settings::{self, Configurable};
18use cranelift_frontend::{FunctionBuilder, FunctionBuilderContext};
19use cranelift_jit::{JITBuilder, JITModule};
20use cranelift_module::{Linkage, Module};
21
22use crate::ast::PolydatNode;
23use crate::ast::SlotShape;
24
25use super::kernels::{JitCore, JitKernelPull, JitKernelPush, JitKernelPushPull, JitKernelRaw};
26
27// ── Extern "C" runtime helpers ─────────────────────────────
28
29/// Extern function: xxhash3 of a u64 (called from JIT code).
30extern "C" fn jit_xxh3_hash(value: u64) -> u64 {
31    guarded(|| xxhash_rust::xxh3::xxh3_64(&value.to_le_bytes()))
32}
33
34/// Extern function: interleave bits of two u64 values (called from JIT code).
35extern "C" fn jit_interleave(a: u64, b: u64) -> u64 {
36    guarded(|| {
37        let mut result: u64 = 0;
38        for i in 0..32 {
39            result |= ((a >> i) & 1) << (2 * i);
40            result |= ((b >> i) & 1) << (2 * i + 1);
41        }
42        result
43    })
44}
45
46/// Extern function: interpolating LUT sample (called from JIT code).
47///
48/// Input is f64 bits in [0,1]. LUT pointer + length are baked constants.
49/// Returns f64 result as u64 bits.
50extern "C" fn jit_lut_sample(input_bits: u64, lut_ptr: u64, lut_len: u64) -> u64 {
51    guarded(|| {
52        let u = f64::from_bits(input_bits).clamp(0.0, 1.0);
53        let n = (lut_len - 1) as f64;
54        let pos = u * n;
55        let idx = (pos as usize).min(lut_len as usize - 2);
56        let frac = pos - idx as f64;
57        let result = unsafe {
58            let ptr = lut_ptr as *const f64;
59            let a = *ptr.add(idx);
60            let b = *ptr.add(idx + 1);
61            a * (1.0 - frac) + b * frac
62        };
63        result.to_bits()
64    })
65}
66
67/// Extern function: LFSR shuffle (called from JIT code).
68extern "C" fn jit_shuffle(input: u64, feedback: u64, size: u64, min: u64) -> u64 {
69    guarded(|| {
70        let mut register = (input % size) + 1;
71        loop {
72            let lsb = register & 1;
73            register >>= 1;
74            if lsb != 0 {
75                register ^= feedback;
76            }
77            if register <= size {
78                break;
79            }
80        }
81        (register - 1) + min
82    })
83}
84
85// Extern functions for math operations (called from JIT code).
86extern "C" fn jit_sin(bits: u64) -> u64 {
87    guarded(|| f64::from_bits(bits).sin().to_bits())
88}
89extern "C" fn jit_cos(bits: u64) -> u64 {
90    guarded(|| f64::from_bits(bits).cos().to_bits())
91}
92extern "C" fn jit_tan(bits: u64) -> u64 {
93    guarded(|| f64::from_bits(bits).tan().to_bits())
94}
95extern "C" fn jit_asin(bits: u64) -> u64 {
96    guarded(|| f64::from_bits(bits).asin().to_bits())
97}
98extern "C" fn jit_acos(bits: u64) -> u64 {
99    guarded(|| f64::from_bits(bits).acos().to_bits())
100}
101extern "C" fn jit_atan(bits: u64) -> u64 {
102    guarded(|| f64::from_bits(bits).atan().to_bits())
103}
104extern "C" fn jit_sqrt(bits: u64) -> u64 {
105    guarded(|| f64::from_bits(bits).sqrt().to_bits())
106}
107extern "C" fn jit_abs_f64(bits: u64) -> u64 {
108    guarded(|| f64::from_bits(bits).abs().to_bits())
109}
110extern "C" fn jit_ln(bits: u64) -> u64 {
111    guarded(|| f64::from_bits(bits).ln().to_bits())
112}
113extern "C" fn jit_exp(bits: u64) -> u64 {
114    guarded(|| f64::from_bits(bits).exp().to_bits())
115}
116extern "C" fn jit_floor_base10(bits: u64) -> u64 {
117    guarded(|| {
118        use crate::numeric::round_numbers::*;
119        let x = f64::from_bits(bits);
120        let r = if !positive_finite(x) {
121            0.0
122        } else {
123            floor_pow10(x)
124        };
125        r.to_bits()
126    })
127}
128extern "C" fn jit_ceiling_base10(bits: u64) -> u64 {
129    guarded(|| {
130        use crate::numeric::round_numbers::*;
131        let x = f64::from_bits(bits);
132        let r = if !positive_finite(x) {
133            0.0
134        } else {
135            let lo = floor_pow10(x);
136            if lo == x { lo } else { lo * 10.0 }
137        };
138        r.to_bits()
139    })
140}
141extern "C" fn jit_closest_base10(bits: u64) -> u64 {
142    guarded(|| {
143        use crate::numeric::round_numbers::*;
144        let x = f64::from_bits(bits);
145        let r = if !positive_finite(x) {
146            0.0
147        } else {
148            let lo = floor_pow10(x);
149            let hi = if lo == x { lo } else { lo * 10.0 };
150            pick_closest(x, lo, hi)
151        };
152        r.to_bits()
153    })
154}
155extern "C" fn jit_floor_decade(bits: u64) -> u64 {
156    guarded(|| {
157        use crate::numeric::round_numbers::*;
158        let x = f64::from_bits(bits);
159        let r = if !positive_finite(x) {
160            0.0
161        } else {
162            let base = floor_pow10(x);
163            (x / base).floor() * base
164        };
165        r.to_bits()
166    })
167}
168extern "C" fn jit_ceiling_decade(bits: u64) -> u64 {
169    guarded(|| {
170        use crate::numeric::round_numbers::*;
171        let x = f64::from_bits(bits);
172        let r = if !positive_finite(x) {
173            0.0
174        } else {
175            let base = floor_pow10(x);
176            (x / base).ceil() * base
177        };
178        r.to_bits()
179    })
180}
181extern "C" fn jit_closest_decade(bits: u64) -> u64 {
182    guarded(|| {
183        use crate::numeric::round_numbers::*;
184        let x = f64::from_bits(bits);
185        let r = if !positive_finite(x) {
186            0.0
187        } else {
188            let base = floor_pow10(x);
189            (x / base).round() * base
190        };
191        r.to_bits()
192    })
193}
194extern "C" fn jit_floor_binomial(bits: u64) -> u64 {
195    guarded(|| {
196        use crate::numeric::round_numbers::*;
197        let x = f64::from_bits(bits);
198        let r = if !positive_finite(x) {
199            0.0
200        } else {
201            floor_pow2(x)
202        };
203        r.to_bits()
204    })
205}
206extern "C" fn jit_ceiling_binomial(bits: u64) -> u64 {
207    guarded(|| {
208        use crate::numeric::round_numbers::*;
209        let x = f64::from_bits(bits);
210        let r = if !positive_finite(x) {
211            0.0
212        } else {
213            let lo = floor_pow2(x);
214            if lo == x { lo } else { lo * 2.0 }
215        };
216        r.to_bits()
217    })
218}
219extern "C" fn jit_closest_binomial(bits: u64) -> u64 {
220    guarded(|| {
221        use crate::numeric::round_numbers::*;
222        let x = f64::from_bits(bits);
223        let r = if !positive_finite(x) {
224            0.0
225        } else {
226            let lo = floor_pow2(x);
227            let hi = if lo == x { lo } else { lo * 2.0 };
228            pick_closest(x, lo, hi)
229        };
230        r.to_bits()
231    })
232}
233extern "C" fn jit_floor_fibonacci(bits: u64) -> u64 {
234    guarded(|| {
235        use crate::numeric::round_numbers::*;
236        let x = f64::from_bits(bits);
237        let r = if !positive_finite(x) {
238            0.0
239        } else {
240            floor_fibonacci_val(x)
241        };
242        r.to_bits()
243    })
244}
245extern "C" fn jit_ceiling_fibonacci(bits: u64) -> u64 {
246    guarded(|| {
247        use crate::numeric::round_numbers::*;
248        let x = f64::from_bits(bits);
249        let r = if !positive_finite(x) {
250            0.0
251        } else {
252            ceiling_fibonacci_val(x)
253        };
254        r.to_bits()
255    })
256}
257extern "C" fn jit_closest_fibonacci(bits: u64) -> u64 {
258    guarded(|| {
259        use crate::numeric::round_numbers::*;
260        let x = f64::from_bits(bits);
261        let r = if !positive_finite(x) {
262            0.0
263        } else {
264            pick_closest(x, floor_fibonacci_val(x), ceiling_fibonacci_val(x))
265        };
266        r.to_bits()
267    })
268}
269
270extern "C" fn jit_atan2(y_bits: u64, x_bits: u64) -> u64 {
271    guarded(|| {
272        f64::from_bits(y_bits)
273            .atan2(f64::from_bits(x_bits))
274            .to_bits()
275    })
276}
277extern "C" fn jit_pow(base_bits: u64, exp_bits: u64) -> u64 {
278    guarded(|| {
279        f64::from_bits(base_bits)
280            .powf(f64::from_bits(exp_bits))
281            .to_bits()
282    })
283}
284extern "C" fn jit_round_nearest(x_bits: u64, iv_bits: u64) -> u64 {
285    guarded(|| {
286        let x = f64::from_bits(x_bits);
287        let interval = f64::from_bits(iv_bits);
288        let r = if !(interval.is_finite() && interval > 0.0) {
289            x
290        } else {
291            (x / interval).round() * interval
292        };
293        r.to_bits()
294    })
295}
296extern "C" fn jit_round_floor(x_bits: u64, iv_bits: u64) -> u64 {
297    guarded(|| {
298        let x = f64::from_bits(x_bits);
299        let interval = f64::from_bits(iv_bits);
300        let r = if !(interval.is_finite() && interval > 0.0) {
301            x
302        } else {
303            (x / interval).floor() * interval
304        };
305        r.to_bits()
306    })
307}
308extern "C" fn jit_round_ceiling(x_bits: u64, iv_bits: u64) -> u64 {
309    guarded(|| {
310        let x = f64::from_bits(x_bits);
311        let interval = f64::from_bits(iv_bits);
312        let r = if !(interval.is_finite() && interval > 0.0) {
313            x
314        } else {
315            (x / interval).ceil() * interval
316        };
317        r.to_bits()
318    })
319}
320
321extern "C" fn jit_pcg(input: u64, seed: u64, stream: u64) -> u64 {
322    guarded(|| {
323        let inc = 2u64.wrapping_mul(stream).wrapping_add(1);
324        crate::numeric::pcg::pcg_seek(seed, inc, input)
325    })
326}
327extern "C" fn jit_pcg_stream(input: u64, stream: u64, seed: u64) -> u64 {
328    guarded(|| {
329        let inc = 2u64.wrapping_mul(stream).wrapping_add(1);
330        crate::numeric::pcg::pcg_seek(seed, inc, input)
331    })
332}
333extern "C" fn jit_n_of(input: u64, n: u64, m: u64) -> u64 {
334    guarded(|| {
335        if m == 0 {
336            return 0;
337        }
338        crate::numeric::n_of_m::n_of_m_eval(input, n, m)
339    })
340}
341
342extern "C" fn jit_cycle_walk(pos: u64, range: u64, seed: u64, inc: u64) -> u64 {
343    guarded(|| {
344        let stream = inc.saturating_sub(1) / 2;
345        let state = crate::numeric::pcg::build_cycle_walk_state(range, seed, stream);
346        crate::numeric::pcg::cycle_walk_inner(
347            pos,
348            range,
349            state.half_bits,
350            state.half_mask,
351            &state.round_keys,
352        )
353    })
354}
355
356extern "C" fn jit_perlin_1d(input: u64, perm_ptr: u64, freq_bits: u64) -> u64 {
357    guarded(|| {
358        let perm = unsafe { &*(perm_ptr as *const crate::numeric::noise::PermTable) };
359        let freq = f64::from_bits(freq_bits);
360        let r = crate::numeric::noise::perlin_1d_algo(perm, input as f64 * freq);
361        r.to_bits()
362    })
363}
364
365extern "C" fn jit_perlin_2d(x: u64, y: u64, perm_ptr: u64, freq_bits: u64) -> u64 {
366    guarded(|| {
367        let perm = unsafe { &*(perm_ptr as *const crate::numeric::noise::PermTable) };
368        let freq = f64::from_bits(freq_bits);
369        let r = crate::numeric::noise::perlin_2d_algo(perm, x as f64 * freq, y as f64 * freq);
370        r.to_bits()
371    })
372}
373
374extern "C" fn jit_simplex_2d(x: u64, y: u64, perm_ptr: u64, freq_bits: u64) -> u64 {
375    guarded(|| {
376        let perm = unsafe { &*(perm_ptr as *const crate::numeric::noise::PermTable) };
377        let freq = f64::from_bits(freq_bits);
378        let r = crate::numeric::noise::simplex_2d_algo(perm, x as f64 * freq, y as f64 * freq);
379        r.to_bits()
380    })
381}
382
383extern "C" fn jit_fractal_noise_1d(input: u64, perm_ptr: u64, freq_bits: u64, octaves: u64) -> u64 {
384    guarded(|| {
385        let perm = unsafe { &*(perm_ptr as *const crate::numeric::noise::PermTable) };
386        let freq = f64::from_bits(freq_bits);
387        let r = crate::numeric::noise::fbm_1d(perm, input as f64, freq, octaves as u32);
388        r.to_bits()
389    })
390}
391
392extern "C" fn jit_fractal_noise_2d(
393    x: u64,
394    y: u64,
395    perm_ptr: u64,
396    freq_bits: u64,
397    octaves: u64,
398) -> u64 {
399    guarded(|| {
400        let perm = unsafe { &*(perm_ptr as *const crate::numeric::noise::PermTable) };
401        let freq = f64::from_bits(freq_bits);
402        let r = crate::numeric::noise::fbm_2d(perm, x as f64, y as f64, freq, octaves as u32);
403        r.to_bits()
404    })
405}
406
407extern "C" fn jit_thread_id() -> u64 {
408    guarded(|| {
409        let id = std::thread::current().id();
410        let id_str = format!("{id:?}");
411        let num = id_str.trim_start_matches("ThreadId(").trim_end_matches(')');
412        num.parse().unwrap_or(0)
413    })
414}
415
416extern "C" fn jit_current_epoch_millis() -> u64 {
417    guarded(|| {
418        std::time::SystemTime::now()
419            .duration_since(std::time::UNIX_EPOCH)
420            .unwrap()
421            .as_millis() as u64
422    })
423}
424
425// ── Catchable predicate violations via setjmp/longjmp ─────────
426//
427// Cranelift-JIT emits DWARF unwind info (`unwind_info=true`) but
428// does not call `__register_frame`; teaching the system
429// unwinder about JIT frames needs either an upstream Cranelift
430// change or a personality-routine shim that's a project on its
431// own. We take the self-contained route instead: a setjmp
432// sentinel installed by the Rust eval wrapper, and extern
433// helpers that `longjmp` back to it on violation.
434//
435// The longjmp skips over the JIT frame entirely — no unwind,
436// no personality lookup, no catch-block walk. Control returns
437// to the Rust wrapper which reads the violation message from a
438// thread-local and raises a normal Rust `panic!`. That panic
439// unwinds through the Rust caller's frames (which have proper
440// `rust_eh_personality` FDEs) and `catch_unwind` catches it
441// like any other panic. Fail-path callers no longer lose the
442// entire process to an abort.
443//
444// Safety
445//   - longjmp skips C-level destructors. The JIT code is pure
446//     machine code with no Drop semantics, so nothing leaks.
447//     The extern helpers themselves hold no resources.
448//   - The thread-local buffer is per-thread, so concurrent
449//     kernels on different tokio worker threads don't share
450//     state. A nested evaluation on the same thread installs
451//     its own buffer and `JmpBufGuard` restores the enclosing
452//     one on every exit path, so the slot behaves as a stack.
453
454/// Platform-independent jmp_buf shim. Allocated oversize (512
455/// bytes, 16-aligned) so the biggest real platform buffer
456/// (glibc Linux: ~200 bytes, macOS: ~192) fits with margin.
457/// We link against the C library's `_setjmp` / `_longjmp`
458/// symbols directly — the `setjmp` macro in the glibc header
459/// expands to `__sigsetjmp`, which saves the signal mask; we
460/// don't need that and `_setjmp` is faster.
461#[repr(C, align(16))]
462struct JitJmpBuf([u8; 512]);
463
464#[cfg(not(windows))]
465unsafe extern "C" {
466    fn _setjmp(env: *mut JitJmpBuf) -> i32;
467    fn _longjmp(env: *mut JitJmpBuf, val: i32) -> !;
468}
469
470// MSVC CRT spelling of the same pair: it exports `longjmp`
471// (no underscore — `_longjmp` doesn't exist there, LNK2019)
472// and an x64 `_setjmp` whose second register argument is
473// recorded as the jmp_buf's `Frame` field. The C compiler
474// normally fills that in via intrinsic; calling from Rust we
475// pass NULL explicitly, which is load-bearing twice over: it
476// keeps rdx from carrying garbage into the buffer, and a zero
477// `Frame` makes `longjmp` do a plain register restore instead
478// of an `RtlUnwindEx` unwind — mandatory here because the
479// frames being skipped are JIT code with no unwind tables
480// registered (the exact problem this setjmp path exists to
481// avoid; see the module comment above).
482#[cfg(windows)]
483unsafe extern "C" {
484    fn _setjmp(env: *mut JitJmpBuf, frame: *mut std::ffi::c_void) -> i32;
485    #[link_name = "longjmp"]
486    fn _longjmp(env: *mut JitJmpBuf, val: i32) -> !;
487}
488
489use std::cell::{Cell, RefCell};
490thread_local! {
491    /// Set by [`invoke_with_catch`] before entering JIT code;
492    /// cleared on return. The extern longjmp helpers consult
493    /// this slot to find their return target. `None` means "no
494    /// wrapper installed" → fall back to abort so violations
495    /// outside a catching wrapper still terminate cleanly
496    /// rather than triggering undefined behavior.
497    static JIT_JMP_BUF: Cell<Option<*mut JitJmpBuf>> = const { Cell::new(None) };
498    /// Populated by the extern helpers right before the
499    /// longjmp; drained by the wrapper after setjmp returns
500    /// non-zero.
501    static JIT_VIOLATION_MSG: RefCell<Option<String>> = const { RefCell::new(None) };
502}
503
504/// Store the violation message and longjmp back to the wrapper.
505/// Used by every predicate extern on the fail path. If no
506/// wrapper is installed on the current thread (e.g. someone
507/// calling the JIT code directly without `invoke_with_catch`),
508/// prints the message and aborts — matches the original
509/// behavior for that call pattern.
510fn jit_violation_longjmp(msg: String) -> ! {
511    JIT_VIOLATION_MSG.with(|m| *m.borrow_mut() = Some(msg.clone()));
512    let buf_ptr: Option<*mut JitJmpBuf> = JIT_JMP_BUF.with(|b| b.get());
513    match buf_ptr {
514        Some(ptr) => unsafe { _longjmp(ptr, 1) },
515        None => {
516            let mut err = std::io::stderr().lock();
517            use std::io::Write;
518            let _ = writeln!(err, "{msg}");
519            let _ = err.flush();
520            std::process::abort();
521        }
522    }
523}
524
525/// RAII restore of the enclosing thread-local `JIT_JMP_BUF`
526/// slot. Ensures the wrapper's buffer pointer doesn't outlive
527/// its stack frame — even if the wrapped closure panics for a
528/// reason unrelated to the JIT predicate (a bug in a
529/// non-JIT sub-path, an OOM, etc.) the guard's `Drop`
530/// reinstates the previous slot so the next `invoke_with_catch`
531/// call doesn't see a dangling pointer.
532struct JmpBufGuard {
533    prev: Option<*mut JitJmpBuf>,
534}
535
536impl Drop for JmpBufGuard {
537    fn drop(&mut self) {
538        JIT_JMP_BUF.with(|b| b.set(self.prev));
539    }
540}
541
542/// Wrapper the kernels, cones, and hybrid segments run fallible
543/// native code under (code that calls a helper); code with no
544/// helper call runs bare. Sets up the setjmp sentinel, runs the
545/// closure (which calls into JIT code), and translates a longjmp
546/// return into a Rust panic carrying the violation message. The panic happens in Rust
547/// land, so `catch_unwind` catches it normally.
548///
549/// Both entry/exit paths flow through the [`JmpBufGuard`] so a
550/// panic from inside `f()` that isn't a JIT violation still
551/// restores the outer slot correctly.
552pub(crate) fn invoke_with_catch<F: FnOnce()>(f: F) {
553    use std::mem::MaybeUninit;
554    let mut buf: MaybeUninit<JitJmpBuf> = MaybeUninit::uninit();
555    let buf_ptr = buf.as_mut_ptr();
556    // Install the jmp_buf for the duration of the call. The
557    // guard restores the previous slot on every exit path
558    // (normal return, longjmp, or non-JIT panic unwinding
559    // through our frame).
560    let prev: Option<*mut JitJmpBuf> = JIT_JMP_BUF.with(|b| b.replace(Some(buf_ptr)));
561    let _guard = JmpBufGuard { prev };
562    #[cfg(not(windows))]
563    let jmpval = unsafe { _setjmp(buf_ptr) };
564    // NULL frame → non-unwinding longjmp; see the extern block.
565    #[cfg(windows)]
566    let jmpval = unsafe { _setjmp(buf_ptr, std::ptr::null_mut()) };
567    if jmpval == 0 {
568        f();
569    } else {
570        // longjmp return. The guard will restore the outer
571        // slot when this frame exits; drain the violation
572        // message and raise a normal Rust panic so the
573        // caller's `catch_unwind` can see it.
574        let msg = JIT_VIOLATION_MSG
575            .with(|m| m.borrow_mut().take())
576            .unwrap_or_else(|| "JIT predicate violation (no message)".into());
577        // Not `panic!`: the hook already saw the original panic (under
578        // `guarded`) and recorded its location for the enrichment the
579        // kernel adds; a second hook call would overwrite it.
580        std::panic::resume_unwind(Box::new(msg));
581    }
582}
583
584/// Extern function: longjmp back to the enclosing wrapper with
585/// an `is_positive` violation message. Called from JIT code on
586/// the predicate-fail path.
587extern "C" fn jit_is_positive_fail(value: u64, name_ptr: u64, name_len: u64) -> u64 {
588    // The pointer targets the `name` const in the node's NodeMeta;
589    // the node is kept alive for the life of the compiled code by
590    // `JitCore::_nodes` / the cone node's `members`, so the str
591    // data is stable. (ptr, len) == (0, 0) means the default name.
592    let name = if name_ptr != 0 {
593        unsafe {
594            std::str::from_utf8_unchecked(std::slice::from_raw_parts(
595                name_ptr as *const u8,
596                name_len as usize,
597            ))
598        }
599    } else {
600        "value"
601    };
602    jit_violation_longjmp(format!(
603        "is_positive({name}): value must be > 0, got {value}"
604    ));
605}
606
607/// Extern function: longjmp back to the enclosing wrapper with
608/// an `in_range` violation message.
609extern "C" fn jit_in_range_fail(value: u64, lo: u64, hi: u64) -> u64 {
610    jit_violation_longjmp(format!("in_range: value {value} outside [{lo}, {hi}]"));
611}
612
613/// Extern function: the failure a node whose body divides by a wire
614/// or constant raises on a zero divisor, in the words the interpreter
615/// raises it (Rust's own): `kind` 0 for a quotient, 1 for a remainder.
616extern "C" fn jit_div_zero_fail(kind: u64) -> u64 {
617    jit_violation_longjmp(
618        if kind == 0 {
619            "attempt to divide by zero"
620        } else {
621            "attempt to calculate the remainder with a divisor of zero"
622        }
623        .to_string(),
624    );
625}
626
627/// `f64_mod` natively: the body itself, since Rust's `%` on floats is
628/// the truncated remainder with the dividend's sign, which no sequence
629/// of Cranelift float instructions reproduces for every input.
630extern "C" fn jit_f64_mod(a_bits: u64, b_bits: u64) -> u64 {
631    let (a, b) = (f64::from_bits(a_bits), f64::from_bits(b_bits));
632    (if b != 0.0 { a % b } else { 0.0 }).to_bits()
633}
634
635/// Extern function: longjmp back to the enclosing wrapper with
636/// an `is_one_of` violation message, carrying the allow-list
637/// contents so the message matches the interpreter's byte for
638/// byte. The pointer targets the node's meta VecU64 const; the
639/// node is kept alive for the life of the compiled code by
640/// `JitCore::_nodes` / the cone node's members, so the data is
641/// stable. (ptr, len) == (0, 0) degrades to an elided set.
642extern "C" fn jit_is_one_of_fail(value: u64, set_ptr: u64, set_len: u64) -> u64 {
643    let msg = if set_ptr != 0 {
644        let set = unsafe { std::slice::from_raw_parts(set_ptr as *const u64, set_len as usize) };
645        format!("is_one_of: value {value} not in allowed set {set:?}")
646    } else {
647        format!("is_one_of: value {value} not in allowed set [..]")
648    };
649    jit_violation_longjmp(msg);
650}
651
652/// Extern function: weighted pick via alias table (called from JIT code).
653///
654/// Performs O(1) alias sampling and value lookup. All array pointers
655/// are baked as i64 immediates in the JIT code.
656extern "C" fn jit_weighted_pick(
657    input: u64,
658    values_ptr: u64,
659    biases_ptr: u64,
660    primaries_ptr: u64,
661    aliases_ptr: u64,
662    n: u64,
663) -> u64 {
664    guarded(|| {
665        let n = n as usize;
666        let slot = (input as usize) % n;
667        let bias_test = ((input >> 32) as f64) / (u32::MAX as f64);
668        unsafe {
669            let biases = std::slice::from_raw_parts(biases_ptr as *const f64, n);
670            let primaries = std::slice::from_raw_parts(primaries_ptr as *const u64, n);
671            let aliases = std::slice::from_raw_parts(aliases_ptr as *const u64, n);
672            let values = std::slice::from_raw_parts(values_ptr as *const u64, n);
673            let index = if bias_test < biases[slot] {
674                primaries[slot]
675            } else {
676                aliases[slot]
677            };
678            values[index as usize]
679        }
680    })
681}
682
683/// Run a body that may panic as its P1 node panics inside an
684/// `extern "C"` helper, where a panic would abort: the panic is caught
685/// and re-raised through the longjmp path, so it surfaces in Rust land
686/// as the same panic the P1 node raises.
687fn guarded<T>(body: impl FnOnce() -> T) -> T {
688    match std::panic::catch_unwind(std::panic::AssertUnwindSafe(body)) {
689        Ok(v) => v,
690        Err(payload) => {
691            let msg = payload
692                .downcast_ref::<String>()
693                .cloned()
694                .or_else(|| payload.downcast_ref::<&str>().map(|s| s.to_string()))
695                .unwrap_or_else(|| "panic in a compiled helper".to_string());
696            jit_violation_longjmp(msg)
697        }
698    }
699}
700
701/// A node's slot kit as native code holds it: shared by every kernel
702/// compiled from the program, compared by identity.
703#[derive(Clone)]
704pub struct SlotKitRef(pub std::sync::Arc<crate::ast::CompiledSlotKit>);
705
706impl SlotKitRef {
707    fn new(kit: crate::ast::CompiledSlotKit) -> Self {
708        SlotKitRef(std::sync::Arc::new(kit))
709    }
710}
711
712impl std::fmt::Debug for SlotKitRef {
713    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
714        write!(
715            f,
716            "SlotKitRef({:p}, {} scratch)",
717            std::sync::Arc::as_ptr(&self.0),
718            self.0.scratch.len()
719        )
720    }
721}
722
723impl PartialEq for SlotKitRef {
724    fn eq(&self, other: &Self) -> bool {
725        std::sync::Arc::ptr_eq(&self.0, &other.0)
726    }
727}
728
729/// The helper behind [`JitOp::SlotCall`]: run a kit's closure over
730/// inputs gathered into the native frame, outputs to scatter from it,
731/// and the state's scratch entries from `base`. A panic in the closure
732/// is the node's own failure and is re-raised through the longjmp path
733/// like every other helper's.
734///
735/// # Safety
736/// Called only from generated code, which passes the kit the step was
737/// compiled with (kept alive by the code that calls it), frame arrays
738/// of the stated lengths, and the scratch the calling state owns, laid
739/// out as the builder placed the kit's entries.
740extern "C" fn jit_slot_call(
741    kit: *const crate::ast::CompiledSlotKit,
742    inputs: *const u64,
743    n_in: u64,
744    outputs: *mut u64,
745    n_out: u64,
746    scratch: *mut crate::ast::ScratchBuf,
747    base: u64,
748    n_scratch: u64,
749) {
750    guarded(|| unsafe {
751        let kit = &*kit;
752        let ins = std::slice::from_raw_parts(inputs, n_in as usize);
753        let outs = std::slice::from_raw_parts_mut(outputs, n_out as usize);
754        let sc = std::slice::from_raw_parts_mut(scratch.add(base as usize), n_scratch as usize);
755        (kit.op)(ins, outs, sc)
756    })
757}
758
759impl JitOp {
760    /// The kit a slot call runs, if this is one.
761    pub(crate) fn slot_kit(&self) -> Option<&SlotKitRef> {
762        match self {
763            JitOp::SlotCall { kit, .. } => Some(kit),
764            _ => None,
765        }
766    }
767
768    /// The scratch entries the step needs in the state that runs it.
769    pub(crate) fn scratch_elems(&self) -> &[crate::ast::ScratchElem] {
770        const STR_ENTRY: [crate::ast::ScratchElem; 1] = [crate::ast::ScratchElem::Str];
771        const F32_ENTRY: [crate::ast::ScratchElem; 1] = [crate::ast::ScratchElem::F32];
772        match self {
773            JitOp::SlotCall { kit, .. } => &kit.0.scratch,
774            JitOp::U64ToStr { .. }
775            | JitOp::I64ToStr { .. }
776            | JitOp::F64ToStr { .. }
777            | JitOp::StrConcat { .. }
778            | JitOp::JsonToStr { .. } => &STR_ENTRY,
779            JitOp::VecProduce { .. } => &F32_ENTRY,
780            _ => &[],
781        }
782    }
783
784    /// Place the step's scratch entries at `base` in the state's
785    /// scratch; the builder that lays the state out calls this once.
786    pub(crate) fn place_scratch(&mut self, base: usize) {
787        match self {
788            JitOp::SlotCall { scratch_base, .. }
789            | JitOp::U64ToStr { scratch_base }
790            | JitOp::I64ToStr { scratch_base }
791            | JitOp::F64ToStr { scratch_base }
792            | JitOp::StrConcat { scratch_base }
793            | JitOp::JsonToStr { scratch_base }
794            | JitOp::VecProduce { scratch_base, .. } => *scratch_base = base,
795            _ => {}
796        }
797    }
798}
799
800/// The vector producers with a named lowering (compiled_handles.md
801/// §6): each writes its result into the step's own `F32` entry and
802/// publishes the pair.
803#[derive(Debug, Clone, Copy, PartialEq, Eq)]
804pub enum VecProducer {
805    /// `vec_add(a, b)` over two `vec_f32` wires.
806    Add,
807    /// `vec_scale(a, k)` over a `vec_f32` and an `f64` wire.
808    Scale,
809    /// `vec_norm(a)` over a `vec_f32` wire.
810    Norm,
811    /// `hash_vec(seed, dim)`.
812    HashVec,
813    /// `xxhash3_vec(seed, dim)`.
814    XxHash3Vec,
815    /// `reg_to_vec_f32(r)`.
816    RegToVec,
817}
818
819/// The vector reductions with a named lowering: each returns the
820/// bits of its `f64` result.
821#[derive(Debug, Clone, Copy, PartialEq, Eq)]
822pub enum VecReducer {
823    /// `vec_dot(a, b)`.
824    Dot,
825    /// `vec_l2(a, b)`.
826    L2,
827    /// `vec_cosine(a, b)`.
828    Cosine,
829    /// `lid_mle(distances, k)`.
830    LidMle,
831}
832
833/// The register lane reads with a named lowering: each returns the
834/// lane as the slot word its port stores.
835#[derive(Debug, Clone, Copy, PartialEq, Eq)]
836pub enum RegLaneRead {
837    /// `reg_lane_f32(r, i)`, widened to f64.
838    F32,
839    /// `reg_lane_i16(r, i)`, sign-extended.
840    I16,
841    /// `reg_lane_i64(r, i)`.
842    I64,
843}
844
845/// The register producers that run through a helper: a bounds check
846/// on a wire index, or a lane operation Cranelift has no instruction
847/// for. Each writes its word into the output slots.
848#[derive(Debug, Clone, Copy, PartialEq, Eq)]
849pub enum RegProducer {
850    /// `reg_with_lane_f32(r, i, v)`.
851    WithLaneF32,
852    /// `reg_gather_f32(v, offset)`.
853    GatherF32,
854    /// `vec_to_reg_f32(v)`.
855    VecToRegF32,
856    /// `reg_mul_i8(a, b)`.
857    MulI8,
858}
859
860/// The `vec_f32` slice a wire's pair names.
861///
862/// # Safety
863/// The pair was published by its producing step into storage alive
864/// until that step reruns (axioms S3, S4), and the wire is a `vec_f32`.
865unsafe fn vec_f32_of<'a>(ptr: u64, len: u64) -> &'a [f32] {
866    if len == 0 {
867        &[]
868    } else {
869        unsafe { std::slice::from_raw_parts(ptr as usize as *const f32, len as usize) }
870    }
871}
872
873/// Write a vector into the step's own `F32` entry and publish its
874/// pair into the output slots: what every vector producer's lowering
875/// does around its arithmetic. `f` fills the entry.
876///
877/// # Safety
878/// As for [`write_str_entry`].
879unsafe fn write_f32_entry(
880    scratch: *mut crate::ast::ScratchBuf,
881    base: u64,
882    buffer: *mut u64,
883    out_slot: u64,
884    f: impl FnOnce(&mut Vec<f32>),
885) {
886    unsafe {
887        let entry = &mut *scratch.add(base as usize);
888        let crate::ast::ScratchBuf::F32(v) = entry else {
889            panic!("a vector lowering's scratch entry is not an f32 vector");
890        };
891        f(v);
892        *buffer.add(out_slot as usize) = v.as_ptr() as usize as u64;
893        *buffer.add(out_slot as usize + 1) = v.len() as u64;
894    }
895}
896
897/// The vector producers natively: the step's input words in order
898/// (`w0..w3`, zero past the last), the same body the node runs, into
899/// the entry.
900macro_rules! vec_producer {
901    ($name:ident, |$out:ident, $w0:ident, $w1:ident, $w2:ident, $w3:ident| $body:expr) => {
902        extern "C" fn $name(
903            scratch: *mut crate::ast::ScratchBuf,
904            base: u64,
905            buffer: *mut u64,
906            out_slot: u64,
907            $w0: u64,
908            $w1: u64,
909            $w2: u64,
910            $w3: u64,
911        ) {
912            guarded(|| unsafe {
913                let _ = ($w2, $w3);
914                write_f32_entry(scratch, base, buffer, out_slot, |$out| $body)
915            })
916        }
917    };
918}
919
920vec_producer!(jit_vec_add, |out, a_ptr, a_len, b_ptr, b_len| {
921    let (a, b) = (vec_f32_of(a_ptr, a_len), vec_f32_of(b_ptr, b_len));
922    crate::numeric::vector::check_lens("vec_add", a.len(), b.len());
923    crate::numeric::vector::add_f32_into(a, b, out)
924});
925vec_producer!(jit_vec_scale, |out, a_ptr, a_len, k_bits, _z| {
926    let a = vec_f32_of(a_ptr, a_len);
927    crate::numeric::vector::scale_f32_into(a, f64::from_bits(k_bits) as f32, out)
928});
929vec_producer!(jit_vec_norm, |out, a_ptr, a_len, _y, _z| {
930    crate::numeric::vector::norm_f32_into(vec_f32_of(a_ptr, a_len), out)
931});
932vec_producer!(jit_hash_vec, |out, seed, dim, _y, _z| {
933    crate::numeric::vector::hash_vec_into(seed, dim, out)
934});
935vec_producer!(jit_xxhash3_vec, |out, seed, dim, _y, _z| {
936    crate::numeric::vector::xxhash3_vec_into(seed, dim, out)
937});
938vec_producer!(jit_reg_to_vec_f32, |out, lo, hi, _y, _z| {
939    out.clear();
940    out.extend_from_slice(&crate::ast::Bits128([lo, hi]).lanes_f32())
941});
942
943/// The vector reductions natively: the input words in order, the
944/// result's bits back.
945macro_rules! vec_reducer {
946    ($name:ident, |$w0:ident, $w1:ident, $w2:ident, $w3:ident| $body:expr) => {
947        extern "C" fn $name($w0: u64, $w1: u64, $w2: u64, $w3: u64) -> u64 {
948            guarded(|| unsafe {
949                let _ = ($w2, $w3);
950                let r: f64 = $body;
951                r.to_bits()
952            })
953        }
954    };
955}
956
957vec_reducer!(jit_vec_dot, |a_ptr, a_len, b_ptr, b_len| {
958    let (a, b) = (vec_f32_of(a_ptr, a_len), vec_f32_of(b_ptr, b_len));
959    crate::numeric::vector::check_lens("vec_dot", a.len(), b.len());
960    crate::numeric::vector::dot_f32(a, b) as f64
961});
962vec_reducer!(jit_vec_l2, |a_ptr, a_len, b_ptr, b_len| {
963    let (a, b) = (vec_f32_of(a_ptr, a_len), vec_f32_of(b_ptr, b_len));
964    crate::numeric::vector::check_lens("vec_l2", a.len(), b.len());
965    (crate::numeric::vector::l2sq_f32(a, b) as f64).sqrt()
966});
967vec_reducer!(jit_vec_cosine, |a_ptr, a_len, b_ptr, b_len| {
968    let (a, b) = (vec_f32_of(a_ptr, a_len), vec_f32_of(b_ptr, b_len));
969    crate::numeric::vector::check_lens("vec_cosine", a.len(), b.len());
970    crate::numeric::vector::cosine_f32(a, b)
971});
972vec_reducer!(jit_lid_mle, |d_ptr, d_len, k_bits, _z| {
973    crate::numeric::vector::lid_mle_of(vec_f32_of(d_ptr, d_len), f64::from_bits(k_bits))
974});
975
976/// `reg_lane_f32` natively: the lane widened, as f64 bits.
977extern "C" fn jit_reg_lane_f32(lo: u64, hi: u64, i: u64) -> u64 {
978    guarded(|| crate::numeric::register::lane_f32(crate::ast::Bits128([lo, hi]), i).to_bits())
979}
980
981/// `reg_lane_i16` natively: the lane sign-extended into the slot word.
982extern "C" fn jit_reg_lane_i16(lo: u64, hi: u64, i: u64) -> u64 {
983    guarded(|| crate::numeric::register::lane_i16(crate::ast::Bits128([lo, hi]), i) as i64 as u64)
984}
985
986/// `reg_lane_i64` natively.
987extern "C" fn jit_reg_lane_i64(lo: u64, hi: u64, i: u64) -> u64 {
988    guarded(|| crate::numeric::register::lane_i64(crate::ast::Bits128([lo, hi]), i) as u64)
989}
990
991/// The register producers natively: the input words in order, the
992/// word into the two output slots.
993macro_rules! reg_producer {
994    ($name:ident, |$w0:ident, $w1:ident, $w2:ident, $w3:ident| $body:expr) => {
995        extern "C" fn $name(
996            buffer: *mut u64,
997            out_slot: u64,
998            $w0: u64,
999            $w1: u64,
1000            $w2: u64,
1001            $w3: u64,
1002        ) {
1003            guarded(|| unsafe {
1004                let _ = ($w2, $w3);
1005                let r: crate::ast::Bits128 = $body;
1006                *buffer.add(out_slot as usize) = r.0[0];
1007                *buffer.add(out_slot as usize + 1) = r.0[1];
1008            })
1009        }
1010    };
1011}
1012
1013reg_producer!(jit_reg_with_lane_f32, |lo, hi, i, v_bits| {
1014    crate::numeric::register::with_lane_f32(
1015        crate::ast::Bits128([lo, hi]),
1016        i,
1017        f64::from_bits(v_bits),
1018    )
1019});
1020reg_producer!(jit_reg_gather_f32, |v_ptr, v_len, offset, _z| {
1021    crate::numeric::register::gather_f32(vec_f32_of(v_ptr, v_len), offset)
1022});
1023reg_producer!(jit_vec_to_reg_f32, |v_ptr, v_len, _y, _z| {
1024    crate::numeric::register::to_reg_f32(vec_f32_of(v_ptr, v_len))
1025});
1026reg_producer!(jit_reg_mul_i8, |a_lo, a_hi, b_lo, b_hi| {
1027    crate::numeric::register::mul_i8(
1028        crate::ast::Bits128([a_lo, a_hi]),
1029        crate::ast::Bits128([b_lo, b_hi]),
1030    )
1031});
1032
1033/// Write a string into the step's own entry and publish its pair into
1034/// the output slots: what every named string lowering does around its
1035/// formatting. `f` fills the cleared entry.
1036///
1037/// # Safety
1038/// Called only from the helpers below, with the state's scratch and
1039/// the entry index the builder placed for the step, and the buffer and
1040/// output slot the step writes.
1041unsafe fn write_str_entry(
1042    scratch: *mut crate::ast::ScratchBuf,
1043    base: u64,
1044    buffer: *mut u64,
1045    out_slot: u64,
1046    f: impl FnOnce(&mut Vec<u8>),
1047) {
1048    unsafe {
1049        let entry = &mut *scratch.add(base as usize);
1050        let crate::ast::ScratchBuf::Str(v) = entry else {
1051            panic!("a string lowering's scratch entry is not a string");
1052        };
1053        v.clear();
1054        f(v);
1055        *buffer.add(out_slot as usize) = v.as_ptr() as usize as u64;
1056        *buffer.add(out_slot as usize + 1) = v.len() as u64;
1057    }
1058}
1059
1060/// `__u64_to_string` natively: the digits straight into the entry.
1061extern "C" fn jit_u64_to_str(
1062    scratch: *mut crate::ast::ScratchBuf,
1063    base: u64,
1064    buffer: *mut u64,
1065    out_slot: u64,
1066    value: u64,
1067) {
1068    use std::io::Write;
1069    guarded(|| unsafe {
1070        write_str_entry(scratch, base, buffer, out_slot, |v| {
1071            write!(v, "{value}").expect("a vector accepts every write")
1072        })
1073    })
1074}
1075
1076/// `__i64_to_string` natively.
1077extern "C" fn jit_i64_to_str(
1078    scratch: *mut crate::ast::ScratchBuf,
1079    base: u64,
1080    buffer: *mut u64,
1081    out_slot: u64,
1082    value: u64,
1083) {
1084    use std::io::Write;
1085    guarded(|| unsafe {
1086        write_str_entry(scratch, base, buffer, out_slot, |v| {
1087            write!(v, "{}", value as i64).expect("a vector accepts every write")
1088        })
1089    })
1090}
1091
1092/// `__f64_to_string` natively: `Display`, which is what `to_string`
1093/// writes on the interpreter.
1094extern "C" fn jit_f64_to_str(
1095    scratch: *mut crate::ast::ScratchBuf,
1096    base: u64,
1097    buffer: *mut u64,
1098    out_slot: u64,
1099    bits: u64,
1100) {
1101    use std::io::Write;
1102    guarded(|| unsafe {
1103        write_str_entry(scratch, base, buffer, out_slot, |v| {
1104            write!(v, "{}", f64::from_bits(bits)).expect("a vector accepts every write")
1105        })
1106    })
1107}
1108
1109/// `str_concat` over string wires natively: every pair's bytes
1110/// appended in order. `pairs` holds `n` `(ptr, len)` pairs the
1111/// generated code stored into its frame.
1112extern "C" fn jit_str_concat(
1113    scratch: *mut crate::ast::ScratchBuf,
1114    base: u64,
1115    buffer: *mut u64,
1116    out_slot: u64,
1117    pairs: *const u64,
1118    n: u64,
1119) {
1120    guarded(|| unsafe {
1121        let words = std::slice::from_raw_parts(pairs, 2 * n as usize);
1122        write_str_entry(scratch, base, buffer, out_slot, |v| {
1123            for pair in words.as_chunks::<2>().0 {
1124                // SAFETY: each pair was published by its producing step
1125                // into storage alive until that step reruns (axioms S3,
1126                // S4), and the wire is a string.
1127                let bytes =
1128                    std::slice::from_raw_parts(pair[0] as usize as *const u8, pair[1] as usize);
1129                v.extend_from_slice(bytes);
1130            }
1131        })
1132    })
1133}
1134
1135/// `json_to_str` natively: the compact serialization straight into
1136/// the entry, the bytes `serde_json::Value::to_string` produces.
1137extern "C" fn jit_json_to_str(
1138    scratch: *mut crate::ast::ScratchBuf,
1139    base: u64,
1140    buffer: *mut u64,
1141    out_slot: u64,
1142    ptr: u64,
1143    len: u64,
1144) {
1145    guarded(|| unsafe {
1146        let pair = [ptr, len];
1147        let value = crate::derive_support::ref_value(&pair);
1148        let json = match value {
1149            crate::ast::Value::Json(j) => j.as_ref(),
1150            other => panic!("expected Json wire, got {other:?}"),
1151        };
1152        write_str_entry(scratch, base, buffer, out_slot, |v| {
1153            serde_json::to_writer(v, json).expect("a vector accepts every write")
1154        })
1155    })
1156}
1157
1158/// Classify a node with the types of its wire inputs known. A node
1159/// with a named native lowering takes it; any other node with a kit
1160/// is a [`JitOp::SlotCall`] of that kit, so a reference pair on either
1161/// side is no bar to native code, and neither is nondeterminism or a
1162/// side effect: the kernels that run the code keep such a step's
1163/// currency its own (a segment of its own on the hybrid kernel, a
1164/// never-current step on pure native code). A node with neither stays
1165/// interpreted.
1166pub fn classify_node_typed(node: &dyn PolydatNode, wire_types: &[crate::ast::PortType]) -> JitOp {
1167    use crate::ast::PortType as PT;
1168    let is_ref = |t: &crate::ast::PortType| t.slot_color() == crate::ast::SlotColor::Ref2;
1169    let vec_produce = |kind: VecProducer| JitOp::VecProduce {
1170        kind,
1171        scratch_base: 0,
1172    };
1173    let ref_copy = |ty: crate::ast::PortType| {
1174        crate::compile::assembly::ref_copy_kit(ty)
1175            .map(|kit| JitOp::SlotCall {
1176                kit: SlotKitRef::new(kit),
1177                scratch_base: 0,
1178            })
1179            .unwrap_or(JitOp::Fallback)
1180    };
1181    let meta = node.meta();
1182    let named = match meta.name.as_str() {
1183        // The string producers with a lowering that writes straight
1184        // into the step's entry (compiled_handles.md §6).
1185        "__u64_to_string" => JitOp::U64ToStr { scratch_base: 0 },
1186        "__i64_to_string" => JitOp::I64ToStr { scratch_base: 0 },
1187        "__f64_to_string" => JitOp::F64ToStr { scratch_base: 0 },
1188        "json_to_str" if wire_types == [crate::ast::PortType::Json] => {
1189            JitOp::JsonToStr { scratch_base: 0 }
1190        }
1191        "str_concat"
1192            if !wire_types.is_empty()
1193                && wire_types.iter().all(|t| *t == crate::ast::PortType::Str) =>
1194        {
1195            JitOp::StrConcat { scratch_base: 0 }
1196        }
1197        // The vector group: each lowering runs the node's own body on
1198        // the wires' slices and writes into the step's entry or
1199        // returns its scalar. Keyed on the exact wire types the body
1200        // is written for; any adapted shape takes the kit.
1201        "vec_add" if wire_types == [PT::VecF32, PT::VecF32] => vec_produce(VecProducer::Add),
1202        "vec_scale" if wire_types == [PT::VecF32, PT::F64] => vec_produce(VecProducer::Scale),
1203        "vec_norm" if wire_types == [PT::VecF32] => vec_produce(VecProducer::Norm),
1204        "hash_vec" if wire_types == [PT::U64, PT::U64] => vec_produce(VecProducer::HashVec),
1205        "xxhash3_vec" if wire_types == [PT::U64, PT::U64] => vec_produce(VecProducer::XxHash3Vec),
1206        "reg_to_vec_f32" if wire_types == [PT::RegF32x4] => vec_produce(VecProducer::RegToVec),
1207        "vec_dot" if wire_types == [PT::VecF32, PT::VecF32] => JitOp::VecReduce(VecReducer::Dot),
1208        "vec_l2" if wire_types == [PT::VecF32, PT::VecF32] => JitOp::VecReduce(VecReducer::L2),
1209        "vec_cosine" if wire_types == [PT::VecF32, PT::VecF32] => {
1210            JitOp::VecReduce(VecReducer::Cosine)
1211        }
1212        "lid_mle" if wire_types == [PT::VecF32, PT::F64] => JitOp::VecReduce(VecReducer::LidMle),
1213        // The register group: the lane reads and the producers with a
1214        // bounds check run through a helper; the dot product and the
1215        // byte shuffle are inline vector instructions.
1216        "reg_lane_f32" if wire_types == [PT::RegF32x4, PT::U64] => JitOp::RegLane(RegLaneRead::F32),
1217        "reg_lane_i16" if wire_types == [PT::RegI16x8, PT::U64] => JitOp::RegLane(RegLaneRead::I16),
1218        "reg_lane_i64" if wire_types == [PT::RegI64x2, PT::U64] => JitOp::RegLane(RegLaneRead::I64),
1219        "reg_with_lane_f32" if wire_types == [PT::RegF32x4, PT::U64, PT::F64] => {
1220            JitOp::RegProduce(RegProducer::WithLaneF32)
1221        }
1222        "reg_gather_f32" if wire_types == [PT::VecF32, PT::U64] => {
1223            JitOp::RegProduce(RegProducer::GatherF32)
1224        }
1225        "vec_to_reg_f32" if wire_types == [PT::VecF32] => {
1226            JitOp::RegProduce(RegProducer::VecToRegF32)
1227        }
1228        "reg_mul_i8" if wire_types == [PT::RegI8x16, PT::RegI8x16] => {
1229            JitOp::RegProduce(RegProducer::MulI8)
1230        }
1231        "reg_dot_f32" if wire_types == [PT::RegF32x4, PT::RegF32x4] => JitOp::RegDotF32,
1232        // The compiler's input passthrough and `default_or(value,
1233        // fallback)` (`value` unless it is `None`, which a compiled slot
1234        // never carries; engine_parity.md, A12): a slot copy of an
1235        // immediate, a copy into the step's own scratch of a reference
1236        // value (axiom S3: a pair is never forwarded).
1237        n if n.starts_with("__port_") || n == "default_or" => match meta.outs.first() {
1238            Some(o) if is_ref(&o.typ) => return ref_copy(o.typ),
1239            _ => JitOp::Identity,
1240        },
1241        // The named selects are native only between one-slot
1242        // immediates; any other shape takes the node's kit below.
1243        "select" | "select_u64" if wire_types.iter().skip(1).any(|t| t.slot_width() != 1) => {
1244            JitOp::Fallback
1245        }
1246        _ if wire_types.iter().any(is_ref) || meta.outs.iter().any(|o| is_ref(&o.typ)) => {
1247            JitOp::Fallback
1248        }
1249        _ => classify_node(node),
1250    };
1251    if !matches!(named, JitOp::Fallback) {
1252        return named;
1253    }
1254    if let Some(kit) = node.compiled_slot(wire_types) {
1255        return JitOp::SlotCall {
1256            kit: SlotKitRef::new(kit),
1257            scratch_base: 0,
1258        };
1259    }
1260    if let Some(op) = node.compiled_u64() {
1261        return JitOp::SlotCall {
1262            kit: SlotKitRef::new(crate::ast::CompiledSlotKit {
1263                scratch: Vec::new(),
1264                op: Box::new(move |inputs, outputs, _| op(inputs, outputs)),
1265            }),
1266            scratch_base: 0,
1267        };
1268    }
1269    JitOp::Fallback
1270}
1271
1272// ── JitOp ──────────────────────────────────────────────────
1273
1274/// Description of a JIT step — what operation to generate.
1275///
1276/// For f64 operations, values are stored in the u64 buffer as their
1277/// bit representation. Cranelift `bitcast` converts between i64/f64.
1278#[derive(Debug, Clone, PartialEq)]
1279pub enum JitOp {
1280    // --- u64 integer ops ---
1281    /// `output[i] = input[i]` for every slot the port spans  (identity / copy)
1282    Identity,
1283    /// `output[0] = input[0] + constant`
1284    AddConst(u64),
1285    /// `output[0] = input[0] * constant`
1286    MulConst(u64),
1287    /// `output[0] = input[0] / constant`
1288    DivConst(u64),
1289    /// `output[0] = input[0] % constant`
1290    ModConst(u64),
1291    /// `output[0] = clamp(input[0], min, max)`  (unsigned)
1292    ClampConst(u64, u64),
1293    /// `output[0] = interleave_bits(input[0], input[1])`  (extern call)
1294    Interleave,
1295    /// `output[i] = mixed-radix decomposition of input[0]`  (inline urem/udiv)
1296    MixedRadixConst(Vec<u64>),
1297    /// `output[0] = xxh3_hash(input[0])`  (extern call)
1298    Hash,
1299    /// `output[0] = splitmix64(input[0]) (fully inlined 64-bit ALU bit mixer)`
1300    SplitMix64,
1301    /// `output[0] = popcount(input[0])`
1302    Popcnt,
1303    /// `output[0] = leading_zeros(input[0])`
1304    Clz,
1305    /// `output[0] = trailing_zeros(input[0])`
1306    Ctz,
1307    /// `output[0] = byte_swap(input[0])`
1308    Bswap,
1309    /// `output[0] = shuffle(input[0])`  (extern call: feedback, size, min)
1310    ShuffleConst(u64, u64, u64),
1311
1312    // --- f64 ops (values stored as u64 bits in buffer) ---
1313    /// `output[0] = input[0] as f64 / u64::MAX as f64`  (u64 → f64 bits)
1314    UnitInterval,
1315    /// `output[0] = f64::from_bits(input[0]) as u64`  (f64 bits → u64, truncate)
1316    F64ToU64,
1317    /// `output[0] = f64::from_bits(input[0]).round() as u64`: half
1318    /// away from zero, as Rust rounds, then the saturating conversion.
1319    RoundToU64,
1320    /// `output[0] = f64::from_bits(input[0]).floor() as u64`
1321    FloorToU64,
1322    /// `output[0] = f64::from_bits(input[0]).ceil() as u64`
1323    CeilToU64,
1324    /// `output[0] = clamp(f64::from_bits(input[0]), min, max)`  → f64 bits
1325    ClampF64Const(u64, u64), // min.to_bits(), max.to_bits()
1326    /// `output[0] = a + (b - a) * f64::from_bits(input[0])`  → f64 bits
1327    LerpConst(u64, u64), // a.to_bits(), b.to_bits()
1328    /// `output[0] = min + range * (input[0] as f64 / MAX)`  → f64 bits  (u64 input)
1329    ScaleRangeConst(u64, u64), // min.to_bits(), range.to_bits()
1330    /// `output[0] = round(f64::from_bits(input[0]) / step) * step`  → f64 bits
1331    QuantizeConst(u64), // step.to_bits()
1332    /// `output[0] = discretize(f64 input, range, buckets)`  → u64
1333    DiscretizeConst(u64, u64), // range.to_bits(), buckets
1334    /// `output[0] = lut_sample(f64 input, lut_ptr, lut_len)`  → f64 bits  (extern call)
1335    LutSampleConst(u64, u64), // lut_ptr as u64, lut_len
1336    /// `output[0] = weighted_pick(input, values_ptr, biases_ptr, primaries_ptr, aliases_ptr, n)`
1337    WeightedPickConst(u64, u64, u64, u64, u64), // values_ptr, biases_ptr, primaries_ptr, aliases_ptr, n
1338
1339    /// Unary f64 math function via extern call. The u8 identifies which function.
1340    /// 0=sin 1=cos 2=tan 3=asin 4=acos 5=atan 6=sqrt 7=abs 8=ln 9=exp
1341    /// 10=floor_base10 11=ceiling_base10 12=closest_base10
1342    /// 13=floor_decade 14=ceiling_decade 15=closest_decade
1343    /// 16=floor_binomial 17=ceiling_binomial 18=closest_binomial
1344    /// 19=floor_fibonacci 20=ceiling_fibonacci 21=closest_fibonacci
1345    MathUnary(u8),
1346    /// Binary f64 math function via extern call.
1347    /// 0=atan2 1=pow 2=round_nearest 3=round_floor 4=round_ceiling
1348    MathBinary(u8),
1349
1350    // --- Two-wire u64 integer ops ---
1351    /// output = `input[0]` + `input[1]`  (wrapping)
1352    U64Add2,
1353    /// output = `input[0]` - `input[1]`  (wrapping)
1354    U64Sub2,
1355    /// output = `input[0]` * `input[1]`  (wrapping)
1356    U64Mul2,
1357    /// output = `input[0]` / `input[1]`  (0 if divisor is 0)
1358    U64Div2,
1359    /// output = `input[0]` % `input[1]`  (0 if divisor is 0)
1360    U64Mod2,
1361    /// output = `input[0]` & `input[1]`
1362    U64And,
1363    /// output = `input[0]` | `input[1]`
1364    U64Or,
1365    /// output = `input[0]` ^ `input[1]`
1366    U64Xor,
1367    /// output = `input[0]` << `input[1]`
1368    U64Shl,
1369    /// output = `input[0]` >> `input[1]`  (logical)
1370    U64Shr,
1371    /// output = !`input[0]`  (unary bitwise NOT)
1372    U64Not,
1373
1374    // --- Inline binary f64 arithmetic (no extern call) ---
1375    /// output = input as f64 (integer to float conversion, not bit reinterpret)
1376    ToF64,
1377
1378    /// output = f64(a) + f64(b)
1379    F64Add,
1380    /// output = f64(a) - f64(b)
1381    F64Sub,
1382    /// output = f64(a) * f64(b)
1383    F64Mul,
1384    /// output = f64(a) / f64(b) (0 if b==0)
1385    F64Div,
1386    /// output = f64(a) % f64(b) (0 if b==0), through `jit_f64_mod`
1387    F64Mod,
1388    /// `output[0] = input[0] / input[1]`, failing on a zero divisor as
1389    /// the body's `/` does (`div_wire`)
1390    U64DivWire,
1391    /// `output[0] = input[0] % input[1]`, failing on a zero divisor as
1392    /// the body's `%` does (`mod_wire`)
1393    U64ModWire,
1394
1395    /// A call of the node's own slot kit from native code
1396    /// (compiled_handles.md §6): the inputs are gathered into the
1397    /// frame, `jit_slot_call` runs the kit's closure over them and the
1398    /// state's scratch entries at `scratch_base`, and the outputs are
1399    /// scattered back. Every node with a kit lowers this way, so a
1400    /// reference pair rides through a segment or a cone as it rides
1401    /// through a closure step.
1402    SlotCall {
1403        /// The kit, shared by every kernel compiled from the program
1404        /// and kept alive by the code that calls it.
1405        kit: SlotKitRef,
1406        /// Index of the kit's first scratch entry in the state's
1407        /// scratch, assigned by the builder that lays the state out.
1408        scratch_base: usize,
1409    },
1410
1411    // --- Named lowerings that write a string into the step's own
1412    // entry (compiled_handles.md §6): no intermediate `String`, no
1413    // frame, the pair published by the helper. Each owns one `Str`
1414    // scratch entry at `scratch_base`.
1415    /// `output = decimal digits of input[0] as a u64`
1416    U64ToStr {
1417        /// The step's string entry in the state's scratch.
1418        scratch_base: usize,
1419    },
1420    /// `output = decimal digits of input[0] as an i64`
1421    I64ToStr {
1422        /// The step's string entry in the state's scratch.
1423        scratch_base: usize,
1424    },
1425    /// `output = Display form of input[0] as an f64`
1426    F64ToStr {
1427        /// The step's string entry in the state's scratch.
1428        scratch_base: usize,
1429    },
1430    /// `output = the concatenation of every input pair's bytes`, for
1431    /// a `str_concat` whose wires are all strings.
1432    StrConcat {
1433        /// The step's string entry in the state's scratch.
1434        scratch_base: usize,
1435    },
1436    /// `output = compact serialization of the JSON value input[0..2] names`
1437    JsonToStr {
1438        /// The step's string entry in the state's scratch.
1439        scratch_base: usize,
1440    },
1441
1442    // --- The vector and register groups (compiled_handles.md §6) ---
1443    /// `output = a vec_f32 written into the step's own `F32` entry`
1444    /// by the producer's body over the input words.
1445    VecProduce {
1446        /// Which producer.
1447        kind: VecProducer,
1448        /// The step's `F32` entry in the state's scratch.
1449        scratch_base: usize,
1450    },
1451    /// `output[0] = f64 bits of the reduction over the input words`
1452    VecReduce(VecReducer),
1453    /// `output[0] = lane input[2] of the register word input[0..2]`,
1454    /// bounds-checked by the helper.
1455    RegLane(RegLaneRead),
1456    /// `output[0..2] = the producer's word over the input words`
1457    RegProduce(RegProducer),
1458    /// `output[0] = ((a0*b0 + a1*b1) + (a2*b2 + a3*b3)) as f64`, the
1459    /// fixed tree of `reg_dot_f32`, over f32x4 words: one `fmul`,
1460    /// four lane extracts, three adds, one promotion.
1461    RegDotF32,
1462    /// `output[0..2] = byte permutation of input[0..2]` by a baked
1463    /// 16-entry mask, one `shuffle`.
1464    RegShuffleConst([u8; 16]),
1465
1466    /// Parameter predicate: pass `input[0]` through to `output[0]`;
1467    /// if `input[0]` == 0, call `jit_is_positive_fail` (panics)
1468    /// with the configured predicate name — (ptr, len) into the
1469    /// node's meta const, (0, 0) for the default. Message parity
1470    /// with the interpreter's `is_positive({name}): …` is asserted
1471    /// by the SRD-105 battery.
1472    IsPositiveCheck {
1473        /// Address of the predicate's name, or 0 for the default.
1474        name_ptr: u64,
1475        /// Its length in bytes.
1476        name_len: u64,
1477    },
1478    /// Parameter predicate: pass `input[0]` through to `output[0]`;
1479    /// if `input[0]` < lo or `input[0]` > hi, call
1480    /// `jit_in_range_fail` (panics). Stored as (lo, hi).
1481    InRangeCheck(u64, u64),
1482    /// Parameter predicate: pass `input[0]` through to `output[0]`;
1483    /// if `input[0]` is not in the allow-list, call
1484    /// `jit_is_one_of_fail` (panics) with the allow-list contents
1485    /// — (ptr, len) into the node's meta VecU64 const, (0, 0)
1486    /// when unavailable. Message parity with the interpreter's
1487    /// `is_one_of: … not in allowed set […]` is asserted by the
1488    /// SRD-105 battery. Inline comparisons use the baked vector.
1489    IsOneOfCheck {
1490        /// The allow-list, baked into the comparisons.
1491        allowed: Vec<u64>,
1492        /// Address of the node's allow-list constant for the message, or 0.
1493        set_ptr: u64,
1494        /// Its length.
1495        set_len: u64,
1496    },
1497
1498    // --- Register-plane ops (type_system_alignment.md §3, native tier of §7) ---
1499    // A register value occupies two consecutive u64 slots; the
1500    // codegen emits one unaligned 128-bit load/store per value
1501    // (buffer is only 8-aligned) and a single vector instruction.
1502    /// Element-wise register binop. (lane_ty index, arith index)
1503    /// — lanes: 0=i8x16 1=i16x8 2=i32x4 3=i64x2 4=f32x4 5=f64x2;
1504    /// arith: 0=add 1=sub 2=mul.
1505    RegBinOp(u8, u8),
1506    /// View retag / two-slot copy (`__reg_view_*`): one 128-bit
1507    /// load + store; the lane typing is static, so no instruction
1508    /// beyond the move.
1509    RegCopy,
1510    /// Broadcast a scalar wire into all lanes. Same lane index
1511    /// vocabulary as `RegBinOp`; float lanes read the f64 slot
1512    /// and demote as needed, integer lanes reduce from u64.
1513    RegSplat(u8),
1514
1515    // --- Comparisons & selections (SRD 110) ---
1516    /// Integer comparison: `output[0]` = if a `<cond>` b { 1 } else { 0 }
1517    U64Cmp(ir::condcodes::IntCC),
1518    /// Float comparison: `output[0]` = if a `<cond>` b { 1 } else { 0 }
1519    F64Cmp(ir::condcodes::FloatCC),
1520    /// Conditional select for u64: `output[0]` = if cond != 0 { a } else { b }
1521    SelectU64,
1522    /// Conditional select for f64: `output[0]` = if cond != 0 { a } else { b }
1523    SelectF64,
1524
1525    // --- Type conversions & lattice adapters (SRD 110) ---
1526    /// Signed integer to float: `output[0]` = (`input[0]` as i64 as f64).to_bits()
1527    I64ToF64,
1528    /// Float to signed integer: `output[0]` = (f64::from_bits(`input[0]`) as i64) as u64
1529    F64ToI64,
1530    /// Sign-extend 32-bit integer: `output[0]` = ((`input[0]` as i32) as i64) as u64
1531    SignExtendI32,
1532    /// Sign-extend 16-bit integer: `output[0]` = ((`input[0]` as i16) as i64) as u64
1533    SignExtendI16,
1534    /// Sign-extend 8-bit integer: `output[0]` = ((`input[0]` as i8) as i64) as u64
1535    SignExtendI8,
1536    /// Zero-extend 32-bit integer: `output[0]` = (`input[0]` as u32) as u64
1537    ZeroExtendU32,
1538    /// Zero-extend 16-bit integer: `output[0]` = (`input[0]` as u16) as u64
1539    ZeroExtendU16,
1540    /// Zero-extend 8-bit integer: `output[0]` = (`input[0]` as u8) as u64
1541    ZeroExtendU8,
1542    /// Truthiness boolean coercion: `output[0]` = if `input[0]` != 0 { 1 } else { 0 }
1543    ToBool,
1544    /// Constant u64: `output[0]` = val
1545    ConstU64(u64),
1546    /// Constant f64: `output[0]` = val_bits
1547    ConstF64(u64),
1548
1549    // --- Interpolation & Hashing (SRD 110) ---
1550    /// Hash range: `output[0]` = if max == 0 { 0 } else { hash(`input[0]`) % max }
1551    HashRangeConst(u64),
1552    /// Hash interval: `output[0]` = min + (hash(`input[0]`) / MAX) * (max - min)
1553    HashIntervalConst(u64, u64),
1554    /// Inverse lerp: `output[0]` = ((`input[0]` - a) / (b - a)).clamp(0, 1)
1555    InvLerpConst(u64, u64),
1556    /// Remap: `output[0]` = out_min + ((`input[0]` - in_min) / (in_max - in_min)) * (out_max - out_min)
1557    RemapConst(u64, u64, u64, u64),
1558
1559    // --- Context & Datetime (SRD 110) ---
1560    /// Epoch offset: `output[0]` = `input[0]`.wrapping_add(base)
1561    EpochOffsetConst(u64),
1562    /// Epoch scale: `output[0]` = `input[0]`.wrapping_mul(factor)
1563    EpochScaleConst(u64),
1564    /// OS thread ID
1565    ThreadId,
1566    /// Wall clock millis
1567    CurrentEpochMillis,
1568
1569    // --- Coherent Noise (SRD 110) ---
1570    /// `output[0] = jit_perlin_1d(input[0], perm, freq)`: (permutation table address, frequency bits).
1571    Perlin1dConst(u64, u64),
1572    /// `jit_perlin_2d` over two inputs: (permutation table address, frequency bits).
1573    Perlin2dConst(u64, u64),
1574    /// `jit_simplex_2d` over two inputs: (permutation table address, frequency bits).
1575    Simplex2dConst(u64, u64),
1576    /// `jit_fractal_noise_1d`: (permutation table address, frequency bits, octaves).
1577    FractalNoise1dConst(u64, u64, u64),
1578    /// `jit_fractal_noise_2d` over two inputs: (permutation table address, frequency bits, octaves).
1579    FractalNoise2dConst(u64, u64, u64),
1580
1581    // --- Variadics & wire arithmetic (SRD 110) ---
1582    /// Variadic sum across all inputs
1583    VariadicSum,
1584    /// Variadic product across all inputs
1585    VariadicProduct,
1586    /// Variadic minimum across all inputs (unsigned)
1587    VariadicMin,
1588    /// Variadic maximum across all inputs (unsigned)
1589    VariadicMax,
1590    /// Checked unsigned addition: `output[0]` = a.checked_add(b).unwrap_or(0)
1591    CheckedAdd,
1592    /// Saturating unsigned subtraction: `output[0]` = a.saturating_sub(b)
1593    CheckedSub,
1594    /// Checked unsigned multiplication: `output[0]` = a.checked_mul(b).unwrap_or(0)
1595    CheckedMul,
1596    /// Smallest multiple of multiple >= value: `output[0]` = if m == 0 { v } else { v.div_ceil(m).saturating_mul(m) }
1597    CeilToMultiple,
1598    /// Multiples at least: `output[0]` = if m == 0 { 0 } else { v.div_ceil(m) }
1599    MultiplesAtLeast,
1600
1601    // --- Probability & permutations (SRD 110) ---
1602    /// Fair coin flip: `output[0]` = `input[0]` & 1
1603    FairCoin,
1604    /// Float blend with constant mix: `output[0]` = (fa * (1 - mix) + fb * mix).round() as u64
1605    BlendConst(u64),
1606    /// LFSR advance step with constant feedback polynomial:
1607    /// `output[0] = (input[0] >> 1) ^ (if input[0] & 1 != 0 { feedback } else { 0 })`
1608    LfsrStepConst(u64),
1609    /// PCG random with constant seed and stream: (seed, stream)
1610    PcgConst(u64, u64),
1611    /// PCG random with wire stream and constant seed: (seed)
1612    PcgStreamConst(u64),
1613    /// Cycle walk: (range, seed, inc)
1614    CycleWalkConst(u64, u64, u64),
1615    /// Unfair coin with constant probability: (p_bits)
1616    UnfairCoinConst(u64),
1617    /// `coin_flip`: the input compared unsigned against a threshold the
1618    /// node computed from its probability at construction; no hash.
1619    CoinFlipConst(u64),
1620    /// Chance with constant probability: (p_bits)
1621    ChanceConst(u64),
1622    /// N-of-M selection with constant n and m: (n, m)
1623    NOfConst(u64, u64),
1624
1625    /// Fallback: no native lowering; the node runs as a closure step
1626    /// on the hybrid kernel and stays interpreted otherwise.
1627    Fallback,
1628}
1629
1630// ── Node classification ────────────────────────────────────
1631
1632/// Classify a Polydat node into a JIT-able operation.
1633///
1634/// Uses `jit_constants()` to extract assembly-time constants
1635/// directly from the node — no probing hacks needed.
1636pub fn classify_node(node: &dyn PolydatNode) -> JitOp {
1637    let name = node.meta().name.as_str();
1638    let consts = node.jit_constants();
1639
1640    match name {
1641        "identity" => JitOp::Identity,
1642        "hash" | "splitmix64" | "scatter" => JitOp::SplitMix64,
1643        "fair_coin" => JitOp::FairCoin,
1644        "unfair_coin" | "bernoulli" => {
1645            if let Some(&p) = consts.first() {
1646                JitOp::UnfairCoinConst(p)
1647            } else {
1648                JitOp::Fallback
1649            }
1650        }
1651        "chance" => {
1652            if let Some(&p) = consts.first() {
1653                JitOp::ChanceConst(p)
1654            } else {
1655                JitOp::Fallback
1656            }
1657        }
1658        "popcnt" | "count_ones" | "popcount" => JitOp::Popcnt,
1659        "clz" | "leading_zeros" => JitOp::Clz,
1660        "ctz" | "trailing_zeros" => JitOp::Ctz,
1661        "bswap" | "swap_bytes" => JitOp::Bswap,
1662        "xxhash3" | "xxh3" => JitOp::Hash,
1663        "hash_range" => {
1664            if let Some(&c) = consts.first() {
1665                JitOp::HashRangeConst(c)
1666            } else {
1667                JitOp::Fallback
1668            }
1669        }
1670        "hash_interval" => {
1671            if consts.len() >= 2 {
1672                JitOp::HashIntervalConst(consts[0], consts[1])
1673            } else {
1674                JitOp::Fallback
1675            }
1676        }
1677        "add" => {
1678            if let Some(&c) = consts.first() {
1679                JitOp::AddConst(c)
1680            } else {
1681                JitOp::Fallback
1682            }
1683        }
1684        "mul" => {
1685            if let Some(&c) = consts.first() {
1686                JitOp::MulConst(c)
1687            } else {
1688                JitOp::Fallback
1689            }
1690        }
1691        "div" => {
1692            if let Some(&c) = consts.first() {
1693                JitOp::DivConst(c)
1694            } else {
1695                JitOp::Fallback
1696            }
1697        }
1698        "mod" => {
1699            if let Some(&c) = consts.first() {
1700                JitOp::ModConst(c)
1701            } else {
1702                JitOp::Fallback
1703            }
1704        }
1705        "clamp" => {
1706            if consts.len() >= 2 {
1707                JitOp::ClampConst(consts[0], consts[1])
1708            } else {
1709                JitOp::Fallback
1710            }
1711        }
1712        "interleave" => JitOp::Interleave,
1713        "mixed_radix" => {
1714            if consts.is_empty() {
1715                JitOp::Fallback
1716            } else {
1717                JitOp::MixedRadixConst(consts)
1718            }
1719        }
1720        "shuffle" => {
1721            if consts.len() >= 3 {
1722                JitOp::ShuffleConst(consts[0], consts[1], consts[2])
1723            } else {
1724                JitOp::Fallback
1725            }
1726        }
1727        // f64 ops
1728        "unit_interval" => JitOp::UnitInterval,
1729        "f64_to_u64" => JitOp::F64ToU64,
1730        "round_to_u64" => JitOp::RoundToU64,
1731        "floor_to_u64" => JitOp::FloorToU64,
1732        "ceil_to_u64" => JitOp::CeilToU64,
1733        "clamp_f64" => {
1734            if consts.len() >= 2 {
1735                JitOp::ClampF64Const(consts[0], consts[1])
1736            } else {
1737                JitOp::Fallback
1738            }
1739        }
1740        "lerp" => {
1741            if consts.len() >= 2 {
1742                JitOp::LerpConst(consts[0], consts[1])
1743            } else {
1744                JitOp::Fallback
1745            }
1746        }
1747        "scale_range" => {
1748            if consts.len() >= 2 {
1749                JitOp::ScaleRangeConst(consts[0], consts[1])
1750            } else {
1751                JitOp::Fallback
1752            }
1753        }
1754        "quantize" => {
1755            if let Some(&c) = consts.first() {
1756                JitOp::QuantizeConst(c)
1757            } else {
1758                JitOp::Fallback
1759            }
1760        }
1761        "discretize" => {
1762            if consts.len() >= 2 {
1763                JitOp::DiscretizeConst(consts[0], consts[1])
1764            } else {
1765                JitOp::Fallback
1766            }
1767        }
1768        "lut_sample" | "dist_normal" | "icd_normal" | "dist_exponential" | "icd_exponential"
1769        | "dist_uniform" | "dist_pareto" | "dist_zipf" | "dist_empirical" => {
1770            if consts.len() >= 2 {
1771                JitOp::LutSampleConst(consts[0], consts[1])
1772            } else {
1773                JitOp::Fallback
1774            }
1775        }
1776        // Math functions
1777        "sin" => JitOp::MathUnary(0),
1778        "cos" => JitOp::MathUnary(1),
1779        "tan" => JitOp::MathUnary(2),
1780        "asin" => JitOp::MathUnary(3),
1781        "acos" => JitOp::MathUnary(4),
1782        "atan" => JitOp::MathUnary(5),
1783        "sqrt" => JitOp::MathUnary(6),
1784        "abs_f64" => JitOp::MathUnary(7),
1785        "ln" => JitOp::MathUnary(8),
1786        "exp" => JitOp::MathUnary(9),
1787        "floor_base10" => JitOp::MathUnary(10),
1788        "ceiling_base10" => JitOp::MathUnary(11),
1789        "closest_base10" => JitOp::MathUnary(12),
1790        "floor_decade" => JitOp::MathUnary(13),
1791        "ceiling_decade" => JitOp::MathUnary(14),
1792        "closest_decade" => JitOp::MathUnary(15),
1793        "floor_binomial" => JitOp::MathUnary(16),
1794        "ceiling_binomial" => JitOp::MathUnary(17),
1795        "closest_binomial" => JitOp::MathUnary(18),
1796        "floor_fibonacci" => JitOp::MathUnary(19),
1797        "ceiling_fibonacci" => JitOp::MathUnary(20),
1798        "closest_fibonacci" => JitOp::MathUnary(21),
1799        "atan2" => JitOp::MathBinary(0),
1800        "pow" => JitOp::MathBinary(1),
1801        "round_nearest" => JitOp::MathBinary(2),
1802        "round_floor" => JitOp::MathBinary(3),
1803        "round_ceiling" => JitOp::MathBinary(4),
1804        "to_f64" => JitOp::ToF64,
1805        // Two-wire u64 ops (no constants)
1806        "u64_add" => JitOp::U64Add2,
1807        "u64_sub" => JitOp::U64Sub2,
1808        "u64_mul" => JitOp::U64Mul2,
1809        "u64_div" => JitOp::U64Div2,
1810        "u64_mod" => JitOp::U64Mod2,
1811        "u64_and" => JitOp::U64And,
1812        "u64_or" => JitOp::U64Or,
1813        "u64_xor" => JitOp::U64Xor,
1814        "u64_shl" => JitOp::U64Shl,
1815        "u64_shr" => JitOp::U64Shr,
1816        "u64_not" => JitOp::U64Not,
1817
1818        // ── Register plane (type_system_alignment.md §3, native tier of §7) ──
1819        "reg_add_i8" => JitOp::RegBinOp(0, 0),
1820        "reg_sub_i8" => JitOp::RegBinOp(0, 1),
1821        // `imul.i8x16` has no cranelift lowering (x86 has no
1822        // byte-lane multiply short of AVX-512; cranelift 0.116
1823        // rejects it in ISLE); `classify_node_typed` lowers
1824        // `reg_mul_i8` through its helper.
1825        "reg_shuffle_bytes" => {
1826            let mut mask = [0u8; 16];
1827            if consts.len() == 16 && consts.iter().all(|&m| m < 16) {
1828                for (m, &c) in mask.iter_mut().zip(consts.iter()) {
1829                    *m = c as u8;
1830                }
1831                JitOp::RegShuffleConst(mask)
1832            } else {
1833                JitOp::Fallback
1834            }
1835        }
1836        "reg_add_i16" => JitOp::RegBinOp(1, 0),
1837        "reg_sub_i16" => JitOp::RegBinOp(1, 1),
1838        "reg_mul_i16" => JitOp::RegBinOp(1, 2),
1839        "reg_add_i32" => JitOp::RegBinOp(2, 0),
1840        "reg_sub_i32" => JitOp::RegBinOp(2, 1),
1841        "reg_mul_i32" => JitOp::RegBinOp(2, 2),
1842        "reg_add_i64" => JitOp::RegBinOp(3, 0),
1843        "reg_sub_i64" => JitOp::RegBinOp(3, 1),
1844        "reg_mul_i64" => JitOp::RegBinOp(3, 2),
1845        "reg_add_f32" => JitOp::RegBinOp(4, 0),
1846        "reg_sub_f32" => JitOp::RegBinOp(4, 1),
1847        "reg_mul_f32" => JitOp::RegBinOp(4, 2),
1848        "reg_add_f64" => JitOp::RegBinOp(5, 0),
1849        "reg_sub_f64" => JitOp::RegBinOp(5, 1),
1850        "reg_mul_f64" => JitOp::RegBinOp(5, 2),
1851        "__reg_view_raw" | "__reg_view_i8x16" | "__reg_view_i16x8" | "__reg_view_i32x4"
1852        | "__reg_view_i64x2" | "__reg_view_f16x8" | "__reg_view_f32x4" | "__reg_view_f64x2" => {
1853            JitOp::RegCopy
1854        }
1855        "reg_splat_i8" => JitOp::RegSplat(0),
1856        "reg_splat_i16" => JitOp::RegSplat(1),
1857        "reg_splat_i32" => JitOp::RegSplat(2),
1858        "reg_splat_i64" => JitOp::RegSplat(3),
1859        "reg_splat_f32" => JitOp::RegSplat(4),
1860        "reg_splat_f64" => JitOp::RegSplat(5),
1861
1862        "f64_add" => JitOp::F64Add,
1863        "f64_sub" => JitOp::F64Sub,
1864        "f64_mul" => JitOp::F64Mul,
1865        "f64_div" => JitOp::F64Div,
1866        "f64_mod" => JitOp::F64Mod,
1867
1868        // ── Comparisons & Selections (SRD 110) ───────────────────
1869        "u64_eq" => JitOp::U64Cmp(ir::condcodes::IntCC::Equal),
1870        "u64_ne" => JitOp::U64Cmp(ir::condcodes::IntCC::NotEqual),
1871        "u64_lt" => JitOp::U64Cmp(ir::condcodes::IntCC::UnsignedLessThan),
1872        "u64_le" => JitOp::U64Cmp(ir::condcodes::IntCC::UnsignedLessThanOrEqual),
1873        "u64_gt" => JitOp::U64Cmp(ir::condcodes::IntCC::UnsignedGreaterThan),
1874        "u64_ge" => JitOp::U64Cmp(ir::condcodes::IntCC::UnsignedGreaterThanOrEqual),
1875        "f64_eq" => JitOp::F64Cmp(ir::condcodes::FloatCC::Equal),
1876        "f64_ne" => JitOp::F64Cmp(ir::condcodes::FloatCC::NotEqual),
1877        "f64_lt" => JitOp::F64Cmp(ir::condcodes::FloatCC::LessThan),
1878        "f64_le" => JitOp::F64Cmp(ir::condcodes::FloatCC::LessThanOrEqual),
1879        "f64_gt" => JitOp::F64Cmp(ir::condcodes::FloatCC::GreaterThan),
1880        "f64_ge" => JitOp::F64Cmp(ir::condcodes::FloatCC::GreaterThanOrEqual),
1881        "select_u64" | "select" => JitOp::SelectU64,
1882        "select_f64" => JitOp::SelectF64,
1883
1884        // ── Wire Arithmetic & Multiples (SRD 110) ────────────────
1885        "div_wire" => JitOp::U64DivWire,
1886        "mod_wire" => JitOp::U64ModWire,
1887        "ceil_to_multiple" => JitOp::CeilToMultiple,
1888        "multiples_at_least" => JitOp::MultiplesAtLeast,
1889        "checked_add" => JitOp::CheckedAdd,
1890        "checked_sub" => JitOp::CheckedSub,
1891        "checked_mul" => JitOp::CheckedMul,
1892
1893        // ── Variadics (SRD 110) ──────────────────────────────────
1894        "sum" => JitOp::VariadicSum,
1895        "product" => JitOp::VariadicProduct,
1896        "min" => JitOp::VariadicMin,
1897        "max" => JitOp::VariadicMax,
1898
1899        // ── PRNG & Probability (SRD 110) ─────────────────────────
1900        "blend" => {
1901            if let Some(&c) = consts.first() {
1902                JitOp::BlendConst(c)
1903            } else {
1904                JitOp::Fallback
1905            }
1906        }
1907        "lfsr_step" => {
1908            if let Some(&fb) = consts.first() {
1909                JitOp::LfsrStepConst(fb)
1910            } else {
1911                JitOp::Fallback
1912            }
1913        }
1914        "pcg" => {
1915            if consts.len() >= 2 {
1916                JitOp::PcgConst(consts[0], consts[1])
1917            } else {
1918                JitOp::Fallback
1919            }
1920        }
1921        "pcg_stream" => {
1922            if let Some(&seed) = consts.first() {
1923                JitOp::PcgStreamConst(seed)
1924            } else {
1925                JitOp::Fallback
1926            }
1927        }
1928        "n_of" => {
1929            if consts.len() >= 2 {
1930                JitOp::NOfConst(consts[0], consts[1])
1931            } else {
1932                JitOp::Fallback
1933            }
1934        }
1935
1936        "cycle_walk" => {
1937            if consts.len() >= 3 {
1938                JitOp::CycleWalkConst(consts[0], consts[1], consts[2])
1939            } else {
1940                JitOp::Fallback
1941            }
1942        }
1943        "coin_flip" => {
1944            // The body is `input < threshold` over the raw input
1945            // (library/fixed.rs), not a hashed unit interval as
1946            // `unfair_coin` is; the node bakes its threshold as its
1947            // one constant.
1948            if let Some(&threshold) = consts.first() {
1949                JitOp::CoinFlipConst(threshold)
1950            } else {
1951                JitOp::Fallback
1952            }
1953        }
1954        // `default_or` without wire types: the typed classifier decides
1955        // (a copy of the value, since a compiled slot is never `None`).
1956        "default_or" => JitOp::Identity,
1957        "const_u64" | "const_bool" | "session_start_millis" => {
1958            if let Some(&c) = consts.first() {
1959                JitOp::ConstU64(c)
1960            } else {
1961                JitOp::Fallback
1962            }
1963        }
1964        "const_f64" => {
1965            if let Some(&c) = consts.first() {
1966                JitOp::ConstF64(c)
1967            } else {
1968                JitOp::Fallback
1969            }
1970        }
1971        "inv_lerp" => {
1972            if consts.len() >= 2 {
1973                JitOp::InvLerpConst(consts[0], consts[1])
1974            } else {
1975                JitOp::Fallback
1976            }
1977        }
1978        "remap" => {
1979            if consts.len() >= 4 {
1980                JitOp::RemapConst(consts[0], consts[1], consts[2], consts[3])
1981            } else {
1982                JitOp::Fallback
1983            }
1984        }
1985        "epoch_offset" => {
1986            if let Some(&c) = consts.first() {
1987                JitOp::EpochOffsetConst(c)
1988            } else {
1989                JitOp::Fallback
1990            }
1991        }
1992        "epoch_scale" => {
1993            if let Some(&c) = consts.first() {
1994                JitOp::EpochScaleConst(c)
1995            } else {
1996                JitOp::Fallback
1997            }
1998        }
1999        "thread_id" => JitOp::ThreadId,
2000        "current_epoch_millis" => JitOp::CurrentEpochMillis,
2001        "perlin_1d" => {
2002            if consts.len() >= 2 {
2003                JitOp::Perlin1dConst(consts[0], consts[1])
2004            } else {
2005                JitOp::Fallback
2006            }
2007        }
2008        "perlin_2d" => {
2009            if consts.len() >= 2 {
2010                JitOp::Perlin2dConst(consts[0], consts[1])
2011            } else {
2012                JitOp::Fallback
2013            }
2014        }
2015        "simplex_2d" => {
2016            if consts.len() >= 2 {
2017                JitOp::Simplex2dConst(consts[0], consts[1])
2018            } else {
2019                JitOp::Fallback
2020            }
2021        }
2022        "fractal_noise_1d" => {
2023            if consts.len() >= 3 {
2024                JitOp::FractalNoise1dConst(consts[0], consts[1], consts[2])
2025            } else {
2026                JitOp::Fallback
2027            }
2028        }
2029        "fractal_noise_2d" => {
2030            if consts.len() >= 3 {
2031                JitOp::FractalNoise2dConst(consts[0], consts[1], consts[2])
2032            } else {
2033                JitOp::Fallback
2034            }
2035        }
2036
2037        // ── Type Conversion Lattice (SRD 110) ────────────────────
2038        "u64_to_f64" | "__u64_to_f64" | "u32_to_f64" | "__u32_to_f64" | "bool_to_f64"
2039        | "__bool_to_f64" | "bool_to_f32" | "__bool_to_f32" | "__f32_to_f64" | "f32_to_f64"
2040        | "__u64_to_f32" | "u64_to_f32" | "__u32_to_f32" | "u32_to_f32" | "__u16_to_f32"
2041        | "u16_to_f32" | "__u8_to_f32" | "u8_to_f32" | "__u16_to_f64" | "u16_to_f64"
2042        | "__u8_to_f64" | "u8_to_f64" | "__u128_to_f64" | "__u128_to_f32" | "__u128_to_f16" => {
2043            JitOp::ToF64
2044        }
2045
2046        "i64_to_f64" | "__i64_to_f64" | "i32_to_f64" | "__i32_to_f64" | "__i64_to_f32"
2047        | "i64_to_f32" | "__i32_to_f32" | "i32_to_f32" | "__i16_to_f32" | "i16_to_f32"
2048        | "__i8_to_f32" | "i8_to_f32" | "__i16_to_f64" | "i16_to_f64" | "__i8_to_f64"
2049        | "i8_to_f64" | "__i128_to_f64" | "__i128_to_f32" | "__i128_to_f16" => JitOp::I64ToF64,
2050
2051        "__f64_to_u64"
2052        | "__f64_to_u64_checked"
2053        | "f64_to_u32"
2054        | "__f64_to_u32"
2055        | "f32_to_u64"
2056        | "__f32_to_u64"
2057        | "f32_to_u32"
2058        | "__f32_to_u32"
2059        | "__f64_to_u16"
2060        | "f64_to_u16"
2061        | "__f64_to_u8"
2062        | "f64_to_u8"
2063        | "__f32_to_u16"
2064        | "f32_to_u16"
2065        | "__f32_to_u8"
2066        | "f32_to_u8"
2067        | "__f16_to_u64"
2068        | "__f16_to_u32"
2069        | "__f16_to_u16"
2070        | "__f16_to_u8"
2071        | "__f64_to_u128"
2072        | "__f32_to_u128"
2073        | "__f16_to_u128"
2074        | "trunc_u64" => JitOp::F64ToU64,
2075        // `round_u64` rounds half away from zero before the saturating
2076        // conversion, which is what `round_to_u64` does too.
2077        "round_u64" => JitOp::RoundToU64,
2078
2079        "f64_to_i64" | "__f64_to_i64" | "f64_to_i32" | "__f64_to_i32" | "f32_to_i64"
2080        | "__f32_to_i64" | "f32_to_i32" | "__f32_to_i32" | "__f64_to_i16" | "f64_to_i16"
2081        | "__f64_to_i8" | "f64_to_i8" | "__f32_to_i16" | "f32_to_i16" | "__f32_to_i8"
2082        | "f32_to_i8" | "__f16_to_i64" | "__f16_to_i32" | "__f16_to_i16" | "__f16_to_i8"
2083        | "__f64_to_i128" | "__f32_to_i128" | "__f16_to_i128" => JitOp::F64ToI64,
2084
2085        "f64_to_f32" | "__f64_to_f32" | "__f16_to_f32" | "__f16_to_f64" | "__f32_to_f16"
2086        | "__f64_to_f16" => JitOp::Identity,
2087
2088        "u32_to_u64" | "__u32_to_u64" | "u64_to_u32" | "__u64_to_u32" | "u32_to_i32"
2089        | "__u32_to_i32" | "i32_to_u32" | "__i32_to_u32" | "u64_to_i64" | "__u64_to_i64"
2090        | "i64_to_u64" | "__i64_to_u64" | "bool_to_u64" | "__bool_to_u64" | "bool_to_i64"
2091        | "__bool_to_i64" | "bool_to_u32" | "__bool_to_u32" | "bool_to_i32" | "__bool_to_i32"
2092        | "__u64_to_u16" | "__u64_to_u8" | "__u64_to_i16" | "__u64_to_i8" | "__i64_to_u32"
2093        | "__i64_to_u16" | "__i64_to_u8" | "__i64_to_i16" | "__i64_to_i8" | "__u32_to_u16"
2094        | "__u32_to_u8" | "__u32_to_i16" | "__u32_to_i8" | "__i32_to_u16" | "__i32_to_u8"
2095        | "__i32_to_i16" | "__i32_to_i8" | "__u16_to_u8" | "__u16_to_i8" | "__i16_to_u8"
2096        | "__i16_to_i8" | "__u128_to_u64" | "__u128_to_i64" | "__i128_to_u64" | "__i128_to_i64"
2097        | "__u128_to_u32" | "__u128_to_u16" | "__u128_to_u8" | "__u128_to_i32"
2098        | "__u128_to_i16" | "__u128_to_i8" | "__i128_to_u32" | "__i128_to_u16" | "__i128_to_u8"
2099        | "__i128_to_i32" | "__i128_to_i16" | "__i128_to_i8" | "__u64_to_u128"
2100        | "__u64_to_i128" | "__i64_to_u128" | "__i64_to_i128" | "__u128_to_i128"
2101        | "__i128_to_u128" | "__bool_to_u16" | "__bool_to_u8" | "__bool_to_i16"
2102        | "__bool_to_i8" | "__bool_to_u128" | "__bool_to_i128" | "__u8_to_f16" | "__u16_to_f16"
2103        | "__i8_to_f16" | "__i16_to_f16" | "__u64_to_f16" | "__i64_to_f16" | "__u32_to_f16"
2104        | "__i32_to_f16" | "__bool_to_f16" => JitOp::Identity,
2105
2106        "i32_to_i64" | "__i32_to_i64" | "i32_to_u64" | "__i32_to_u64" | "__u32_to_i64"
2107        | "u32_to_i64" | "__u32_to_u128" | "__u32_to_i128" | "__i32_to_u128" | "__i32_to_i128" => {
2108            JitOp::SignExtendI32
2109        }
2110
2111        "__i16_to_i32" | "i16_to_i32" | "__i16_to_i64" | "i16_to_i64" | "__i16_to_u32"
2112        | "i16_to_u32" | "__i16_to_u64" | "i16_to_u64" | "__i16_to_u128" | "__i16_to_i128" => {
2113            JitOp::SignExtendI16
2114        }
2115
2116        "__i8_to_i16" | "i8_to_i16" | "__i8_to_i32" | "i8_to_i32" | "__i8_to_i64" | "i8_to_i64"
2117        | "__i8_to_u16" | "i8_to_u16" | "__i8_to_u32" | "i8_to_u32" | "__i8_to_u64"
2118        | "i8_to_u64" | "__i8_to_u128" | "__i8_to_i128" => JitOp::SignExtendI8,
2119
2120        "__u16_to_u32" | "u16_to_u32" | "__u16_to_u64" | "u16_to_u64" | "__u16_to_i32"
2121        | "u16_to_i32" | "__u16_to_i64" | "u16_to_i64" | "__u16_to_u128" | "__u16_to_i128"
2122        | "__u16_to_i16" | "u16_to_i16" | "__i16_to_u16" | "i16_to_u16" => JitOp::ZeroExtendU16,
2123
2124        "__u8_to_u16" | "u8_to_u16" | "__u8_to_u32" | "u8_to_u32" | "__u8_to_u64" | "u8_to_u64"
2125        | "__u8_to_i16" | "u8_to_i16" | "__u8_to_i32" | "u8_to_i32" | "__u8_to_i64"
2126        | "u8_to_i64" | "__u8_to_u128" | "__u8_to_i128" | "__u8_to_i8" | "u8_to_i8"
2127        | "__i8_to_u8" | "i8_to_u8" => JitOp::ZeroExtendU8,
2128
2129        "u64_to_i32" | "__u64_to_i32" | "i64_to_i32" | "__i64_to_i32" => JitOp::ZeroExtendU32,
2130
2131        "u64_to_bool" | "__u64_to_bool" | "i64_to_bool" | "__i64_to_bool" | "u32_to_bool"
2132        | "__u32_to_bool" | "i32_to_bool" | "__i32_to_bool" | "f64_to_bool" | "__f64_to_bool"
2133        | "f32_to_bool" | "__f32_to_bool" | "__f16_to_bool" | "f16_to_bool" | "__u8_to_bool"
2134        | "__u16_to_bool" | "__i8_to_bool" | "__i16_to_bool" | "__u128_to_bool"
2135        | "__i128_to_bool" => JitOp::ToBool,
2136
2137        "weighted_pick" => {
2138            if consts.len() >= 5 {
2139                JitOp::WeightedPickConst(consts[0], consts[1], consts[2], consts[3], consts[4])
2140            } else {
2141                JitOp::Fallback
2142            }
2143        }
2144
2145        // ── Parameter helpers (SRD 12) ─────────────────────────
2146        // `is_positive` / `in_range` are JIT-lowered inline: one
2147        // comparison on the happy path, an extern call on the
2148        // fail path (which panics). The pass-through is a plain
2149        // store, no function call overhead on the typical cycle.
2150        "is_positive" => {
2151            let name = node.meta().ins.iter().find_map(|slot| match slot {
2152                crate::ast::Slot::Const {
2153                    name,
2154                    value: crate::ast::ConstValue::Str(v),
2155                } if name == "name" => Some(v),
2156                _ => None,
2157            });
2158            match name {
2159                Some(v) => JitOp::IsPositiveCheck {
2160                    name_ptr: v.as_ptr() as u64,
2161                    name_len: v.len() as u64,
2162                },
2163                None => JitOp::IsPositiveCheck {
2164                    name_ptr: 0,
2165                    name_len: 0,
2166                },
2167            }
2168        }
2169        "in_range" => {
2170            if consts.len() >= 2 {
2171                JitOp::InRangeCheck(consts[0], consts[1])
2172            } else {
2173                JitOp::Fallback
2174            }
2175        }
2176        "is_one_of" => {
2177            if consts.is_empty() {
2178                JitOp::Fallback
2179            } else {
2180                let set = node.meta().ins.iter().find_map(|slot| match slot {
2181                    crate::ast::Slot::Const {
2182                        name,
2183                        value: crate::ast::ConstValue::VecU64(v),
2184                    } if name == "allowed" => Some(v),
2185                    _ => None,
2186                });
2187                let (set_ptr, set_len) = match set {
2188                    Some(v) => (v.as_ptr() as u64, v.len() as u64),
2189                    None => (0, 0),
2190                };
2191                JitOp::IsOneOfCheck {
2192                    allowed: consts,
2193                    set_ptr,
2194                    set_len,
2195                }
2196            }
2197        }
2198        // Every other node with a kit (`required`, `this_or`,
2199        // `matches`, the context nodes) takes `JitOp::SlotCall` in
2200        // `classify_node_typed`; only a node with no kit stays
2201        // interpreted.
2202        _ => JitOp::Fallback,
2203    }
2204}
2205
2206// ── Kernel constructors ────────────────────────────────────
2207
2208/// Compile a set of JIT steps into a raw (no-provenance) native kernel.
2209///
2210/// Each step has: jit_op, input_slots (buffer indices), output_slots.
2211/// The generated function reads coords from the buffer, executes
2212/// all steps in order, and writes results to the buffer.
2213#[doc(hidden)]
2214pub fn compile_jit_raw(
2215    coord_count: usize,
2216    total_slots: usize,
2217    steps: Vec<(JitOp, Vec<usize>, Vec<usize>)>,
2218    output_map: HashMap<String, usize>,
2219    nodes: Vec<Box<dyn PolydatNode>>,
2220) -> Result<JitKernelRaw, String> {
2221    compile_jit_raw_with(
2222        coord_count,
2223        total_slots,
2224        steps,
2225        output_map,
2226        nodes,
2227        crate::compile::externs::Externs::default(),
2228        super::kernels::ScratchPlan::default(),
2229        Vec::new(),
2230    )
2231}
2232
2233/// `compile_jit_raw` for a graph with extern inputs: their defaults
2234/// are written through into the buffer at build.
2235#[allow(clippy::too_many_arguments)]
2236pub(crate) fn compile_jit_raw_with(
2237    coord_count: usize,
2238    total_slots: usize,
2239    steps: Vec<(JitOp, Vec<usize>, Vec<usize>)>,
2240    output_map: HashMap<String, usize>,
2241    nodes: Vec<Box<dyn PolydatNode>>,
2242    externs: crate::compile::externs::Externs,
2243    scratch: super::kernels::ScratchPlan,
2244    volatile: Vec<usize>,
2245) -> Result<JitKernelRaw, String> {
2246    let (raw_fn, _, code) = compile_jit_impl(&steps, false, Some(total_slots))?;
2247    let mut core = JitCore::new(
2248        total_slots,
2249        coord_count,
2250        output_map,
2251        code,
2252        nodes,
2253        scratch,
2254        volatile,
2255    );
2256    core.set_externs(externs);
2257    Ok(JitKernelRaw {
2258        core,
2259        code_fn: raw_fn,
2260    })
2261}
2262
2263/// A compiled segment for an engine that owns its own buffer: the
2264/// entry point and the module that keeps it alive (SRD-105 cones,
2265/// hybrid JIT segments).
2266pub(crate) type JitSegmentCode = (NativeFn, super::kernels::JitCode);
2267
2268/// An entry with no kernel wrapper: a cone node or a hybrid segment
2269/// owns the function pointer and code, and the state evaluating it
2270/// provides the buffer and the scratch.
2271pub(crate) fn compile_jit_entry(
2272    steps: &[(JitOp, Vec<usize>, Vec<usize>)],
2273    tracker: Option<usize>,
2274) -> Result<JitSegmentCode, String> {
2275    let (raw_fn, _, code) = compile_jit_impl(steps, false, tracker)?;
2276    Ok((raw_fn, code))
2277}
2278
2279/// Compile a set of JIT steps into a push (per-node dirty tracking) native kernel.
2280#[allow(clippy::too_many_arguments)]
2281pub(crate) fn compile_jit_push(
2282    coord_count: usize,
2283    total_slots: usize,
2284    steps: Vec<(JitOp, Vec<usize>, Vec<usize>)>,
2285    output_map: HashMap<String, usize>,
2286    nodes: Vec<Box<dyn PolydatNode>>,
2287    input_dependents: Vec<Vec<usize>>,
2288    externs: crate::compile::externs::Externs,
2289    scratch: super::kernels::ScratchPlan,
2290    volatile: Vec<usize>,
2291) -> Result<JitKernelPush, String> {
2292    let step_count = steps.len();
2293    let (_, prov_fn, code) = compile_jit_impl(&steps, true, Some(total_slots))?;
2294    let mut core = JitCore::new(
2295        total_slots,
2296        coord_count,
2297        output_map,
2298        code,
2299        nodes,
2300        scratch,
2301        volatile,
2302    );
2303    core.set_externs(externs);
2304    Ok(JitKernelPush {
2305        core,
2306        code_fn_prov: prov_fn,
2307        node_clean: vec![0u8; step_count],
2308        input_dependents,
2309    })
2310}
2311
2312/// Compile a set of JIT steps into a pull (cone guard) native kernel.
2313#[allow(clippy::too_many_arguments)]
2314pub(crate) fn compile_jit_pull(
2315    coord_count: usize,
2316    total_slots: usize,
2317    steps: Vec<(JitOp, Vec<usize>, Vec<usize>)>,
2318    output_map: HashMap<String, usize>,
2319    nodes: Vec<Box<dyn PolydatNode>>,
2320    input_dependents: &[Vec<usize>],
2321    externs: crate::compile::externs::Externs,
2322    scratch: super::kernels::ScratchPlan,
2323    volatile: Vec<usize>,
2324) -> Result<JitKernelPull, String> {
2325    let buffer_len = total_slots;
2326    // Pull uses the RAW jit function (no per-node clean checks)
2327    let (raw_fn, _, code) = compile_jit_impl(&steps, false, Some(total_slots))?;
2328    let step_outs: Vec<&[usize]> = steps.iter().map(|(_, _, o)| o.as_slice()).collect();
2329    let slot_provenance =
2330        crate::compile::slot_provenance(coord_count, buffer_len, &step_outs, input_dependents);
2331    let mut core = JitCore::new(
2332        total_slots,
2333        coord_count,
2334        output_map,
2335        code,
2336        nodes,
2337        scratch,
2338        volatile,
2339    );
2340    core.set_externs(externs);
2341    Ok(JitKernelPull {
2342        core,
2343        code_fn: raw_fn,
2344        slot_provenance,
2345        changed_mask: crate::kernel::ProvMask::all_below(coord_count),
2346        force_run: false,
2347    })
2348}
2349
2350/// Compile a set of JIT steps into a push+pull (full optimization) native kernel.
2351#[allow(clippy::too_many_arguments)]
2352pub(crate) fn compile_jit_push_pull(
2353    coord_count: usize,
2354    total_slots: usize,
2355    steps: Vec<(JitOp, Vec<usize>, Vec<usize>)>,
2356    output_map: HashMap<String, usize>,
2357    nodes: Vec<Box<dyn PolydatNode>>,
2358    input_dependents: Vec<Vec<usize>>,
2359    externs: crate::compile::externs::Externs,
2360    scratch: super::kernels::ScratchPlan,
2361    volatile: Vec<usize>,
2362) -> Result<JitKernelPushPull, String> {
2363    let step_count = steps.len();
2364    let buffer_len = total_slots;
2365    let (_, prov_fn, code) = compile_jit_impl(&steps, true, Some(total_slots))?;
2366    let step_outs: Vec<&[usize]> = steps.iter().map(|(_, _, o)| o.as_slice()).collect();
2367    let slot_provenance =
2368        crate::compile::slot_provenance(coord_count, buffer_len, &step_outs, &input_dependents);
2369    let mut core = JitCore::new(
2370        total_slots,
2371        coord_count,
2372        output_map,
2373        code,
2374        nodes,
2375        scratch,
2376        volatile,
2377    );
2378    core.set_externs(externs);
2379    Ok(JitKernelPushPull {
2380        core,
2381        code_fn_prov: prov_fn,
2382        node_clean: vec![0u8; step_count],
2383        input_dependents,
2384        slot_provenance,
2385        changed_mask: crate::kernel::ProvMask::all_below(coord_count),
2386        force_run: false,
2387    })
2388}
2389
2390// ── Core Cranelift IR generation ───────────────────────────
2391
2392/// A native entry point over a state's slot buffer and scratch.
2393pub type NativeFn = unsafe fn(*const u64, *mut u64, *mut crate::ast::ScratchBuf);
2394/// The provenance variant: a clean flag per step follows the scratch.
2395pub type NativeProvFn = unsafe fn(*const u64, *mut u64, *mut crate::ast::ScratchBuf, *mut u8);
2396
2397/// `(raw_fn, prov_fn, code)` — produced by the core JIT compile: the
2398/// scalar entry point, the provenance-tracking entry point, and the
2399/// finalized code that keeps both alive with the kits they call.
2400type JitCompiled = (NativeFn, NativeProvFn, super::kernels::JitCode);
2401
2402/// Core JIT compilation. Returns (raw_fn, prov_fn, code).
2403/// If provenance=false, prov_fn is a dummy transmute of raw_fn.
2404/// If provenance=true, raw_fn is a dummy transmute of prov_fn.
2405fn compile_jit_impl(
2406    steps: &[(JitOp, Vec<usize>, Vec<usize>)],
2407    provenance: bool,
2408    tracker: Option<usize>,
2409) -> Result<JitCompiled, String> {
2410    let mut flag_builder = settings::builder();
2411    flag_builder.set("opt_level", "speed").unwrap();
2412    // Unwind tables and frame pointers are kept for debuggers and
2413    // profilers walking JIT frames; failures never unwind through
2414    // native code, they longjmp past it (see the setjmp section
2415    // above).
2416    flag_builder.set("unwind_info", "true").unwrap();
2417    flag_builder.set("preserve_frame_pointers", "true").unwrap();
2418    let isa = super::host_isa::build_host_isa(flag_builder)?;
2419
2420    let mut jit_builder = JITBuilder::with_isa(isa, cranelift_module::default_libcall_names());
2421
2422    // Register extern functions
2423    jit_builder.symbol("jit_xxh3_hash", jit_xxh3_hash as *const u8);
2424    jit_builder.symbol("jit_interleave", jit_interleave as *const u8);
2425    jit_builder.symbol("jit_shuffle", jit_shuffle as *const u8);
2426    jit_builder.symbol("jit_lut_sample", jit_lut_sample as *const u8);
2427    jit_builder.symbol("jit_weighted_pick", jit_weighted_pick as *const u8);
2428    jit_builder.symbol("jit_pcg", jit_pcg as *const u8);
2429    jit_builder.symbol("jit_pcg_stream", jit_pcg_stream as *const u8);
2430    jit_builder.symbol("jit_n_of", jit_n_of as *const u8);
2431    jit_builder.symbol("jit_cycle_walk", jit_cycle_walk as *const u8);
2432    jit_builder.symbol("jit_perlin_1d", jit_perlin_1d as *const u8);
2433    jit_builder.symbol("jit_perlin_2d", jit_perlin_2d as *const u8);
2434    jit_builder.symbol("jit_simplex_2d", jit_simplex_2d as *const u8);
2435    jit_builder.symbol("jit_fractal_noise_1d", jit_fractal_noise_1d as *const u8);
2436    jit_builder.symbol("jit_fractal_noise_2d", jit_fractal_noise_2d as *const u8);
2437    jit_builder.symbol("jit_thread_id", jit_thread_id as *const u8);
2438    jit_builder.symbol(
2439        "jit_current_epoch_millis",
2440        jit_current_epoch_millis as *const u8,
2441    );
2442    // Parameter-helper predicates (SRD 12 §"Parameter resolution
2443    // and validation"): happy path is inline, violation is an
2444    // extern call that never returns.
2445    jit_builder.symbol("jit_is_positive_fail", jit_is_positive_fail as *const u8);
2446    jit_builder.symbol("jit_in_range_fail", jit_in_range_fail as *const u8);
2447    jit_builder.symbol("jit_is_one_of_fail", jit_is_one_of_fail as *const u8);
2448    // A node's slot kit, called from native code (compiled_handles.md §6),
2449    // and the string producers that write into the step's entry directly.
2450    jit_builder.symbol("jit_slot_call", jit_slot_call as *const u8);
2451    jit_builder.symbol("jit_u64_to_str", jit_u64_to_str as *const u8);
2452    jit_builder.symbol("jit_i64_to_str", jit_i64_to_str as *const u8);
2453    jit_builder.symbol("jit_f64_to_str", jit_f64_to_str as *const u8);
2454    jit_builder.symbol("jit_str_concat", jit_str_concat as *const u8);
2455    jit_builder.symbol("jit_json_to_str", jit_json_to_str as *const u8);
2456    jit_builder.symbol("jit_vec_add", jit_vec_add as *const u8);
2457    jit_builder.symbol("jit_vec_scale", jit_vec_scale as *const u8);
2458    jit_builder.symbol("jit_vec_norm", jit_vec_norm as *const u8);
2459    jit_builder.symbol("jit_hash_vec", jit_hash_vec as *const u8);
2460    jit_builder.symbol("jit_xxhash3_vec", jit_xxhash3_vec as *const u8);
2461    jit_builder.symbol("jit_reg_to_vec_f32", jit_reg_to_vec_f32 as *const u8);
2462    jit_builder.symbol("jit_vec_dot", jit_vec_dot as *const u8);
2463    jit_builder.symbol("jit_vec_l2", jit_vec_l2 as *const u8);
2464    jit_builder.symbol("jit_vec_cosine", jit_vec_cosine as *const u8);
2465    jit_builder.symbol("jit_lid_mle", jit_lid_mle as *const u8);
2466    jit_builder.symbol("jit_reg_lane_f32", jit_reg_lane_f32 as *const u8);
2467    jit_builder.symbol("jit_reg_lane_i16", jit_reg_lane_i16 as *const u8);
2468    jit_builder.symbol("jit_reg_lane_i64", jit_reg_lane_i64 as *const u8);
2469    jit_builder.symbol("jit_reg_with_lane_f32", jit_reg_with_lane_f32 as *const u8);
2470    jit_builder.symbol("jit_reg_gather_f32", jit_reg_gather_f32 as *const u8);
2471    jit_builder.symbol("jit_vec_to_reg_f32", jit_vec_to_reg_f32 as *const u8);
2472    jit_builder.symbol("jit_reg_mul_i8", jit_reg_mul_i8 as *const u8);
2473    // Math externs
2474    jit_builder.symbol("jit_sin", jit_sin as *const u8);
2475    jit_builder.symbol("jit_cos", jit_cos as *const u8);
2476    jit_builder.symbol("jit_tan", jit_tan as *const u8);
2477    jit_builder.symbol("jit_asin", jit_asin as *const u8);
2478    jit_builder.symbol("jit_acos", jit_acos as *const u8);
2479    jit_builder.symbol("jit_atan", jit_atan as *const u8);
2480    jit_builder.symbol("jit_sqrt", jit_sqrt as *const u8);
2481    jit_builder.symbol("jit_abs_f64", jit_abs_f64 as *const u8);
2482    jit_builder.symbol("jit_ln", jit_ln as *const u8);
2483    jit_builder.symbol("jit_exp", jit_exp as *const u8);
2484    jit_builder.symbol("jit_floor_base10", jit_floor_base10 as *const u8);
2485    jit_builder.symbol("jit_ceiling_base10", jit_ceiling_base10 as *const u8);
2486    jit_builder.symbol("jit_closest_base10", jit_closest_base10 as *const u8);
2487    jit_builder.symbol("jit_floor_decade", jit_floor_decade as *const u8);
2488    jit_builder.symbol("jit_ceiling_decade", jit_ceiling_decade as *const u8);
2489    jit_builder.symbol("jit_closest_decade", jit_closest_decade as *const u8);
2490    jit_builder.symbol("jit_floor_binomial", jit_floor_binomial as *const u8);
2491    jit_builder.symbol("jit_ceiling_binomial", jit_ceiling_binomial as *const u8);
2492    jit_builder.symbol("jit_closest_binomial", jit_closest_binomial as *const u8);
2493    jit_builder.symbol("jit_floor_fibonacci", jit_floor_fibonacci as *const u8);
2494    jit_builder.symbol("jit_ceiling_fibonacci", jit_ceiling_fibonacci as *const u8);
2495    jit_builder.symbol("jit_closest_fibonacci", jit_closest_fibonacci as *const u8);
2496    jit_builder.symbol("jit_atan2", jit_atan2 as *const u8);
2497    jit_builder.symbol("jit_pow", jit_pow as *const u8);
2498    jit_builder.symbol("jit_round_nearest", jit_round_nearest as *const u8);
2499    jit_builder.symbol("jit_round_floor", jit_round_floor as *const u8);
2500    jit_builder.symbol("jit_round_ceiling", jit_round_ceiling as *const u8);
2501    jit_builder.symbol("jit_f64_mod", jit_f64_mod as *const u8);
2502    jit_builder.symbol("jit_div_zero_fail", jit_div_zero_fail as *const u8);
2503
2504    let mut module = JITModule::new(jit_builder);
2505
2506    // Declare extern: hash(u64) -> u64
2507    let hash_func_id = {
2508        let mut sig = module.make_signature();
2509        sig.params.push(AbiParam::new(types::I64));
2510        sig.returns.push(AbiParam::new(types::I64));
2511        module
2512            .declare_function("jit_xxh3_hash", Linkage::Import, &sig)
2513            .map_err(|e| format!("declare hash: {e}"))?
2514    };
2515
2516    // Declare extern: interleave(u64, u64) -> u64
2517    let interleave_func_id = {
2518        let mut sig = module.make_signature();
2519        sig.params.push(AbiParam::new(types::I64));
2520        sig.params.push(AbiParam::new(types::I64));
2521        sig.returns.push(AbiParam::new(types::I64));
2522        module
2523            .declare_function("jit_interleave", Linkage::Import, &sig)
2524            .map_err(|e| format!("declare interleave: {e}"))?
2525    };
2526
2527    // Declare extern: shuffle(u64, u64, u64, u64) -> u64
2528    let shuffle_func_id = {
2529        let mut sig = module.make_signature();
2530        for _ in 0..4 {
2531            sig.params.push(AbiParam::new(types::I64));
2532        }
2533        sig.returns.push(AbiParam::new(types::I64));
2534        module
2535            .declare_function("jit_shuffle", Linkage::Import, &sig)
2536            .map_err(|e| format!("declare shuffle: {e}"))?
2537    };
2538
2539    // Declare extern: lut_sample(u64, u64, u64) -> u64
2540    let lut_sample_func_id = {
2541        let mut sig = module.make_signature();
2542        for _ in 0..3 {
2543            sig.params.push(AbiParam::new(types::I64));
2544        }
2545        sig.returns.push(AbiParam::new(types::I64));
2546        module
2547            .declare_function("jit_lut_sample", Linkage::Import, &sig)
2548            .map_err(|e| format!("declare lut_sample: {e}"))?
2549    };
2550
2551    // Declare extern: weighted_pick(u64, u64, u64, u64, u64, u64) -> u64
2552    let weighted_pick_func_id = {
2553        let mut sig = module.make_signature();
2554        for _ in 0..6 {
2555            sig.params.push(AbiParam::new(types::I64));
2556        }
2557        sig.returns.push(AbiParam::new(types::I64));
2558        module
2559            .declare_function("jit_weighted_pick", Linkage::Import, &sig)
2560            .map_err(|e| format!("declare weighted_pick: {e}"))?
2561    };
2562
2563    let pcg_func_id = {
2564        let mut sig = module.make_signature();
2565        for _ in 0..3 {
2566            sig.params.push(AbiParam::new(types::I64));
2567        }
2568        sig.returns.push(AbiParam::new(types::I64));
2569        module
2570            .declare_function("jit_pcg", Linkage::Import, &sig)
2571            .map_err(|e| format!("declare pcg: {e}"))?
2572    };
2573    let pcg_stream_func_id = {
2574        let mut sig = module.make_signature();
2575        for _ in 0..3 {
2576            sig.params.push(AbiParam::new(types::I64));
2577        }
2578        sig.returns.push(AbiParam::new(types::I64));
2579        module
2580            .declare_function("jit_pcg_stream", Linkage::Import, &sig)
2581            .map_err(|e| format!("declare pcg_stream: {e}"))?
2582    };
2583    let n_of_func_id = {
2584        let mut sig = module.make_signature();
2585        for _ in 0..3 {
2586            sig.params.push(AbiParam::new(types::I64));
2587        }
2588        sig.returns.push(AbiParam::new(types::I64));
2589        module
2590            .declare_function("jit_n_of", Linkage::Import, &sig)
2591            .map_err(|e| format!("declare n_of: {e}"))?
2592    };
2593    let cycle_walk_func_id = {
2594        let mut sig = module.make_signature();
2595        for _ in 0..4 {
2596            sig.params.push(AbiParam::new(types::I64));
2597        }
2598        sig.returns.push(AbiParam::new(types::I64));
2599        module
2600            .declare_function("jit_cycle_walk", Linkage::Import, &sig)
2601            .map_err(|e| format!("declare cycle_walk: {e}"))?
2602    };
2603    let perlin_1d_func_id = {
2604        let mut sig = module.make_signature();
2605        for _ in 0..3 {
2606            sig.params.push(AbiParam::new(types::I64));
2607        }
2608        sig.returns.push(AbiParam::new(types::I64));
2609        module
2610            .declare_function("jit_perlin_1d", Linkage::Import, &sig)
2611            .map_err(|e| format!("declare perlin_1d: {e}"))?
2612    };
2613    let perlin_2d_func_id = {
2614        let mut sig = module.make_signature();
2615        for _ in 0..4 {
2616            sig.params.push(AbiParam::new(types::I64));
2617        }
2618        sig.returns.push(AbiParam::new(types::I64));
2619        module
2620            .declare_function("jit_perlin_2d", Linkage::Import, &sig)
2621            .map_err(|e| format!("declare perlin_2d: {e}"))?
2622    };
2623    let simplex_2d_func_id = {
2624        let mut sig = module.make_signature();
2625        for _ in 0..4 {
2626            sig.params.push(AbiParam::new(types::I64));
2627        }
2628        sig.returns.push(AbiParam::new(types::I64));
2629        module
2630            .declare_function("jit_simplex_2d", Linkage::Import, &sig)
2631            .map_err(|e| format!("declare simplex_2d: {e}"))?
2632    };
2633    let fractal_noise_1d_func_id = {
2634        let mut sig = module.make_signature();
2635        for _ in 0..4 {
2636            sig.params.push(AbiParam::new(types::I64));
2637        }
2638        sig.returns.push(AbiParam::new(types::I64));
2639        module
2640            .declare_function("jit_fractal_noise_1d", Linkage::Import, &sig)
2641            .map_err(|e| format!("declare fractal_noise_1d: {e}"))?
2642    };
2643    let fractal_noise_2d_func_id = {
2644        let mut sig = module.make_signature();
2645        for _ in 0..5 {
2646            sig.params.push(AbiParam::new(types::I64));
2647        }
2648        sig.returns.push(AbiParam::new(types::I64));
2649        module
2650            .declare_function("jit_fractal_noise_2d", Linkage::Import, &sig)
2651            .map_err(|e| format!("declare fractal_noise_2d: {e}"))?
2652    };
2653    let thread_id_func_id = {
2654        let mut sig = module.make_signature();
2655        sig.returns.push(AbiParam::new(types::I64));
2656        module
2657            .declare_function("jit_thread_id", Linkage::Import, &sig)
2658            .map_err(|e| format!("declare thread_id: {e}"))?
2659    };
2660    let current_epoch_millis_func_id = {
2661        let mut sig = module.make_signature();
2662        sig.returns.push(AbiParam::new(types::I64));
2663        module
2664            .declare_function("jit_current_epoch_millis", Linkage::Import, &sig)
2665            .map_err(|e| format!("declare current_epoch_millis: {e}"))?
2666    };
2667
2668    // Declare math externs: unary (u64) -> u64
2669    let math_unary_names = [
2670        "jit_sin",
2671        "jit_cos",
2672        "jit_tan",
2673        "jit_asin",
2674        "jit_acos",
2675        "jit_atan",
2676        "jit_sqrt",
2677        "jit_abs_f64",
2678        "jit_ln",
2679        "jit_exp",
2680        "jit_floor_base10",
2681        "jit_ceiling_base10",
2682        "jit_closest_base10",
2683        "jit_floor_decade",
2684        "jit_ceiling_decade",
2685        "jit_closest_decade",
2686        "jit_floor_binomial",
2687        "jit_ceiling_binomial",
2688        "jit_closest_binomial",
2689        "jit_floor_fibonacci",
2690        "jit_ceiling_fibonacci",
2691        "jit_closest_fibonacci",
2692    ];
2693    let mut math_unary_ids = Vec::new();
2694    for name in &math_unary_names {
2695        let mut sig = module.make_signature();
2696        sig.params.push(AbiParam::new(types::I64));
2697        sig.returns.push(AbiParam::new(types::I64));
2698        math_unary_ids.push(
2699            module
2700                .declare_function(name, Linkage::Import, &sig)
2701                .map_err(|e| format!("declare {name}: {e}"))?,
2702        );
2703    }
2704
2705    // Declare param-helper extern:
2706    // jit_is_positive_fail(u64, name_ptr, name_len) -> u64
2707    // (never returns, but the ABI requires a return type).
2708    let is_positive_fail_id = {
2709        let mut sig = module.make_signature();
2710        sig.params.push(AbiParam::new(types::I64));
2711        sig.params.push(AbiParam::new(types::I64));
2712        sig.params.push(AbiParam::new(types::I64));
2713        sig.returns.push(AbiParam::new(types::I64));
2714        module
2715            .declare_function("jit_is_positive_fail", Linkage::Import, &sig)
2716            .map_err(|e| format!("declare is_positive_fail: {e}"))?
2717    };
2718
2719    // Declare param-helper extern: jit_in_range_fail(u64, u64, u64) -> u64
2720    let in_range_fail_id = {
2721        let mut sig = module.make_signature();
2722        for _ in 0..3 {
2723            sig.params.push(AbiParam::new(types::I64));
2724        }
2725        sig.returns.push(AbiParam::new(types::I64));
2726        module
2727            .declare_function("jit_in_range_fail", Linkage::Import, &sig)
2728            .map_err(|e| format!("declare in_range_fail: {e}"))?
2729    };
2730
2731    // Declare param-helper extern:
2732    // jit_is_one_of_fail(u64, set_ptr, set_len) -> u64
2733    let is_one_of_fail_id = {
2734        let mut sig = module.make_signature();
2735        sig.params.push(AbiParam::new(types::I64));
2736        sig.params.push(AbiParam::new(types::I64));
2737        sig.params.push(AbiParam::new(types::I64));
2738        sig.returns.push(AbiParam::new(types::I64));
2739        module
2740            .declare_function("jit_is_one_of_fail", Linkage::Import, &sig)
2741            .map_err(|e| format!("declare is_one_of_fail: {e}"))?
2742    };
2743
2744    // Declare math externs: binary (u64, u64) -> u64
2745    let math_binary_names = [
2746        "jit_atan2",
2747        "jit_pow",
2748        "jit_round_nearest",
2749        "jit_round_floor",
2750        "jit_round_ceiling",
2751        "jit_f64_mod",
2752    ];
2753    const F64_MOD_HELPER: usize = 5;
2754
2755    // Declare the zero-divisor failure: jit_div_zero_fail(kind) -> u64
2756    let div_zero_fail_id = {
2757        let mut sig = module.make_signature();
2758        sig.params.push(AbiParam::new(types::I64));
2759        sig.returns.push(AbiParam::new(types::I64));
2760        module
2761            .declare_function("jit_div_zero_fail", Linkage::Import, &sig)
2762            .map_err(|e| format!("declare div_zero_fail: {e}"))?
2763    };
2764    let mut math_binary_ids = Vec::new();
2765    for name in &math_binary_names {
2766        let mut sig = module.make_signature();
2767        sig.params.push(AbiParam::new(types::I64));
2768        sig.params.push(AbiParam::new(types::I64));
2769        sig.returns.push(AbiParam::new(types::I64));
2770        math_binary_ids.push(
2771            module
2772                .declare_function(name, Linkage::Import, &sig)
2773                .map_err(|e| format!("declare {name}: {e}"))?,
2774        );
2775    }
2776
2777    // Declare extern: jit_slot_call(kit, inputs, n_in, outputs, n_out,
2778    // scratch, base, n_scratch)
2779    let slot_call_id = {
2780        let mut sig = module.make_signature();
2781        for _ in 0..8 {
2782            sig.params.push(AbiParam::new(types::I64));
2783        }
2784        module
2785            .declare_function("jit_slot_call", Linkage::Import, &sig)
2786            .map_err(|e| format!("declare jit_slot_call: {e}"))?
2787    };
2788
2789    // Declare the string producers: (scratch, base, buffer, out_slot,
2790    // value) for the scalar conversions, (…, ptr, len) for the JSON
2791    // serialization and (…, pairs ptr, n) for the concatenation.
2792    let mut declare_str = |name: &str, args: usize| -> Result<cranelift_module::FuncId, String> {
2793        let mut sig = module.make_signature();
2794        for _ in 0..args {
2795            sig.params.push(AbiParam::new(types::I64));
2796        }
2797        module
2798            .declare_function(name, Linkage::Import, &sig)
2799            .map_err(|e| format!("declare {name}: {e}"))
2800    };
2801    let u64_to_str_id = declare_str("jit_u64_to_str", 5)?;
2802    let i64_to_str_id = declare_str("jit_i64_to_str", 5)?;
2803    let f64_to_str_id = declare_str("jit_f64_to_str", 5)?;
2804    let str_concat_id = declare_str("jit_str_concat", 6)?;
2805    let json_to_str_id = declare_str("jit_json_to_str", 6)?;
2806
2807    // Declare the vector and register helpers: a producer takes
2808    // (scratch, base, buffer, out_slot, w0..w3), a reducer (w0..w3)
2809    // and returns bits, a lane read (lo, hi, i) and returns the word,
2810    // a register producer (buffer, out_slot, w0..w3).
2811    let mut declare_words =
2812        |name: &str, args: usize, returns: bool| -> Result<cranelift_module::FuncId, String> {
2813            let mut sig = module.make_signature();
2814            for _ in 0..args {
2815                sig.params.push(AbiParam::new(types::I64));
2816            }
2817            if returns {
2818                sig.returns.push(AbiParam::new(types::I64));
2819            }
2820            module
2821                .declare_function(name, Linkage::Import, &sig)
2822                .map_err(|e| format!("declare {name}: {e}"))
2823        };
2824    let vec_producer_ids = [
2825        (VecProducer::Add, declare_words("jit_vec_add", 8, false)?),
2826        (
2827            VecProducer::Scale,
2828            declare_words("jit_vec_scale", 8, false)?,
2829        ),
2830        (VecProducer::Norm, declare_words("jit_vec_norm", 8, false)?),
2831        (
2832            VecProducer::HashVec,
2833            declare_words("jit_hash_vec", 8, false)?,
2834        ),
2835        (
2836            VecProducer::XxHash3Vec,
2837            declare_words("jit_xxhash3_vec", 8, false)?,
2838        ),
2839        (
2840            VecProducer::RegToVec,
2841            declare_words("jit_reg_to_vec_f32", 8, false)?,
2842        ),
2843    ];
2844    let vec_reducer_ids = [
2845        (VecReducer::Dot, declare_words("jit_vec_dot", 4, true)?),
2846        (VecReducer::L2, declare_words("jit_vec_l2", 4, true)?),
2847        (
2848            VecReducer::Cosine,
2849            declare_words("jit_vec_cosine", 4, true)?,
2850        ),
2851        (VecReducer::LidMle, declare_words("jit_lid_mle", 4, true)?),
2852    ];
2853    let reg_lane_ids = [
2854        (
2855            RegLaneRead::F32,
2856            declare_words("jit_reg_lane_f32", 3, true)?,
2857        ),
2858        (
2859            RegLaneRead::I16,
2860            declare_words("jit_reg_lane_i16", 3, true)?,
2861        ),
2862        (
2863            RegLaneRead::I64,
2864            declare_words("jit_reg_lane_i64", 3, true)?,
2865        ),
2866    ];
2867    let reg_producer_ids = [
2868        (
2869            RegProducer::WithLaneF32,
2870            declare_words("jit_reg_with_lane_f32", 6, false)?,
2871        ),
2872        (
2873            RegProducer::GatherF32,
2874            declare_words("jit_reg_gather_f32", 6, false)?,
2875        ),
2876        (
2877            RegProducer::VecToRegF32,
2878            declare_words("jit_vec_to_reg_f32", 6, false)?,
2879        ),
2880        (
2881            RegProducer::MulI8,
2882            declare_words("jit_reg_mul_i8", 6, false)?,
2883        ),
2884    ];
2885
2886    // Function signature depends on provenance mode:
2887    // Without: fn(coords: *const u64, buffer: *mut u64, scratch: *mut ScratchBuf)
2888    // With:    fn(coords, buffer, scratch, clean: *mut u8)
2889    let mut sig = module.make_signature();
2890    sig.params.push(AbiParam::new(types::I64)); // coords ptr
2891    sig.params.push(AbiParam::new(types::I64)); // buffer ptr
2892    sig.params.push(AbiParam::new(types::I64)); // scratch ptr
2893    if provenance {
2894        sig.params.push(AbiParam::new(types::I64)); // clean ptr
2895    }
2896    let func_id = module
2897        .declare_function("polydat_kernel", Linkage::Local, &sig)
2898        .map_err(|e| format!("declare kernel: {e}"))?;
2899
2900    let mut ctx = module.make_context();
2901    ctx.func.signature = sig;
2902
2903    let mut fb_ctx = FunctionBuilderContext::new();
2904    {
2905        let mut builder = FunctionBuilder::new(&mut ctx.func, &mut fb_ctx);
2906        let block = builder.create_block();
2907        builder.append_block_params_for_function_params(block);
2908        builder.switch_to_block(block);
2909        builder.seal_block(block);
2910
2911        let _coords_ptr = builder.block_params(block)[0];
2912        let buffer_ptr = builder.block_params(block)[1];
2913        let scratch_ptr = builder.block_params(block)[2];
2914        let clean_ptr = if provenance {
2915            Some(builder.block_params(block)[3])
2916        } else {
2917            None
2918        };
2919
2920        // Import extern functions for calls
2921        let hash_func_ref = module.declare_func_in_func(hash_func_id, builder.func);
2922        let interleave_func_ref = module.declare_func_in_func(interleave_func_id, builder.func);
2923        let shuffle_func_ref = module.declare_func_in_func(shuffle_func_id, builder.func);
2924        let lut_sample_func_ref = module.declare_func_in_func(lut_sample_func_id, builder.func);
2925        let weighted_pick_func_ref =
2926            module.declare_func_in_func(weighted_pick_func_id, builder.func);
2927        let is_positive_fail_ref = module.declare_func_in_func(is_positive_fail_id, builder.func);
2928        let in_range_fail_ref = module.declare_func_in_func(in_range_fail_id, builder.func);
2929        let div_zero_fail_ref = module.declare_func_in_func(div_zero_fail_id, builder.func);
2930        let is_one_of_fail_ref = module.declare_func_in_func(is_one_of_fail_id, builder.func);
2931        let slot_call_ref = module.declare_func_in_func(slot_call_id, builder.func);
2932        let u64_to_str_ref = module.declare_func_in_func(u64_to_str_id, builder.func);
2933        let i64_to_str_ref = module.declare_func_in_func(i64_to_str_id, builder.func);
2934        let f64_to_str_ref = module.declare_func_in_func(f64_to_str_id, builder.func);
2935        let str_concat_ref = module.declare_func_in_func(str_concat_id, builder.func);
2936        let json_to_str_ref = module.declare_func_in_func(json_to_str_id, builder.func);
2937        let vec_producer_refs: Vec<(VecProducer, ir::FuncRef)> = vec_producer_ids
2938            .iter()
2939            .map(|(k, id)| (*k, module.declare_func_in_func(*id, builder.func)))
2940            .collect();
2941        let vec_reducer_refs: Vec<(VecReducer, ir::FuncRef)> = vec_reducer_ids
2942            .iter()
2943            .map(|(k, id)| (*k, module.declare_func_in_func(*id, builder.func)))
2944            .collect();
2945        let reg_lane_refs: Vec<(RegLaneRead, ir::FuncRef)> = reg_lane_ids
2946            .iter()
2947            .map(|(k, id)| (*k, module.declare_func_in_func(*id, builder.func)))
2948            .collect();
2949        let reg_producer_refs: Vec<(RegProducer, ir::FuncRef)> = reg_producer_ids
2950            .iter()
2951            .map(|(k, id)| (*k, module.declare_func_in_func(*id, builder.func)))
2952            .collect();
2953        let pcg_func_ref = module.declare_func_in_func(pcg_func_id, builder.func);
2954        let pcg_stream_func_ref = module.declare_func_in_func(pcg_stream_func_id, builder.func);
2955        let n_of_func_ref = module.declare_func_in_func(n_of_func_id, builder.func);
2956        let cycle_walk_func_ref = module.declare_func_in_func(cycle_walk_func_id, builder.func);
2957        let perlin_1d_func_ref = module.declare_func_in_func(perlin_1d_func_id, builder.func);
2958        let perlin_2d_func_ref = module.declare_func_in_func(perlin_2d_func_id, builder.func);
2959        let simplex_2d_func_ref = module.declare_func_in_func(simplex_2d_func_id, builder.func);
2960        let fractal_noise_1d_func_ref =
2961            module.declare_func_in_func(fractal_noise_1d_func_id, builder.func);
2962        let fractal_noise_2d_func_ref =
2963            module.declare_func_in_func(fractal_noise_2d_func_id, builder.func);
2964        let thread_id_func_ref = module.declare_func_in_func(thread_id_func_id, builder.func);
2965        let current_epoch_millis_func_ref =
2966            module.declare_func_in_func(current_epoch_millis_func_id, builder.func);
2967        let math_unary_refs: Vec<_> = math_unary_ids
2968            .iter()
2969            .map(|id| module.declare_func_in_func(*id, builder.func))
2970            .collect();
2971        let math_binary_refs: Vec<_> = math_binary_ids
2972            .iter()
2973            .map(|id| module.declare_func_in_func(*id, builder.func))
2974            .collect();
2975        // Generate code for each step
2976        for (step_idx, (jit_op, input_slots, output_slots)) in steps.iter().enumerate() {
2977            // Provenance guard: if clean[step_idx] != 0, skip this node.
2978            let skip_block = if let Some(cp) = clean_ptr {
2979                let skip = builder.create_block();
2980                let cont = builder.create_block();
2981                // Load clean[step_idx] (u8)
2982                let offset = builder.ins().iconst(types::I64, step_idx as i64);
2983                let addr = builder.ins().iadd(cp, offset);
2984                let flag = builder.ins().load(types::I8, ir::MemFlags::new(), addr, 0);
2985                let zero = builder.ins().iconst(types::I8, 0);
2986                let is_clean = builder
2987                    .ins()
2988                    .icmp(ir::condcodes::IntCC::NotEqual, flag, zero);
2989                builder.ins().brif(is_clean, skip, &[], cont, &[]);
2990                builder.switch_to_block(cont);
2991                builder.seal_block(cont);
2992                Some(skip)
2993            } else {
2994                None
2995            };
2996            // A7: name the step for the failure path. The store stays only
2997            // when the step calls a helper, the one way native code fails;
2998            // a step of inline arithmetic pays nothing.
2999            let tracker_store = tracker.map(|t| {
3000                let idx = builder.ins().iconst(types::I64, step_idx as i64);
3001                let inst = store_slot(&mut builder, buffer_ptr, t, idx);
3002                (inst, builder.func.dfg.num_insts())
3003            });
3004            match jit_op {
3005                JitOp::Identity => {
3006                    // A copy of every slot the port spans: one for a
3007                    // carrier or handle, two for a 128-bit immediate.
3008                    for (&i, &o) in input_slots.iter().zip(output_slots.iter()) {
3009                        let val = load_slot(&mut builder, buffer_ptr, i);
3010                        store_slot(&mut builder, buffer_ptr, o, val);
3011                    }
3012                }
3013                JitOp::AddConst(c) => {
3014                    let val = load_slot(&mut builder, buffer_ptr, input_slots[0]);
3015                    let c_val = builder.ins().iconst(types::I64, *c as i64);
3016                    let result = builder.ins().iadd(val, c_val);
3017                    store_slot(&mut builder, buffer_ptr, output_slots[0], result);
3018                }
3019                JitOp::MulConst(c) => {
3020                    let val = load_slot(&mut builder, buffer_ptr, input_slots[0]);
3021                    let c_val = builder.ins().iconst(types::I64, *c as i64);
3022                    let result = builder.ins().imul(val, c_val);
3023                    store_slot(&mut builder, buffer_ptr, output_slots[0], result);
3024                }
3025                JitOp::DivConst(c) | JitOp::ModConst(c) => {
3026                    // The body's `/` or `%` by the constant: a zero
3027                    // constant fails at every evaluation as it does.
3028                    let is_div = matches!(jit_op, JitOp::DivConst(_));
3029                    let val = load_slot(&mut builder, buffer_ptr, input_slots[0]);
3030                    if *c == 0 {
3031                        let kind = builder.ins().iconst(types::I64, if is_div { 0 } else { 1 });
3032                        let _ = builder.ins().call(div_zero_fail_ref, &[kind]);
3033                        store_slot(&mut builder, buffer_ptr, output_slots[0], val);
3034                    } else {
3035                        let c_val = builder.ins().iconst(types::I64, *c as i64);
3036                        let result = if is_div {
3037                            builder.ins().udiv(val, c_val)
3038                        } else {
3039                            builder.ins().urem(val, c_val)
3040                        };
3041                        store_slot(&mut builder, buffer_ptr, output_slots[0], result);
3042                    }
3043                }
3044                JitOp::U64DivWire | JitOp::U64ModWire => {
3045                    // The body's `/` or `%` by the wire: a zero divisor
3046                    // fails as it does there; `udiv` and `urem` trap on
3047                    // one, so the failure branches first.
3048                    let is_div = matches!(jit_op, JitOp::U64DivWire);
3049                    let a = load_slot(&mut builder, buffer_ptr, input_slots[0]);
3050                    let b = load_slot(&mut builder, buffer_ptr, input_slots[1]);
3051                    let zero = builder.ins().iconst(types::I64, 0);
3052                    let is_zero = builder.ins().icmp(ir::condcodes::IntCC::Equal, b, zero);
3053                    let fail_block = builder.create_block();
3054                    let ok_block = builder.create_block();
3055                    builder.ins().brif(is_zero, fail_block, &[], ok_block, &[]);
3056                    builder.switch_to_block(fail_block);
3057                    builder.seal_block(fail_block);
3058                    let kind = builder.ins().iconst(types::I64, if is_div { 0 } else { 1 });
3059                    let _ = builder.ins().call(div_zero_fail_ref, &[kind]);
3060                    builder.ins().jump(ok_block, &[]);
3061                    builder.switch_to_block(ok_block);
3062                    builder.seal_block(ok_block);
3063                    let result = if is_div {
3064                        builder.ins().udiv(a, b)
3065                    } else {
3066                        builder.ins().urem(a, b)
3067                    };
3068                    store_slot(&mut builder, buffer_ptr, output_slots[0], result);
3069                }
3070                JitOp::ClampConst(min, max) => {
3071                    let val = load_slot(&mut builder, buffer_ptr, input_slots[0]);
3072                    let min_val = builder.ins().iconst(types::I64, *min as i64);
3073                    let max_val = builder.ins().iconst(types::I64, *max as i64);
3074                    let clamped_lo = builder.ins().umax(val, min_val);
3075                    let clamped = builder.ins().umin(clamped_lo, max_val);
3076                    store_slot(&mut builder, buffer_ptr, output_slots[0], clamped);
3077                }
3078                JitOp::Interleave => {
3079                    let a = load_slot(&mut builder, buffer_ptr, input_slots[0]);
3080                    let b = load_slot(&mut builder, buffer_ptr, input_slots[1]);
3081                    let call = builder.ins().call(interleave_func_ref, &[a, b]);
3082                    let result = builder.inst_results(call)[0];
3083                    store_slot(&mut builder, buffer_ptr, output_slots[0], result);
3084                }
3085                JitOp::MixedRadixConst(radixes) => {
3086                    // Unrolled: for each radix, emit urem + udiv
3087                    let mut remainder = load_slot(&mut builder, buffer_ptr, input_slots[0]);
3088                    for (i, &radix) in radixes.iter().enumerate() {
3089                        if radix == 0 {
3090                            // Unbounded: output = remainder
3091                            store_slot(&mut builder, buffer_ptr, output_slots[i], remainder);
3092                        } else {
3093                            let r = builder.ins().iconst(types::I64, radix as i64);
3094                            let digit = builder.ins().urem(remainder, r);
3095                            store_slot(&mut builder, buffer_ptr, output_slots[i], digit);
3096                            remainder = builder.ins().udiv(remainder, r);
3097                        }
3098                    }
3099                }
3100                JitOp::Hash => {
3101                    let val = load_slot(&mut builder, buffer_ptr, input_slots[0]);
3102                    let call = builder.ins().call(hash_func_ref, &[val]);
3103                    let result = builder.inst_results(call)[0];
3104                    store_slot(&mut builder, buffer_ptr, output_slots[0], result);
3105                }
3106                JitOp::SplitMix64 => {
3107                    let x0 = load_slot(&mut builder, buffer_ptr, input_slots[0]);
3108                    let c_gamma = builder
3109                        .ins()
3110                        .iconst(types::I64, 0x9e3779b97f4a7c15u64 as i64);
3111                    let x1 = builder.ins().iadd(x0, c_gamma);
3112                    let s30 = builder.ins().ushr_imm(x1, 30);
3113                    let x2 = builder.ins().bxor(x1, s30);
3114                    let c_m1 = builder
3115                        .ins()
3116                        .iconst(types::I64, 0xbf58476d1ce4e5b9u64 as i64);
3117                    let x3 = builder.ins().imul(x2, c_m1);
3118                    let s27 = builder.ins().ushr_imm(x3, 27);
3119                    let x4 = builder.ins().bxor(x3, s27);
3120                    let c_m2 = builder
3121                        .ins()
3122                        .iconst(types::I64, 0x94d049bb133111ebu64 as i64);
3123                    let x5 = builder.ins().imul(x4, c_m2);
3124                    let s31 = builder.ins().ushr_imm(x5, 31);
3125                    let result = builder.ins().bxor(x5, s31);
3126                    store_slot(&mut builder, buffer_ptr, output_slots[0], result);
3127                }
3128                JitOp::FairCoin => {
3129                    let x0 = load_slot(&mut builder, buffer_ptr, input_slots[0]);
3130                    let c_gamma = builder
3131                        .ins()
3132                        .iconst(types::I64, 0x9e3779b97f4a7c15u64 as i64);
3133                    let x1 = builder.ins().iadd(x0, c_gamma);
3134                    let s30 = builder.ins().ushr_imm(x1, 30);
3135                    let x2 = builder.ins().bxor(x1, s30);
3136                    let c_m1 = builder
3137                        .ins()
3138                        .iconst(types::I64, 0xbf58476d1ce4e5b9u64 as i64);
3139                    let x3 = builder.ins().imul(x2, c_m1);
3140                    let s27 = builder.ins().ushr_imm(x3, 27);
3141                    let x4 = builder.ins().bxor(x3, s27);
3142                    let c_m2 = builder
3143                        .ins()
3144                        .iconst(types::I64, 0x94d049bb133111ebu64 as i64);
3145                    let x5 = builder.ins().imul(x4, c_m2);
3146                    let s31 = builder.ins().ushr_imm(x5, 31);
3147                    let h = builder.ins().bxor(x5, s31);
3148                    let one = builder.ins().iconst(types::I64, 1);
3149                    let result = builder.ins().band(h, one);
3150                    store_slot(&mut builder, buffer_ptr, output_slots[0], result);
3151                }
3152                JitOp::CoinFlipConst(threshold) => {
3153                    let x = load_slot(&mut builder, buffer_ptr, input_slots[0]);
3154                    let thr = builder.ins().iconst(types::I64, *threshold as i64);
3155                    let cmp = builder
3156                        .ins()
3157                        .icmp(ir::condcodes::IntCC::UnsignedLessThan, x, thr);
3158                    let zero = builder.ins().iconst(types::I64, 0);
3159                    let one = builder.ins().iconst(types::I64, 1);
3160                    let result = builder.ins().select(cmp, one, zero);
3161                    store_slot(&mut builder, buffer_ptr, output_slots[0], result);
3162                }
3163                JitOp::UnfairCoinConst(p_bits) => {
3164                    let x0 = load_slot(&mut builder, buffer_ptr, input_slots[0]);
3165                    let c_gamma = builder
3166                        .ins()
3167                        .iconst(types::I64, 0x9e3779b97f4a7c15u64 as i64);
3168                    let x1 = builder.ins().iadd(x0, c_gamma);
3169                    let s30 = builder.ins().ushr_imm(x1, 30);
3170                    let x2 = builder.ins().bxor(x1, s30);
3171                    let c_m1 = builder
3172                        .ins()
3173                        .iconst(types::I64, 0xbf58476d1ce4e5b9u64 as i64);
3174                    let x3 = builder.ins().imul(x2, c_m1);
3175                    let s27 = builder.ins().ushr_imm(x3, 27);
3176                    let x4 = builder.ins().bxor(x3, s27);
3177                    let c_m2 = builder
3178                        .ins()
3179                        .iconst(types::I64, 0x94d049bb133111ebu64 as i64);
3180                    let x5 = builder.ins().imul(x4, c_m2);
3181                    let s31 = builder.ins().ushr_imm(x5, 31);
3182                    let h = builder.ins().bxor(x5, s31);
3183
3184                    let fval = builder.ins().fcvt_from_uint(types::F64, h);
3185                    let max_f = builder.ins().f64const(u64::MAX as f64);
3186                    let unit = builder.ins().fdiv(fval, max_f);
3187                    let p_f = builder.ins().f64const(f64::from_bits(*p_bits));
3188                    let cmp = builder
3189                        .ins()
3190                        .fcmp(ir::condcodes::FloatCC::LessThan, unit, p_f);
3191                    let zero = builder.ins().iconst(types::I64, 0);
3192                    let one = builder.ins().iconst(types::I64, 1);
3193                    let result = builder.ins().select(cmp, one, zero);
3194                    store_slot(&mut builder, buffer_ptr, output_slots[0], result);
3195                }
3196                JitOp::ChanceConst(p_bits) => {
3197                    let x0 = load_slot(&mut builder, buffer_ptr, input_slots[0]);
3198                    let c_gamma = builder
3199                        .ins()
3200                        .iconst(types::I64, 0x9e3779b97f4a7c15u64 as i64);
3201                    let x1 = builder.ins().iadd(x0, c_gamma);
3202                    let s30 = builder.ins().ushr_imm(x1, 30);
3203                    let x2 = builder.ins().bxor(x1, s30);
3204                    let c_m1 = builder
3205                        .ins()
3206                        .iconst(types::I64, 0xbf58476d1ce4e5b9u64 as i64);
3207                    let x3 = builder.ins().imul(x2, c_m1);
3208                    let s27 = builder.ins().ushr_imm(x3, 27);
3209                    let x4 = builder.ins().bxor(x3, s27);
3210                    let c_m2 = builder
3211                        .ins()
3212                        .iconst(types::I64, 0x94d049bb133111ebu64 as i64);
3213                    let x5 = builder.ins().imul(x4, c_m2);
3214                    let s31 = builder.ins().ushr_imm(x5, 31);
3215                    let h = builder.ins().bxor(x5, s31);
3216
3217                    let fval = builder.ins().fcvt_from_uint(types::F64, h);
3218                    let max_f = builder.ins().f64const(u64::MAX as f64);
3219                    let unit = builder.ins().fdiv(fval, max_f);
3220                    let p_f = builder.ins().f64const(f64::from_bits(*p_bits));
3221                    let cmp = builder
3222                        .ins()
3223                        .fcmp(ir::condcodes::FloatCC::LessThan, unit, p_f);
3224                    let zero_bits = builder.ins().iconst(types::I64, 0.0_f64.to_bits() as i64);
3225                    let one_bits = builder.ins().iconst(types::I64, 1.0_f64.to_bits() as i64);
3226                    let result = builder.ins().select(cmp, one_bits, zero_bits);
3227                    store_slot(&mut builder, buffer_ptr, output_slots[0], result);
3228                }
3229                JitOp::Popcnt => {
3230                    let val = load_slot(&mut builder, buffer_ptr, input_slots[0]);
3231                    let result = builder.ins().popcnt(val);
3232                    store_slot(&mut builder, buffer_ptr, output_slots[0], result);
3233                }
3234                JitOp::Clz => {
3235                    let val = load_slot(&mut builder, buffer_ptr, input_slots[0]);
3236                    let result = builder.ins().clz(val);
3237                    store_slot(&mut builder, buffer_ptr, output_slots[0], result);
3238                }
3239                JitOp::Ctz => {
3240                    let val = load_slot(&mut builder, buffer_ptr, input_slots[0]);
3241                    let result = builder.ins().ctz(val);
3242                    store_slot(&mut builder, buffer_ptr, output_slots[0], result);
3243                }
3244                JitOp::Bswap => {
3245                    let val = load_slot(&mut builder, buffer_ptr, input_slots[0]);
3246                    let result = builder.ins().bswap(val);
3247                    store_slot(&mut builder, buffer_ptr, output_slots[0], result);
3248                }
3249                JitOp::ShuffleConst(feedback, size, min) => {
3250                    let val = load_slot(&mut builder, buffer_ptr, input_slots[0]);
3251                    let fb = builder.ins().iconst(types::I64, *feedback as i64);
3252                    let sz = builder.ins().iconst(types::I64, *size as i64);
3253                    let mn = builder.ins().iconst(types::I64, *min as i64);
3254                    let call = builder.ins().call(shuffle_func_ref, &[val, fb, sz, mn]);
3255                    let result = builder.inst_results(call)[0];
3256                    store_slot(&mut builder, buffer_ptr, output_slots[0], result);
3257                }
3258
3259                // --- f64 ops ---
3260                JitOp::UnitInterval => {
3261                    // u64 → f64: input as f64 / u64::MAX as f64
3262                    let val = load_slot(&mut builder, buffer_ptr, input_slots[0]);
3263                    let fval = builder.ins().fcvt_from_uint(types::F64, val);
3264                    let max_f = builder.ins().f64const(u64::MAX as f64);
3265                    let result = builder.ins().fdiv(fval, max_f);
3266                    store_slot_f64(&mut builder, buffer_ptr, output_slots[0], result);
3267                }
3268                JitOp::F64ToU64 => {
3269                    let fval = load_slot_f64(&mut builder, buffer_ptr, input_slots[0]);
3270                    let result = builder.ins().fcvt_to_uint_sat(types::I64, fval);
3271                    store_slot(&mut builder, buffer_ptr, output_slots[0], result);
3272                }
3273                JitOp::RoundToU64 => {
3274                    let fval = load_slot_f64(&mut builder, buffer_ptr, input_slots[0]);
3275                    let rounded = round_half_away(&mut builder, fval);
3276                    let result = builder.ins().fcvt_to_uint_sat(types::I64, rounded);
3277                    store_slot(&mut builder, buffer_ptr, output_slots[0], result);
3278                }
3279                JitOp::FloorToU64 => {
3280                    let fval = load_slot_f64(&mut builder, buffer_ptr, input_slots[0]);
3281                    let floored = builder.ins().floor(fval);
3282                    let result = builder.ins().fcvt_to_uint_sat(types::I64, floored);
3283                    store_slot(&mut builder, buffer_ptr, output_slots[0], result);
3284                }
3285                JitOp::CeilToU64 => {
3286                    let fval = load_slot_f64(&mut builder, buffer_ptr, input_slots[0]);
3287                    let ceiled = builder.ins().ceil(fval);
3288                    let result = builder.ins().fcvt_to_uint_sat(types::I64, ceiled);
3289                    store_slot(&mut builder, buffer_ptr, output_slots[0], result);
3290                }
3291                JitOp::ClampF64Const(min_bits, max_bits) => {
3292                    let fval = load_slot_f64(&mut builder, buffer_ptr, input_slots[0]);
3293                    let fmin = builder.ins().f64const(f64::from_bits(*min_bits));
3294                    let fmax = builder.ins().f64const(f64::from_bits(*max_bits));
3295                    let clamped = clamp_ir(&mut builder, fval, fmin, fmax);
3296                    store_slot_f64(&mut builder, buffer_ptr, output_slots[0], clamped);
3297                }
3298                JitOp::LerpConst(a_bits, b_bits) => {
3299                    // a + t * (b - a)
3300                    let t = load_slot_f64(&mut builder, buffer_ptr, input_slots[0]);
3301                    let a = builder.ins().f64const(f64::from_bits(*a_bits));
3302                    let b = builder.ins().f64const(f64::from_bits(*b_bits));
3303                    let diff = builder.ins().fsub(b, a);
3304                    let scaled = builder.ins().fmul(t, diff);
3305                    let result = builder.ins().fadd(a, scaled);
3306                    store_slot_f64(&mut builder, buffer_ptr, output_slots[0], result);
3307                }
3308                JitOp::ScaleRangeConst(min_bits, range_bits) => {
3309                    // min + range * (input as f64 / u64::MAX as f64)
3310                    let val = load_slot(&mut builder, buffer_ptr, input_slots[0]);
3311                    let fval = builder.ins().fcvt_from_uint(types::F64, val);
3312                    let max_f = builder.ins().f64const(u64::MAX as f64);
3313                    let t = builder.ins().fdiv(fval, max_f);
3314                    let fmin = builder.ins().f64const(f64::from_bits(*min_bits));
3315                    let frange = builder.ins().f64const(f64::from_bits(*range_bits));
3316                    let scaled = builder.ins().fmul(t, frange);
3317                    let result = builder.ins().fadd(fmin, scaled);
3318                    store_slot_f64(&mut builder, buffer_ptr, output_slots[0], result);
3319                }
3320                JitOp::QuantizeConst(step_bits) => {
3321                    // round(val / step) * step
3322                    let fval = load_slot_f64(&mut builder, buffer_ptr, input_slots[0]);
3323                    let step = builder.ins().f64const(f64::from_bits(*step_bits));
3324                    let divided = builder.ins().fdiv(fval, step);
3325                    let rounded = round_half_away(&mut builder, divided);
3326                    let result = builder.ins().fmul(rounded, step);
3327                    store_slot_f64(&mut builder, buffer_ptr, output_slots[0], result);
3328                }
3329
3330                JitOp::LutSampleConst(lut_ptr, lut_len) => {
3331                    // Extern call: jit_lut_sample(input_bits, lut_ptr, lut_len) -> f64 bits
3332                    let input = load_slot(&mut builder, buffer_ptr, input_slots[0]);
3333                    let ptr_val = builder.ins().iconst(types::I64, *lut_ptr as i64);
3334                    let len_val = builder.ins().iconst(types::I64, *lut_len as i64);
3335                    let call = builder
3336                        .ins()
3337                        .call(lut_sample_func_ref, &[input, ptr_val, len_val]);
3338                    let result = builder.inst_results(call)[0];
3339                    store_slot(&mut builder, buffer_ptr, output_slots[0], result);
3340                }
3341                JitOp::DiscretizeConst(range_bits, buckets) => {
3342                    // clamp(input, 0.0, range - eps) / range * buckets → u64
3343                    let fval = load_slot_f64(&mut builder, buffer_ptr, input_slots[0]);
3344                    let range = f64::from_bits(*range_bits);
3345                    let fzero = builder.ins().f64const(0.0);
3346                    let frange_m_eps = builder.ins().f64const(range - f64::EPSILON);
3347                    let frange = builder.ins().f64const(range);
3348                    let fbuckets = builder.ins().f64const(*buckets as f64);
3349                    let clamped = clamp_ir(&mut builder, fval, fzero, frange_m_eps);
3350                    let divided = builder.ins().fdiv(clamped, frange);
3351                    let scaled = builder.ins().fmul(divided, fbuckets);
3352                    let as_u64 = builder.ins().fcvt_to_uint_sat(types::I64, scaled);
3353                    let max_bucket = builder.ins().iconst(types::I64, (*buckets - 1) as i64);
3354                    let result = builder.ins().umin(as_u64, max_bucket);
3355                    store_slot(&mut builder, buffer_ptr, output_slots[0], result);
3356                }
3357
3358                JitOp::WeightedPickConst(values_ptr, biases_ptr, primaries_ptr, aliases_ptr, n) => {
3359                    // Extern call: jit_weighted_pick(input, values_ptr, biases_ptr, primaries_ptr, aliases_ptr, n)
3360                    let input = load_slot(&mut builder, buffer_ptr, input_slots[0]);
3361                    let v_ptr = builder.ins().iconst(types::I64, *values_ptr as i64);
3362                    let b_ptr = builder.ins().iconst(types::I64, *biases_ptr as i64);
3363                    let p_ptr = builder.ins().iconst(types::I64, *primaries_ptr as i64);
3364                    let a_ptr = builder.ins().iconst(types::I64, *aliases_ptr as i64);
3365                    let n_val = builder.ins().iconst(types::I64, *n as i64);
3366                    let call = builder.ins().call(
3367                        weighted_pick_func_ref,
3368                        &[input, v_ptr, b_ptr, p_ptr, a_ptr, n_val],
3369                    );
3370                    let result = builder.inst_results(call)[0];
3371                    store_slot(&mut builder, buffer_ptr, output_slots[0], result);
3372                }
3373
3374                JitOp::MathUnary(idx) => {
3375                    let input = load_slot(&mut builder, buffer_ptr, input_slots[0]);
3376                    let func_ref = math_unary_refs[*idx as usize];
3377                    let call = builder.ins().call(func_ref, &[input]);
3378                    let result = builder.inst_results(call)[0];
3379                    store_slot(&mut builder, buffer_ptr, output_slots[0], result);
3380                }
3381
3382                JitOp::MathBinary(idx) => {
3383                    let a = load_slot(&mut builder, buffer_ptr, input_slots[0]);
3384                    let b = load_slot(&mut builder, buffer_ptr, input_slots[1]);
3385                    let func_ref = math_binary_refs[*idx as usize];
3386                    let call = builder.ins().call(func_ref, &[a, b]);
3387                    let result = builder.inst_results(call)[0];
3388                    store_slot(&mut builder, buffer_ptr, output_slots[0], result);
3389                }
3390
3391                JitOp::ToF64 => {
3392                    let val = load_slot(&mut builder, buffer_ptr, input_slots[0]);
3393                    let fval = builder.ins().fcvt_from_uint(types::F64, val);
3394                    store_slot_f64(&mut builder, buffer_ptr, output_slots[0], fval);
3395                }
3396
3397                // ── Register plane: one vector instruction per op ──
3398                JitOp::RegBinOp(lane, arith) => {
3399                    let vt = reg_lane_type(*lane);
3400                    let a = load_reg128(&mut builder, buffer_ptr, input_slots[0], vt);
3401                    let b = load_reg128(&mut builder, buffer_ptr, input_slots[2], vt);
3402                    let is_float = matches!(*lane, 4 | 5);
3403                    let r = match (arith, is_float) {
3404                        (0, false) => builder.ins().iadd(a, b),
3405                        (1, false) => builder.ins().isub(a, b),
3406                        (2, false) => builder.ins().imul(a, b),
3407                        (0, true) => builder.ins().fadd(a, b),
3408                        (1, true) => builder.ins().fsub(a, b),
3409                        (2, true) => builder.ins().fmul(a, b),
3410                        _ => unreachable!("RegBinOp arith index out of range"),
3411                    };
3412                    store_reg128(&mut builder, buffer_ptr, output_slots[0], r);
3413                }
3414                JitOp::RegCopy => {
3415                    let v = load_reg128(&mut builder, buffer_ptr, input_slots[0], types::I64X2);
3416                    store_reg128(&mut builder, buffer_ptr, output_slots[0], v);
3417                }
3418                JitOp::RegSplat(lane) => {
3419                    let vt = reg_lane_type(*lane);
3420                    let scalar = match *lane {
3421                        // Integer lanes: u64 slot reduced to lane width.
3422                        0 => {
3423                            let v = load_slot(&mut builder, buffer_ptr, input_slots[0]);
3424                            builder.ins().ireduce(types::I8, v)
3425                        }
3426                        1 => {
3427                            let v = load_slot(&mut builder, buffer_ptr, input_slots[0]);
3428                            builder.ins().ireduce(types::I16, v)
3429                        }
3430                        2 => {
3431                            let v = load_slot(&mut builder, buffer_ptr, input_slots[0]);
3432                            builder.ins().ireduce(types::I32, v)
3433                        }
3434                        3 => load_slot(&mut builder, buffer_ptr, input_slots[0]),
3435                        // Float lanes: f64 slot, demoted for f32.
3436                        4 => {
3437                            let f = load_slot_f64(&mut builder, buffer_ptr, input_slots[0]);
3438                            builder.ins().fdemote(types::F32, f)
3439                        }
3440                        5 => load_slot_f64(&mut builder, buffer_ptr, input_slots[0]),
3441                        _ => unreachable!("RegSplat lane index out of range"),
3442                    };
3443                    let v = builder.ins().splat(vt, scalar);
3444                    store_reg128(&mut builder, buffer_ptr, output_slots[0], v);
3445                }
3446
3447                // Two-wire u64 integer ops — pure Cranelift, no extern call
3448                JitOp::U64Add2 => {
3449                    let a = load_slot(&mut builder, buffer_ptr, input_slots[0]);
3450                    let b = load_slot(&mut builder, buffer_ptr, input_slots[1]);
3451                    let result = builder.ins().iadd(a, b);
3452                    store_slot(&mut builder, buffer_ptr, output_slots[0], result);
3453                }
3454                JitOp::U64Sub2 => {
3455                    let a = load_slot(&mut builder, buffer_ptr, input_slots[0]);
3456                    let b = load_slot(&mut builder, buffer_ptr, input_slots[1]);
3457                    let result = builder.ins().isub(a, b);
3458                    store_slot(&mut builder, buffer_ptr, output_slots[0], result);
3459                }
3460                JitOp::U64Mul2 => {
3461                    let a = load_slot(&mut builder, buffer_ptr, input_slots[0]);
3462                    let b = load_slot(&mut builder, buffer_ptr, input_slots[1]);
3463                    let result = builder.ins().imul(a, b);
3464                    store_slot(&mut builder, buffer_ptr, output_slots[0], result);
3465                }
3466                JitOp::U64Div2 => {
3467                    let a = load_slot(&mut builder, buffer_ptr, input_slots[0]);
3468                    let b = load_slot(&mut builder, buffer_ptr, input_slots[1]);
3469                    // Guard: if b == 0, store 0; else store a / b.
3470                    // Must branch because udiv traps on zero divisor.
3471                    let zero = builder.ins().iconst(types::I64, 0);
3472                    let is_zero = builder.ins().icmp(ir::condcodes::IntCC::Equal, b, zero);
3473                    let div_block = builder.create_block();
3474                    let merge_block = builder.create_block();
3475                    builder.append_block_param(merge_block, types::I64);
3476                    builder
3477                        .ins()
3478                        .brif(is_zero, merge_block, &[zero], div_block, &[]);
3479                    builder.switch_to_block(div_block);
3480                    builder.seal_block(div_block);
3481                    let div_result = builder.ins().udiv(a, b);
3482                    builder.ins().jump(merge_block, &[div_result]);
3483                    builder.switch_to_block(merge_block);
3484                    builder.seal_block(merge_block);
3485                    let result = builder.block_params(merge_block)[0];
3486                    store_slot(&mut builder, buffer_ptr, output_slots[0], result);
3487                }
3488                JitOp::U64Mod2 => {
3489                    let a = load_slot(&mut builder, buffer_ptr, input_slots[0]);
3490                    let b = load_slot(&mut builder, buffer_ptr, input_slots[1]);
3491                    // Guard: if b == 0, store 0; else store a % b.
3492                    // Must branch because urem traps on zero divisor.
3493                    let zero = builder.ins().iconst(types::I64, 0);
3494                    let is_zero = builder.ins().icmp(ir::condcodes::IntCC::Equal, b, zero);
3495                    let rem_block = builder.create_block();
3496                    let merge_block = builder.create_block();
3497                    builder.append_block_param(merge_block, types::I64);
3498                    builder
3499                        .ins()
3500                        .brif(is_zero, merge_block, &[zero], rem_block, &[]);
3501                    builder.switch_to_block(rem_block);
3502                    builder.seal_block(rem_block);
3503                    let rem_result = builder.ins().urem(a, b);
3504                    builder.ins().jump(merge_block, &[rem_result]);
3505                    builder.switch_to_block(merge_block);
3506                    builder.seal_block(merge_block);
3507                    let result = builder.block_params(merge_block)[0];
3508                    store_slot(&mut builder, buffer_ptr, output_slots[0], result);
3509                }
3510                JitOp::U64And => {
3511                    let a = load_slot(&mut builder, buffer_ptr, input_slots[0]);
3512                    let b = load_slot(&mut builder, buffer_ptr, input_slots[1]);
3513                    let result = builder.ins().band(a, b);
3514                    store_slot(&mut builder, buffer_ptr, output_slots[0], result);
3515                }
3516                JitOp::U64Or => {
3517                    let a = load_slot(&mut builder, buffer_ptr, input_slots[0]);
3518                    let b = load_slot(&mut builder, buffer_ptr, input_slots[1]);
3519                    let result = builder.ins().bor(a, b);
3520                    store_slot(&mut builder, buffer_ptr, output_slots[0], result);
3521                }
3522                JitOp::U64Xor => {
3523                    let a = load_slot(&mut builder, buffer_ptr, input_slots[0]);
3524                    let b = load_slot(&mut builder, buffer_ptr, input_slots[1]);
3525                    let result = builder.ins().bxor(a, b);
3526                    store_slot(&mut builder, buffer_ptr, output_slots[0], result);
3527                }
3528                JitOp::U64Shl => {
3529                    let a = load_slot(&mut builder, buffer_ptr, input_slots[0]);
3530                    let b = load_slot(&mut builder, buffer_ptr, input_slots[1]);
3531                    let result = builder.ins().ishl(a, b);
3532                    store_slot(&mut builder, buffer_ptr, output_slots[0], result);
3533                }
3534                JitOp::U64Shr => {
3535                    let a = load_slot(&mut builder, buffer_ptr, input_slots[0]);
3536                    let b = load_slot(&mut builder, buffer_ptr, input_slots[1]);
3537                    let result = builder.ins().ushr(a, b);
3538                    store_slot(&mut builder, buffer_ptr, output_slots[0], result);
3539                }
3540                JitOp::U64Not => {
3541                    let a = load_slot(&mut builder, buffer_ptr, input_slots[0]);
3542                    let result = builder.ins().bnot(a);
3543                    store_slot(&mut builder, buffer_ptr, output_slots[0], result);
3544                }
3545
3546                // Inline binary f64 arithmetic — pure Cranelift, no extern call
3547                JitOp::F64Add => {
3548                    let a = load_slot_f64(&mut builder, buffer_ptr, input_slots[0]);
3549                    let b = load_slot_f64(&mut builder, buffer_ptr, input_slots[1]);
3550                    let result = builder.ins().fadd(a, b);
3551                    store_slot_f64(&mut builder, buffer_ptr, output_slots[0], result);
3552                }
3553                JitOp::F64Sub => {
3554                    let a = load_slot_f64(&mut builder, buffer_ptr, input_slots[0]);
3555                    let b = load_slot_f64(&mut builder, buffer_ptr, input_slots[1]);
3556                    let result = builder.ins().fsub(a, b);
3557                    store_slot_f64(&mut builder, buffer_ptr, output_slots[0], result);
3558                }
3559                JitOp::F64Mul => {
3560                    let a = load_slot_f64(&mut builder, buffer_ptr, input_slots[0]);
3561                    let b = load_slot_f64(&mut builder, buffer_ptr, input_slots[1]);
3562                    let result = builder.ins().fmul(a, b);
3563                    store_slot_f64(&mut builder, buffer_ptr, output_slots[0], result);
3564                }
3565                JitOp::F64Div => {
3566                    let a = load_slot_f64(&mut builder, buffer_ptr, input_slots[0]);
3567                    let b = load_slot_f64(&mut builder, buffer_ptr, input_slots[1]);
3568                    // Guard: if b == 0, result = 0; else result = a / b
3569                    let zero = builder.ins().f64const(0.0);
3570                    let is_zero = builder.ins().fcmp(ir::condcodes::FloatCC::Equal, b, zero);
3571                    let div_result = builder.ins().fdiv(a, b);
3572                    let result = builder.ins().select(is_zero, zero, div_result);
3573                    store_slot_f64(&mut builder, buffer_ptr, output_slots[0], result);
3574                }
3575                JitOp::F64Mod => {
3576                    // The body through its helper: Rust's `%` on floats.
3577                    let a = load_slot(&mut builder, buffer_ptr, input_slots[0]);
3578                    let b = load_slot(&mut builder, buffer_ptr, input_slots[1]);
3579                    let call = builder
3580                        .ins()
3581                        .call(math_binary_refs[F64_MOD_HELPER], &[a, b]);
3582                    let result = builder.inst_results(call)[0];
3583                    store_slot(&mut builder, buffer_ptr, output_slots[0], result);
3584                }
3585
3586                JitOp::IsPositiveCheck { name_ptr, name_len } => {
3587                    // if input == 0: call jit_is_positive_fail (panics);
3588                    // else: store input → output.
3589                    // The branch splits to a fail block for the
3590                    // violation path; the merge reads through the
3591                    // common path after either branch completes.
3592                    let val = load_slot(&mut builder, buffer_ptr, input_slots[0]);
3593                    let zero = builder.ins().iconst(types::I64, 0);
3594                    let is_zero = builder.ins().icmp(ir::condcodes::IntCC::Equal, val, zero);
3595                    let fail_block = builder.create_block();
3596                    let ok_block = builder.create_block();
3597                    builder.ins().brif(is_zero, fail_block, &[], ok_block, &[]);
3598
3599                    builder.switch_to_block(fail_block);
3600                    builder.seal_block(fail_block);
3601                    let np = builder.ins().iconst(types::I64, *name_ptr as i64);
3602                    let nl = builder.ins().iconst(types::I64, *name_len as i64);
3603                    let _ = builder.ins().call(is_positive_fail_ref, &[val, np, nl]);
3604                    // Extern panics — this is unreachable. Jump to
3605                    // ok_block to keep the IR well-formed; the
3606                    // branch never runs in practice.
3607                    builder.ins().jump(ok_block, &[]);
3608
3609                    builder.switch_to_block(ok_block);
3610                    builder.seal_block(ok_block);
3611                    store_slot(&mut builder, buffer_ptr, output_slots[0], val);
3612                }
3613
3614                JitOp::InRangeCheck(lo, hi) => {
3615                    // if input < lo || input > hi: call
3616                    // jit_in_range_fail (panics); else store
3617                    // input → output.
3618                    let val = load_slot(&mut builder, buffer_ptr, input_slots[0]);
3619                    let lo_v = builder.ins().iconst(types::I64, *lo as i64);
3620                    let hi_v = builder.ins().iconst(types::I64, *hi as i64);
3621                    let below =
3622                        builder
3623                            .ins()
3624                            .icmp(ir::condcodes::IntCC::UnsignedLessThan, val, lo_v);
3625                    let above =
3626                        builder
3627                            .ins()
3628                            .icmp(ir::condcodes::IntCC::UnsignedGreaterThan, val, hi_v);
3629                    let out_of_range = builder.ins().bor(below, above);
3630
3631                    let fail_block = builder.create_block();
3632                    let ok_block = builder.create_block();
3633                    builder
3634                        .ins()
3635                        .brif(out_of_range, fail_block, &[], ok_block, &[]);
3636
3637                    builder.switch_to_block(fail_block);
3638                    builder.seal_block(fail_block);
3639                    let _ = builder.ins().call(in_range_fail_ref, &[val, lo_v, hi_v]);
3640                    builder.ins().jump(ok_block, &[]);
3641
3642                    builder.switch_to_block(ok_block);
3643                    builder.seal_block(ok_block);
3644                    store_slot(&mut builder, buffer_ptr, output_slots[0], val);
3645                }
3646
3647                JitOp::IsOneOfCheck {
3648                    allowed,
3649                    set_ptr,
3650                    set_len,
3651                } => {
3652                    // Unroll the allow-list as N inline eq
3653                    // comparisons OR'd together. Fast-path is
3654                    // 1–8 values (the common case); pathologically
3655                    // large allow-lists still JIT but cost N
3656                    // comparisons per cycle.
3657                    let val = load_slot(&mut builder, buffer_ptr, input_slots[0]);
3658                    let mut any_match = builder.ins().iconst(types::I8, 0);
3659                    for allow in allowed.iter() {
3660                        let c = builder.ins().iconst(types::I64, *allow as i64);
3661                        let eq = builder.ins().icmp(ir::condcodes::IntCC::Equal, val, c);
3662                        any_match = builder.ins().bor(any_match, eq);
3663                    }
3664                    let fail_block = builder.create_block();
3665                    let ok_block = builder.create_block();
3666                    // If any_match == 0 (no equality hit),
3667                    // branch to the fail extern. Otherwise
3668                    // jump straight to ok_block.
3669                    builder
3670                        .ins()
3671                        .brif(any_match, ok_block, &[], fail_block, &[]);
3672
3673                    builder.switch_to_block(fail_block);
3674                    builder.seal_block(fail_block);
3675                    let sp = builder.ins().iconst(types::I64, *set_ptr as i64);
3676                    let sl = builder.ins().iconst(types::I64, *set_len as i64);
3677                    let _ = builder.ins().call(is_one_of_fail_ref, &[val, sp, sl]);
3678                    builder.ins().jump(ok_block, &[]);
3679
3680                    builder.switch_to_block(ok_block);
3681                    builder.seal_block(ok_block);
3682                    store_slot(&mut builder, buffer_ptr, output_slots[0], val);
3683                }
3684
3685                JitOp::U64Cmp(cc) => {
3686                    let a = load_slot(&mut builder, buffer_ptr, input_slots[0]);
3687                    let b = load_slot(&mut builder, buffer_ptr, input_slots[1]);
3688                    let cmp = builder.ins().icmp(*cc, a, b);
3689                    let zero = builder.ins().iconst(types::I64, 0);
3690                    let one = builder.ins().iconst(types::I64, 1);
3691                    let result = builder.ins().select(cmp, one, zero);
3692                    store_slot(&mut builder, buffer_ptr, output_slots[0], result);
3693                }
3694                JitOp::F64Cmp(cc) => {
3695                    let a = load_slot_f64(&mut builder, buffer_ptr, input_slots[0]);
3696                    let b = load_slot_f64(
3697                        &mut builder,
3698                        buffer_ptr,
3699                        if input_slots.len() > 1 {
3700                            input_slots[1]
3701                        } else {
3702                            input_slots[0]
3703                        },
3704                    );
3705                    let cmp = builder.ins().fcmp(*cc, a, b);
3706                    let zero = builder.ins().iconst(types::I64, 0);
3707                    let one = builder.ins().iconst(types::I64, 1);
3708                    let result = builder.ins().select(cmp, one, zero);
3709                    store_slot(&mut builder, buffer_ptr, output_slots[0], result);
3710                }
3711                JitOp::SelectU64 => {
3712                    let cond = load_slot(&mut builder, buffer_ptr, input_slots[0]);
3713                    let a = load_slot(
3714                        &mut builder,
3715                        buffer_ptr,
3716                        if input_slots.len() > 1 {
3717                            input_slots[1]
3718                        } else {
3719                            input_slots[0]
3720                        },
3721                    );
3722                    let b = load_slot(
3723                        &mut builder,
3724                        buffer_ptr,
3725                        if input_slots.len() > 2 {
3726                            input_slots[2]
3727                        } else {
3728                            input_slots[0]
3729                        },
3730                    );
3731                    let zero = builder.ins().iconst(types::I64, 0);
3732                    let is_nonzero = builder
3733                        .ins()
3734                        .icmp(ir::condcodes::IntCC::NotEqual, cond, zero);
3735                    let result = builder.ins().select(is_nonzero, a, b);
3736                    store_slot(&mut builder, buffer_ptr, output_slots[0], result);
3737                }
3738                JitOp::SelectF64 => {
3739                    let cond = load_slot(&mut builder, buffer_ptr, input_slots[0]);
3740                    let a = load_slot_f64(
3741                        &mut builder,
3742                        buffer_ptr,
3743                        if input_slots.len() > 1 {
3744                            input_slots[1]
3745                        } else {
3746                            input_slots[0]
3747                        },
3748                    );
3749                    let b = load_slot_f64(
3750                        &mut builder,
3751                        buffer_ptr,
3752                        if input_slots.len() > 2 {
3753                            input_slots[2]
3754                        } else {
3755                            input_slots[0]
3756                        },
3757                    );
3758                    let zero = builder.ins().iconst(types::I64, 0);
3759                    let is_nonzero = builder
3760                        .ins()
3761                        .icmp(ir::condcodes::IntCC::NotEqual, cond, zero);
3762                    let result = builder.ins().select(is_nonzero, a, b);
3763                    store_slot_f64(&mut builder, buffer_ptr, output_slots[0], result);
3764                }
3765
3766                JitOp::I64ToF64 => {
3767                    let val = load_slot(&mut builder, buffer_ptr, input_slots[0]);
3768                    let fval = builder.ins().fcvt_from_sint(types::F64, val);
3769                    store_slot_f64(&mut builder, buffer_ptr, output_slots[0], fval);
3770                }
3771                JitOp::F64ToI64 => {
3772                    let fval = load_slot_f64(&mut builder, buffer_ptr, input_slots[0]);
3773                    let ival = builder.ins().fcvt_to_sint(types::I64, fval);
3774                    store_slot(&mut builder, buffer_ptr, output_slots[0], ival);
3775                }
3776                JitOp::SignExtendI32 => {
3777                    let val = load_slot(&mut builder, buffer_ptr, input_slots[0]);
3778                    let i32_val = builder.ins().ireduce(types::I32, val);
3779                    let sext_val = builder.ins().sextend(types::I64, i32_val);
3780                    store_slot(&mut builder, buffer_ptr, output_slots[0], sext_val);
3781                }
3782                JitOp::SignExtendI16 => {
3783                    let val = load_slot(&mut builder, buffer_ptr, input_slots[0]);
3784                    let i16_val = builder.ins().ireduce(types::I16, val);
3785                    let sext_val = builder.ins().sextend(types::I64, i16_val);
3786                    store_slot(&mut builder, buffer_ptr, output_slots[0], sext_val);
3787                }
3788                JitOp::SignExtendI8 => {
3789                    let val = load_slot(&mut builder, buffer_ptr, input_slots[0]);
3790                    let i8_val = builder.ins().ireduce(types::I8, val);
3791                    let sext_val = builder.ins().sextend(types::I64, i8_val);
3792                    store_slot(&mut builder, buffer_ptr, output_slots[0], sext_val);
3793                }
3794                JitOp::ZeroExtendU32 => {
3795                    let val = load_slot(&mut builder, buffer_ptr, input_slots[0]);
3796                    let mask = builder.ins().iconst(types::I64, 0xFFFFFFFFu64 as i64);
3797                    let result = builder.ins().band(val, mask);
3798                    store_slot(&mut builder, buffer_ptr, output_slots[0], result);
3799                }
3800                JitOp::ZeroExtendU16 => {
3801                    let val = load_slot(&mut builder, buffer_ptr, input_slots[0]);
3802                    let mask = builder.ins().iconst(types::I64, 0xFFFFu64 as i64);
3803                    let result = builder.ins().band(val, mask);
3804                    store_slot(&mut builder, buffer_ptr, output_slots[0], result);
3805                }
3806                JitOp::ZeroExtendU8 => {
3807                    let val = load_slot(&mut builder, buffer_ptr, input_slots[0]);
3808                    let mask = builder.ins().iconst(types::I64, 0xFFu64 as i64);
3809                    let result = builder.ins().band(val, mask);
3810                    store_slot(&mut builder, buffer_ptr, output_slots[0], result);
3811                }
3812                JitOp::ToBool => {
3813                    let val = load_slot(&mut builder, buffer_ptr, input_slots[0]);
3814                    let zero = builder.ins().iconst(types::I64, 0);
3815                    let one = builder.ins().iconst(types::I64, 1);
3816                    let cmp = builder
3817                        .ins()
3818                        .icmp(ir::condcodes::IntCC::NotEqual, val, zero);
3819                    let result = builder.ins().select(cmp, one, zero);
3820                    store_slot(&mut builder, buffer_ptr, output_slots[0], result);
3821                }
3822                JitOp::ConstU64(v) | JitOp::ConstF64(v) => {
3823                    let result = builder.ins().iconst(types::I64, *v as i64);
3824                    store_slot(&mut builder, buffer_ptr, output_slots[0], result);
3825                }
3826                JitOp::HashRangeConst(max) => {
3827                    let input = load_slot(&mut builder, buffer_ptr, input_slots[0]);
3828                    let c_gamma = builder
3829                        .ins()
3830                        .iconst(types::I64, 0x9e3779b97f4a7c15u64 as i64);
3831                    let x1 = builder.ins().iadd(input, c_gamma);
3832                    let s30 = builder.ins().ushr_imm(x1, 30);
3833                    let x2 = builder.ins().bxor(x1, s30);
3834                    let c_m1 = builder
3835                        .ins()
3836                        .iconst(types::I64, 0xbf58476d1ce4e5b9u64 as i64);
3837                    let x3 = builder.ins().imul(x2, c_m1);
3838                    let s27 = builder.ins().ushr_imm(x3, 27);
3839                    let x4 = builder.ins().bxor(x3, s27);
3840                    let c_m2 = builder
3841                        .ins()
3842                        .iconst(types::I64, 0x94d049bb133111ebu64 as i64);
3843                    let x5 = builder.ins().imul(x4, c_m2);
3844                    let s31 = builder.ins().ushr_imm(x5, 31);
3845                    let h = builder.ins().bxor(x5, s31);
3846                    if *max == 0 {
3847                        let zero = builder.ins().iconst(types::I64, 0);
3848                        store_slot(&mut builder, buffer_ptr, output_slots[0], zero);
3849                    } else {
3850                        let m = builder.ins().iconst(types::I64, *max as i64);
3851                        let rem = builder.ins().urem(h, m);
3852                        store_slot(&mut builder, buffer_ptr, output_slots[0], rem);
3853                    }
3854                }
3855                JitOp::HashIntervalConst(min_bits, max_bits) => {
3856                    let input = load_slot(&mut builder, buffer_ptr, input_slots[0]);
3857                    let c_gamma = builder
3858                        .ins()
3859                        .iconst(types::I64, 0x9e3779b97f4a7c15u64 as i64);
3860                    let x1 = builder.ins().iadd(input, c_gamma);
3861                    let s30 = builder.ins().ushr_imm(x1, 30);
3862                    let x2 = builder.ins().bxor(x1, s30);
3863                    let c_m1 = builder
3864                        .ins()
3865                        .iconst(types::I64, 0xbf58476d1ce4e5b9u64 as i64);
3866                    let x3 = builder.ins().imul(x2, c_m1);
3867                    let s27 = builder.ins().ushr_imm(x3, 27);
3868                    let x4 = builder.ins().bxor(x3, s27);
3869                    let c_m2 = builder
3870                        .ins()
3871                        .iconst(types::I64, 0x94d049bb133111ebu64 as i64);
3872                    let x5 = builder.ins().imul(x4, c_m2);
3873                    let s31 = builder.ins().ushr_imm(x5, 31);
3874                    let h = builder.ins().bxor(x5, s31);
3875
3876                    let h_f = builder.ins().fcvt_from_uint(types::F64, h);
3877                    let denom = builder.ins().f64const(u64::MAX as f64);
3878                    let unit = builder.ins().fdiv(h_f, denom);
3879                    let min_f = f64::from_bits(*min_bits);
3880                    let max_f = f64::from_bits(*max_bits);
3881                    let span = builder.ins().f64const(max_f - min_f);
3882                    let min_val = builder.ins().f64const(min_f);
3883                    let scaled = builder.ins().fmul(unit, span);
3884                    let res_f = builder.ins().fadd(min_val, scaled);
3885                    let res = builder
3886                        .ins()
3887                        .bitcast(types::I64, ir::MemFlags::new(), res_f);
3888                    store_slot(&mut builder, buffer_ptr, output_slots[0], res);
3889                }
3890                JitOp::InvLerpConst(a_bits, b_bits) => {
3891                    let input = load_slot(&mut builder, buffer_ptr, input_slots[0]);
3892                    let in_f = builder
3893                        .ins()
3894                        .bitcast(types::F64, ir::MemFlags::new(), input);
3895                    let a_f = f64::from_bits(*a_bits);
3896                    let b_f = f64::from_bits(*b_bits);
3897                    let a_val = builder.ins().f64const(a_f);
3898                    // The body's operations in its order: the reciprocal
3899                    // of the span (infinite for an empty one), the
3900                    // product, the clamp.
3901                    let inv_span = builder.ins().f64const(1.0 / (b_f - a_f));
3902                    let diff = builder.ins().fsub(in_f, a_val);
3903                    let t = builder.ins().fmul(diff, inv_span);
3904                    let zero = builder.ins().f64const(0.0);
3905                    let one = builder.ins().f64const(1.0);
3906                    let res_f = clamp_ir(&mut builder, t, zero, one);
3907                    let res = builder
3908                        .ins()
3909                        .bitcast(types::I64, ir::MemFlags::new(), res_f);
3910                    store_slot(&mut builder, buffer_ptr, output_slots[0], res);
3911                }
3912                JitOp::RemapConst(in_min_bits, in_max_bits, out_min_bits, out_max_bits) => {
3913                    let input = load_slot(&mut builder, buffer_ptr, input_slots[0]);
3914                    let in_f = builder
3915                        .ins()
3916                        .bitcast(types::F64, ir::MemFlags::new(), input);
3917                    let in_min = f64::from_bits(*in_min_bits);
3918                    let in_max = f64::from_bits(*in_max_bits);
3919                    let out_min = f64::from_bits(*out_min_bits);
3920                    let out_max = f64::from_bits(*out_max_bits);
3921                    // The body's operations in its order: a division by
3922                    // the span (not a product with its reciprocal, which
3923                    // differs in the last bit), then the affine step.
3924                    let in_span_val = builder.ins().f64const(in_max - in_min);
3925                    let in_min_val = builder.ins().f64const(in_min);
3926                    let out_min_val = builder.ins().f64const(out_min);
3927                    let out_span_val = builder.ins().f64const(out_max - out_min);
3928                    let diff = builder.ins().fsub(in_f, in_min_val);
3929                    let t = builder.ins().fdiv(diff, in_span_val);
3930                    let scaled = builder.ins().fmul(t, out_span_val);
3931                    let res_f = builder.ins().fadd(out_min_val, scaled);
3932                    let res = builder
3933                        .ins()
3934                        .bitcast(types::I64, ir::MemFlags::new(), res_f);
3935                    store_slot(&mut builder, buffer_ptr, output_slots[0], res);
3936                }
3937                JitOp::EpochOffsetConst(base) => {
3938                    let val = load_slot(&mut builder, buffer_ptr, input_slots[0]);
3939                    let b = builder.ins().iconst(types::I64, *base as i64);
3940                    let res = builder.ins().iadd(val, b);
3941                    store_slot(&mut builder, buffer_ptr, output_slots[0], res);
3942                }
3943                JitOp::EpochScaleConst(factor) => {
3944                    let val = load_slot(&mut builder, buffer_ptr, input_slots[0]);
3945                    let f = builder.ins().iconst(types::I64, *factor as i64);
3946                    let res = builder.ins().imul(val, f);
3947                    store_slot(&mut builder, buffer_ptr, output_slots[0], res);
3948                }
3949                JitOp::ThreadId => {
3950                    let call = builder.ins().call(thread_id_func_ref, &[]);
3951                    let res = builder.inst_results(call)[0];
3952                    store_slot(&mut builder, buffer_ptr, output_slots[0], res);
3953                }
3954                JitOp::CurrentEpochMillis => {
3955                    let call = builder.ins().call(current_epoch_millis_func_ref, &[]);
3956                    let res = builder.inst_results(call)[0];
3957                    store_slot(&mut builder, buffer_ptr, output_slots[0], res);
3958                }
3959                JitOp::Perlin1dConst(perm_ptr, freq_bits) => {
3960                    let input = load_slot(&mut builder, buffer_ptr, input_slots[0]);
3961                    let p = builder.ins().iconst(types::I64, *perm_ptr as i64);
3962                    let fb = builder.ins().iconst(types::I64, *freq_bits as i64);
3963                    let call = builder.ins().call(perlin_1d_func_ref, &[input, p, fb]);
3964                    let res = builder.inst_results(call)[0];
3965                    store_slot(&mut builder, buffer_ptr, output_slots[0], res);
3966                }
3967                JitOp::Perlin2dConst(perm_ptr, freq_bits) => {
3968                    let x = load_slot(&mut builder, buffer_ptr, input_slots[0]);
3969                    let y = load_slot(&mut builder, buffer_ptr, input_slots[1]);
3970                    let p = builder.ins().iconst(types::I64, *perm_ptr as i64);
3971                    let fb = builder.ins().iconst(types::I64, *freq_bits as i64);
3972                    let call = builder.ins().call(perlin_2d_func_ref, &[x, y, p, fb]);
3973                    let res = builder.inst_results(call)[0];
3974                    store_slot(&mut builder, buffer_ptr, output_slots[0], res);
3975                }
3976                JitOp::Simplex2dConst(perm_ptr, freq_bits) => {
3977                    let x = load_slot(&mut builder, buffer_ptr, input_slots[0]);
3978                    let y = load_slot(&mut builder, buffer_ptr, input_slots[1]);
3979                    let p = builder.ins().iconst(types::I64, *perm_ptr as i64);
3980                    let fb = builder.ins().iconst(types::I64, *freq_bits as i64);
3981                    let call = builder.ins().call(simplex_2d_func_ref, &[x, y, p, fb]);
3982                    let res = builder.inst_results(call)[0];
3983                    store_slot(&mut builder, buffer_ptr, output_slots[0], res);
3984                }
3985                JitOp::FractalNoise1dConst(perm_ptr, freq_bits, octaves) => {
3986                    let input = load_slot(&mut builder, buffer_ptr, input_slots[0]);
3987                    let p = builder.ins().iconst(types::I64, *perm_ptr as i64);
3988                    let fb = builder.ins().iconst(types::I64, *freq_bits as i64);
3989                    let oct = builder.ins().iconst(types::I64, *octaves as i64);
3990                    let call = builder
3991                        .ins()
3992                        .call(fractal_noise_1d_func_ref, &[input, p, fb, oct]);
3993                    let res = builder.inst_results(call)[0];
3994                    store_slot(&mut builder, buffer_ptr, output_slots[0], res);
3995                }
3996                JitOp::FractalNoise2dConst(perm_ptr, freq_bits, octaves) => {
3997                    let x = load_slot(&mut builder, buffer_ptr, input_slots[0]);
3998                    let y = load_slot(&mut builder, buffer_ptr, input_slots[1]);
3999                    let p = builder.ins().iconst(types::I64, *perm_ptr as i64);
4000                    let fb = builder.ins().iconst(types::I64, *freq_bits as i64);
4001                    let oct = builder.ins().iconst(types::I64, *octaves as i64);
4002                    let call = builder
4003                        .ins()
4004                        .call(fractal_noise_2d_func_ref, &[x, y, p, fb, oct]);
4005                    let res = builder.inst_results(call)[0];
4006                    store_slot(&mut builder, buffer_ptr, output_slots[0], res);
4007                }
4008                JitOp::CycleWalkConst(range, seed, inc) => {
4009                    let pos = load_slot(&mut builder, buffer_ptr, input_slots[0]);
4010                    let r = builder.ins().iconst(types::I64, *range as i64);
4011                    let s = builder.ins().iconst(types::I64, *seed as i64);
4012                    let i = builder.ins().iconst(types::I64, *inc as i64);
4013                    let call = builder.ins().call(cycle_walk_func_ref, &[pos, r, s, i]);
4014                    let res = builder.inst_results(call)[0];
4015                    store_slot(&mut builder, buffer_ptr, output_slots[0], res);
4016                }
4017
4018                JitOp::VariadicSum => {
4019                    if input_slots.is_empty() {
4020                        let zero = builder.ins().iconst(types::I64, 0);
4021                        store_slot(&mut builder, buffer_ptr, output_slots[0], zero);
4022                    } else {
4023                        let mut acc = load_slot(&mut builder, buffer_ptr, input_slots[0]);
4024                        for &slot in &input_slots[1..] {
4025                            let v = load_slot(&mut builder, buffer_ptr, slot);
4026                            acc = builder.ins().iadd(acc, v);
4027                        }
4028                        store_slot(&mut builder, buffer_ptr, output_slots[0], acc);
4029                    }
4030                }
4031                JitOp::VariadicProduct => {
4032                    if input_slots.is_empty() {
4033                        let one = builder.ins().iconst(types::I64, 1);
4034                        store_slot(&mut builder, buffer_ptr, output_slots[0], one);
4035                    } else {
4036                        let mut acc = load_slot(&mut builder, buffer_ptr, input_slots[0]);
4037                        for &slot in &input_slots[1..] {
4038                            let v = load_slot(&mut builder, buffer_ptr, slot);
4039                            acc = builder.ins().imul(acc, v);
4040                        }
4041                        store_slot(&mut builder, buffer_ptr, output_slots[0], acc);
4042                    }
4043                }
4044                JitOp::VariadicMin => {
4045                    if input_slots.is_empty() {
4046                        let zero = builder.ins().iconst(types::I64, 0);
4047                        store_slot(&mut builder, buffer_ptr, output_slots[0], zero);
4048                    } else {
4049                        let mut acc = load_slot(&mut builder, buffer_ptr, input_slots[0]);
4050                        for &slot in &input_slots[1..] {
4051                            let v = load_slot(&mut builder, buffer_ptr, slot);
4052                            let cmp =
4053                                builder
4054                                    .ins()
4055                                    .icmp(ir::condcodes::IntCC::UnsignedLessThan, v, acc);
4056                            acc = builder.ins().select(cmp, v, acc);
4057                        }
4058                        store_slot(&mut builder, buffer_ptr, output_slots[0], acc);
4059                    }
4060                }
4061                JitOp::VariadicMax => {
4062                    if input_slots.is_empty() {
4063                        let zero = builder.ins().iconst(types::I64, 0);
4064                        store_slot(&mut builder, buffer_ptr, output_slots[0], zero);
4065                    } else {
4066                        let mut acc = load_slot(&mut builder, buffer_ptr, input_slots[0]);
4067                        for &slot in &input_slots[1..] {
4068                            let v = load_slot(&mut builder, buffer_ptr, slot);
4069                            let cmp = builder.ins().icmp(
4070                                ir::condcodes::IntCC::UnsignedGreaterThan,
4071                                v,
4072                                acc,
4073                            );
4074                            acc = builder.ins().select(cmp, v, acc);
4075                        }
4076                        store_slot(&mut builder, buffer_ptr, output_slots[0], acc);
4077                    }
4078                }
4079
4080                JitOp::CeilToMultiple => {
4081                    let val = load_slot(&mut builder, buffer_ptr, input_slots[0]);
4082                    let m = load_slot(&mut builder, buffer_ptr, input_slots[1]);
4083                    let zero = builder.ins().iconst(types::I64, 0);
4084                    let one = builder.ins().iconst(types::I64, 1);
4085                    let is_zero = builder.ins().icmp(ir::condcodes::IntCC::Equal, m, zero);
4086                    let calc_block = builder.create_block();
4087                    let merge_block = builder.create_block();
4088                    builder.append_block_param(merge_block, types::I64);
4089                    builder
4090                        .ins()
4091                        .brif(is_zero, merge_block, &[val], calc_block, &[]);
4092                    builder.switch_to_block(calc_block);
4093                    builder.seal_block(calc_block);
4094                    // `div_ceil` without the sum that overflows near the
4095                    // top, then the saturating product: the body's.
4096                    let div = div_ceil(&mut builder, val, m, one);
4097                    let high = builder.ins().umulhi(div, m);
4098                    let low = builder.ins().imul(div, m);
4099                    let zero_hi = builder.ins().iconst(types::I64, 0);
4100                    let overflows =
4101                        builder
4102                            .ins()
4103                            .icmp(ir::condcodes::IntCC::NotEqual, high, zero_hi);
4104                    let max = builder.ins().iconst(types::I64, -1);
4105                    let mul = builder.ins().select(overflows, max, low);
4106                    builder.ins().jump(merge_block, &[mul]);
4107                    builder.switch_to_block(merge_block);
4108                    builder.seal_block(merge_block);
4109                    let result = builder.block_params(merge_block)[0];
4110                    store_slot(&mut builder, buffer_ptr, output_slots[0], result);
4111                }
4112                JitOp::CheckedAdd => {
4113                    let a = load_slot(&mut builder, buffer_ptr, input_slots[0]);
4114                    let b = load_slot(&mut builder, buffer_ptr, input_slots[1]);
4115                    let sum = builder.ins().iadd(a, b);
4116                    let is_overflow =
4117                        builder
4118                            .ins()
4119                            .icmp(ir::condcodes::IntCC::UnsignedLessThan, sum, a);
4120                    let zero = builder.ins().iconst(types::I64, 0);
4121                    let result = builder.ins().select(is_overflow, zero, sum);
4122                    store_slot(&mut builder, buffer_ptr, output_slots[0], result);
4123                }
4124                JitOp::CheckedSub => {
4125                    let a = load_slot(&mut builder, buffer_ptr, input_slots[0]);
4126                    let b = load_slot(&mut builder, buffer_ptr, input_slots[1]);
4127                    let is_lt = builder
4128                        .ins()
4129                        .icmp(ir::condcodes::IntCC::UnsignedLessThan, a, b);
4130                    let diff = builder.ins().isub(a, b);
4131                    let zero = builder.ins().iconst(types::I64, 0);
4132                    let result = builder.ins().select(is_lt, zero, diff);
4133                    store_slot(&mut builder, buffer_ptr, output_slots[0], result);
4134                }
4135                JitOp::CheckedMul => {
4136                    let a = load_slot(&mut builder, buffer_ptr, input_slots[0]);
4137                    let b = load_slot(&mut builder, buffer_ptr, input_slots[1]);
4138                    let prod = builder.ins().imul(a, b);
4139                    let zero = builder.ins().iconst(types::I64, 0);
4140                    let a_is_zero = builder.ins().icmp(ir::condcodes::IntCC::Equal, a, zero);
4141                    let div_block = builder.create_block();
4142                    let merge_block = builder.create_block();
4143                    builder.append_block_param(merge_block, types::I64);
4144                    builder
4145                        .ins()
4146                        .brif(a_is_zero, merge_block, &[zero], div_block, &[]);
4147                    builder.switch_to_block(div_block);
4148                    builder.seal_block(div_block);
4149                    let div = builder.ins().udiv(prod, a);
4150                    let ok = builder.ins().icmp(ir::condcodes::IntCC::Equal, div, b);
4151                    let mul_res = builder.ins().select(ok, prod, zero);
4152                    builder.ins().jump(merge_block, &[mul_res]);
4153                    builder.switch_to_block(merge_block);
4154                    builder.seal_block(merge_block);
4155                    let result = builder.block_params(merge_block)[0];
4156                    store_slot(&mut builder, buffer_ptr, output_slots[0], result);
4157                }
4158                JitOp::MultiplesAtLeast => {
4159                    let val = load_slot(&mut builder, buffer_ptr, input_slots[0]);
4160                    let m = load_slot(&mut builder, buffer_ptr, input_slots[1]);
4161                    let zero = builder.ins().iconst(types::I64, 0);
4162                    let one = builder.ins().iconst(types::I64, 1);
4163                    let is_zero = builder.ins().icmp(ir::condcodes::IntCC::Equal, m, zero);
4164                    let calc_block = builder.create_block();
4165                    let merge_block = builder.create_block();
4166                    builder.append_block_param(merge_block, types::I64);
4167                    builder
4168                        .ins()
4169                        .brif(is_zero, merge_block, &[zero], calc_block, &[]);
4170                    builder.switch_to_block(calc_block);
4171                    builder.seal_block(calc_block);
4172                    let div = div_ceil(&mut builder, val, m, one);
4173                    builder.ins().jump(merge_block, &[div]);
4174                    builder.switch_to_block(merge_block);
4175                    builder.seal_block(merge_block);
4176                    let result = builder.block_params(merge_block)[0];
4177                    store_slot(&mut builder, buffer_ptr, output_slots[0], result);
4178                }
4179
4180                JitOp::BlendConst(mix_bits) => {
4181                    // The body reinterprets both inputs' bits as f64
4182                    // and returns the mix's bits (`blend` in
4183                    // polydat-nodes `probability.rs`); the lowering does the
4184                    // same, not a numeric conversion.
4185                    let a = load_slot(&mut builder, buffer_ptr, input_slots[0]);
4186                    let b = load_slot(&mut builder, buffer_ptr, input_slots[1]);
4187                    let fa = builder.ins().bitcast(types::F64, ir::MemFlags::new(), a);
4188                    let fb = builder.ins().bitcast(types::F64, ir::MemFlags::new(), b);
4189                    let mix_f64 = f64::from_bits(*mix_bits);
4190                    let mix_val = builder.ins().f64const(mix_f64);
4191                    let one = builder.ins().f64const(1.0);
4192                    let one_minus_mix = builder.ins().fsub(one, mix_val);
4193                    let a_part = builder.ins().fmul(fa, one_minus_mix);
4194                    let b_part = builder.ins().fmul(fb, mix_val);
4195                    let sum = builder.ins().fadd(a_part, b_part);
4196                    let result = builder.ins().bitcast(types::I64, ir::MemFlags::new(), sum);
4197                    store_slot(&mut builder, buffer_ptr, output_slots[0], result);
4198                }
4199                JitOp::LfsrStepConst(feedback) => {
4200                    let val = load_slot(&mut builder, buffer_ptr, input_slots[0]);
4201                    let feedback = builder.ins().iconst(types::I64, *feedback as i64);
4202                    let one = builder.ins().iconst(types::I64, 1);
4203                    let zero = builder.ins().iconst(types::I64, 0);
4204                    let shifted = builder.ins().ushr(val, one);
4205                    let lsb = builder.ins().band(val, one);
4206                    let is_odd = builder
4207                        .ins()
4208                        .icmp(ir::condcodes::IntCC::NotEqual, lsb, zero);
4209                    let fb_mask = builder.ins().select(is_odd, feedback, zero);
4210                    let result = builder.ins().bxor(shifted, fb_mask);
4211                    store_slot(&mut builder, buffer_ptr, output_slots[0], result);
4212                }
4213                JitOp::PcgConst(seed, stream) => {
4214                    let input = load_slot(&mut builder, buffer_ptr, input_slots[0]);
4215                    let s = builder.ins().iconst(types::I64, *seed as i64);
4216                    let st = builder.ins().iconst(types::I64, *stream as i64);
4217                    let call = builder.ins().call(pcg_func_ref, &[input, s, st]);
4218                    let result = builder.inst_results(call)[0];
4219                    store_slot(&mut builder, buffer_ptr, output_slots[0], result);
4220                }
4221                JitOp::PcgStreamConst(seed) => {
4222                    let input = load_slot(&mut builder, buffer_ptr, input_slots[0]);
4223                    let st = load_slot(&mut builder, buffer_ptr, input_slots[1]);
4224                    let s = builder.ins().iconst(types::I64, *seed as i64);
4225                    let call = builder.ins().call(pcg_stream_func_ref, &[input, st, s]);
4226                    let result = builder.inst_results(call)[0];
4227                    store_slot(&mut builder, buffer_ptr, output_slots[0], result);
4228                }
4229                JitOp::NOfConst(n, m) => {
4230                    let input = load_slot(&mut builder, buffer_ptr, input_slots[0]);
4231                    let n_val = builder.ins().iconst(types::I64, *n as i64);
4232                    let m_val = builder.ins().iconst(types::I64, *m as i64);
4233                    let call = builder.ins().call(n_of_func_ref, &[input, n_val, m_val]);
4234                    let result = builder.inst_results(call)[0];
4235                    store_slot(&mut builder, buffer_ptr, output_slots[0], result);
4236                }
4237
4238                JitOp::SlotCall { kit, scratch_base } => {
4239                    // Gather the inputs into the frame, call the kit
4240                    // over them and the state's scratch, scatter the
4241                    // outputs back. The kit's address is an immediate:
4242                    // the kit is shared by every kernel compiled from
4243                    // the program and outlives the code.
4244                    let n_in = input_slots.len();
4245                    let n_out = output_slots.len();
4246                    let frame = |builder: &mut FunctionBuilder, n: usize| {
4247                        builder.create_sized_stack_slot(ir::StackSlotData::new(
4248                            ir::StackSlotKind::ExplicitSlot,
4249                            (n.max(1) * 8) as u32,
4250                            3,
4251                        ))
4252                    };
4253                    let in_frame = frame(&mut builder, n_in);
4254                    let out_frame = frame(&mut builder, n_out);
4255                    for (k, &s) in input_slots.iter().enumerate() {
4256                        let v = load_slot(&mut builder, buffer_ptr, s);
4257                        builder.ins().stack_store(v, in_frame, (k * 8) as i32);
4258                    }
4259                    let kit_ptr = builder
4260                        .ins()
4261                        .iconst(types::I64, std::sync::Arc::as_ptr(&kit.0) as usize as i64);
4262                    let in_ptr = builder.ins().stack_addr(types::I64, in_frame, 0);
4263                    let n_in_v = builder.ins().iconst(types::I64, n_in as i64);
4264                    let out_ptr = builder.ins().stack_addr(types::I64, out_frame, 0);
4265                    let n_out_v = builder.ins().iconst(types::I64, n_out as i64);
4266                    let base_v = builder.ins().iconst(types::I64, *scratch_base as i64);
4267                    let n_sc_v = builder.ins().iconst(types::I64, kit.0.scratch.len() as i64);
4268                    builder.ins().call(
4269                        slot_call_ref,
4270                        &[
4271                            kit_ptr,
4272                            in_ptr,
4273                            n_in_v,
4274                            out_ptr,
4275                            n_out_v,
4276                            scratch_ptr,
4277                            base_v,
4278                            n_sc_v,
4279                        ],
4280                    );
4281                    for (k, &s) in output_slots.iter().enumerate() {
4282                        let v = builder
4283                            .ins()
4284                            .stack_load(types::I64, out_frame, (k * 8) as i32);
4285                        store_slot(&mut builder, buffer_ptr, s, v);
4286                    }
4287                }
4288
4289                JitOp::U64ToStr { scratch_base }
4290                | JitOp::I64ToStr { scratch_base }
4291                | JitOp::F64ToStr { scratch_base } => {
4292                    // The helper writes the digits into the step's entry
4293                    // and publishes the pair into the output slots.
4294                    let func = match jit_op {
4295                        JitOp::U64ToStr { .. } => u64_to_str_ref,
4296                        JitOp::I64ToStr { .. } => i64_to_str_ref,
4297                        _ => f64_to_str_ref,
4298                    };
4299                    let value = load_slot(&mut builder, buffer_ptr, input_slots[0]);
4300                    let base_v = builder.ins().iconst(types::I64, *scratch_base as i64);
4301                    let out_v = builder.ins().iconst(types::I64, output_slots[0] as i64);
4302                    builder
4303                        .ins()
4304                        .call(func, &[scratch_ptr, base_v, buffer_ptr, out_v, value]);
4305                }
4306                JitOp::JsonToStr { scratch_base } => {
4307                    let ptr = load_slot(&mut builder, buffer_ptr, input_slots[0]);
4308                    let len = load_slot(&mut builder, buffer_ptr, input_slots[1]);
4309                    let base_v = builder.ins().iconst(types::I64, *scratch_base as i64);
4310                    let out_v = builder.ins().iconst(types::I64, output_slots[0] as i64);
4311                    builder.ins().call(
4312                        json_to_str_ref,
4313                        &[scratch_ptr, base_v, buffer_ptr, out_v, ptr, len],
4314                    );
4315                }
4316                JitOp::StrConcat { scratch_base } => {
4317                    // The input pairs go into the frame in order; the
4318                    // helper appends each one's bytes into the entry.
4319                    let n_words = input_slots.len();
4320                    let frame = builder.create_sized_stack_slot(ir::StackSlotData::new(
4321                        ir::StackSlotKind::ExplicitSlot,
4322                        (n_words.max(1) * 8) as u32,
4323                        3,
4324                    ));
4325                    for (k, &s) in input_slots.iter().enumerate() {
4326                        let v = load_slot(&mut builder, buffer_ptr, s);
4327                        builder.ins().stack_store(v, frame, (k * 8) as i32);
4328                    }
4329                    let pairs_ptr = builder.ins().stack_addr(types::I64, frame, 0);
4330                    let n_v = builder.ins().iconst(types::I64, (n_words / 2) as i64);
4331                    let base_v = builder.ins().iconst(types::I64, *scratch_base as i64);
4332                    let out_v = builder.ins().iconst(types::I64, output_slots[0] as i64);
4333                    builder.ins().call(
4334                        str_concat_ref,
4335                        &[scratch_ptr, base_v, buffer_ptr, out_v, pairs_ptr, n_v],
4336                    );
4337                }
4338
4339                JitOp::VecProduce { kind, scratch_base } => {
4340                    // The helper runs the node's body over the input
4341                    // words and publishes the pair from the step's
4342                    // entry.
4343                    let func = func_of(&vec_producer_refs, *kind);
4344                    let base_v = builder.ins().iconst(types::I64, *scratch_base as i64);
4345                    let out_v = builder.ins().iconst(types::I64, output_slots[0] as i64);
4346                    let words = load_words(&mut builder, buffer_ptr, input_slots, 4);
4347                    let mut args = vec![scratch_ptr, base_v, buffer_ptr, out_v];
4348                    args.extend(words);
4349                    builder.ins().call(func, &args);
4350                }
4351                JitOp::VecReduce(kind) => {
4352                    let func = func_of(&vec_reducer_refs, *kind);
4353                    let words = load_words(&mut builder, buffer_ptr, input_slots, 4);
4354                    let call = builder.ins().call(func, &words);
4355                    let bits = builder.inst_results(call)[0];
4356                    store_slot(&mut builder, buffer_ptr, output_slots[0], bits);
4357                }
4358                JitOp::RegLane(kind) => {
4359                    let func = func_of(&reg_lane_refs, *kind);
4360                    let words = load_words(&mut builder, buffer_ptr, input_slots, 3);
4361                    let call = builder.ins().call(func, &words);
4362                    let word = builder.inst_results(call)[0];
4363                    store_slot(&mut builder, buffer_ptr, output_slots[0], word);
4364                }
4365                JitOp::RegProduce(kind) => {
4366                    let func = func_of(&reg_producer_refs, *kind);
4367                    let out_v = builder.ins().iconst(types::I64, output_slots[0] as i64);
4368                    let words = load_words(&mut builder, buffer_ptr, input_slots, 4);
4369                    let mut args = vec![buffer_ptr, out_v];
4370                    args.extend(words);
4371                    builder.ins().call(func, &args);
4372                }
4373                JitOp::RegDotF32 => {
4374                    // The products at f32 precision, then the fixed
4375                    // tree ((p0+p1)+(p2+p3)) at f32, then widened: the
4376                    // node's body, operation for operation.
4377                    let a = load_reg128(&mut builder, buffer_ptr, input_slots[0], types::F32X4);
4378                    let b = load_reg128(&mut builder, buffer_ptr, input_slots[2], types::F32X4);
4379                    let p = builder.ins().fmul(a, b);
4380                    let p0 = builder.ins().extractlane(p, 0);
4381                    let p1 = builder.ins().extractlane(p, 1);
4382                    let p2 = builder.ins().extractlane(p, 2);
4383                    let p3 = builder.ins().extractlane(p, 3);
4384                    let s01 = builder.ins().fadd(p0, p1);
4385                    let s23 = builder.ins().fadd(p2, p3);
4386                    let s = builder.ins().fadd(s01, s23);
4387                    let wide = builder.ins().fpromote(types::F64, s);
4388                    store_slot_f64(&mut builder, buffer_ptr, output_slots[0], wide);
4389                }
4390                JitOp::RegShuffleConst(mask) => {
4391                    // Output byte i is input byte mask[i]: the word's
4392                    // bytes lie in memory in little-endian order, which
4393                    // is the order `shuffle` numbers its lanes.
4394                    let x = load_reg128(&mut builder, buffer_ptr, input_slots[0], types::I8X16);
4395                    let imm = builder
4396                        .func
4397                        .dfg
4398                        .immediates
4399                        .push(ir::ConstantData::from(&mask[..]));
4400                    let r = builder.ins().shuffle(x, x, imm);
4401                    store_reg128(&mut builder, buffer_ptr, output_slots[0], r);
4402                }
4403
4404                JitOp::Fallback => {
4405                    // Can't JIT this node — skip (caller should
4406                    // not include fallback ops in JIT steps)
4407                }
4408            }
4409            if let Some((inst, mark)) = tracker_store {
4410                let calls = (mark..builder.func.dfg.num_insts()).any(|i| {
4411                    builder.func.dfg.insts[ir::Inst::from_u32(i as u32)]
4412                        .opcode()
4413                        .is_call()
4414                });
4415                if !calls {
4416                    builder.func.layout.remove_inst(inst);
4417                }
4418            }
4419
4420            // Provenance: set clean[step_idx] = 1, then jump to skip block
4421            if let (Some(cp), Some(skip)) = (clean_ptr, skip_block) {
4422                let offset = builder.ins().iconst(types::I64, step_idx as i64);
4423                let addr = builder.ins().iadd(cp, offset);
4424                let one = builder.ins().iconst(types::I8, 1);
4425                builder.ins().store(ir::MemFlags::new(), one, addr, 0);
4426                builder.ins().jump(skip, &[]);
4427                builder.switch_to_block(skip);
4428                builder.seal_block(skip);
4429            }
4430        }
4431
4432        builder.ins().return_(&[]);
4433        builder.finalize();
4434    }
4435    // Code that calls nothing cannot fail: no helper, no longjmp, no
4436    // panic. The kernel that runs it skips the catch.
4437    let fallible = ctx.func.layout.blocks().any(|block| {
4438        ctx.func
4439            .layout
4440            .block_insts(block)
4441            .any(|inst| ctx.func.dfg.insts[inst].opcode().is_call())
4442    });
4443
4444    module
4445        .define_function(func_id, &mut ctx)
4446        .map_err(|e| format!("define function: {e}"))?;
4447    module.clear_context(&mut ctx);
4448    module
4449        .finalize_definitions()
4450        .map_err(|e| format!("finalize: {e}"))?;
4451
4452    let code_ptr = module.get_finalized_function(func_id);
4453    // The kits the code calls, kept alive beside it.
4454    let kits: Vec<SlotKitRef> = steps
4455        .iter()
4456        .filter_map(|(op, _, _)| op.slot_kit().cloned())
4457        .collect();
4458    let code = super::kernels::JitCode::new(module, kits, fallible);
4459
4460    if provenance {
4461        let prov_fn: NativeProvFn = unsafe { mem::transmute(code_ptr) };
4462        let dummy_raw: NativeFn = unsafe { mem::transmute(code_ptr) };
4463        Ok((dummy_raw, prov_fn, code))
4464    } else {
4465        let raw_fn: NativeFn = unsafe { mem::transmute(code_ptr) };
4466        let dummy_prov: NativeProvFn = unsafe { mem::transmute(code_ptr) };
4467        Ok((raw_fn, dummy_prov, code))
4468    }
4469}
4470
4471// ── Buffer slot helpers ────────────────────────────────────
4472
4473/// Load a u64 from buffer[slot].
4474fn load_slot(builder: &mut FunctionBuilder, buffer_ptr: ir::Value, slot: usize) -> ir::Value {
4475    let offset = (slot * 8) as i32;
4476    builder
4477        .ins()
4478        .load(types::I64, ir::MemFlags::trusted(), buffer_ptr, offset)
4479}
4480
4481/// Store a u64 to buffer[slot].
4482fn store_slot(
4483    builder: &mut FunctionBuilder,
4484    buffer_ptr: ir::Value,
4485    slot: usize,
4486    value: ir::Value,
4487) -> ir::Inst {
4488    let offset = (slot * 8) as i32;
4489    builder
4490        .ins()
4491        .store(ir::MemFlags::trusted(), value, buffer_ptr, offset)
4492}
4493
4494/// Cranelift vector type for a register lane index (the
4495/// `RegBinOp`/`RegSplat` vocabulary).
4496fn reg_lane_type(lane: u8) -> ir::Type {
4497    match lane {
4498        0 => types::I8X16,
4499        1 => types::I16X8,
4500        2 => types::I32X4,
4501        3 => types::I64X2,
4502        4 => types::F32X4,
4503        5 => types::F64X2,
4504        _ => unreachable!("register lane index out of range"),
4505    }
4506}
4507
4508/// Load a 128-bit register value from its two consecutive slots
4509/// (an `Imm2` port occupies two consecutive slots, axiom S1). The buffer is only
4510/// 8-aligned, so the load must NOT carry the aligned flag —
4511/// `MemFlags::new()` permits unaligned 128-bit access.
4512fn load_reg128(
4513    builder: &mut FunctionBuilder,
4514    buffer_ptr: ir::Value,
4515    first_slot: usize,
4516    vt: ir::Type,
4517) -> ir::Value {
4518    let offset = (first_slot * 8) as i32;
4519    builder
4520        .ins()
4521        .load(vt, ir::MemFlags::new(), buffer_ptr, offset)
4522}
4523
4524/// Store a 128-bit register value into its two consecutive slots.
4525fn store_reg128(
4526    builder: &mut FunctionBuilder,
4527    buffer_ptr: ir::Value,
4528    first_slot: usize,
4529    value: ir::Value,
4530) {
4531    let offset = (first_slot * 8) as i32;
4532    builder
4533        .ins()
4534        .store(ir::MemFlags::new(), value, buffer_ptr, offset);
4535}
4536
4537/// `x` rounded half away from zero, as `f64::round` rounds: the
4538/// truncation, plus one in the sign of `x` when the fraction's
4539/// magnitude reaches a half. Exact: where the fraction is nonzero the
4540/// truncation is below 2^52, so the step is representable.
4541fn round_half_away(builder: &mut FunctionBuilder, x: ir::Value) -> ir::Value {
4542    let t = builder.ins().trunc(x);
4543    let frac = builder.ins().fsub(x, t);
4544    let mag = builder.ins().fabs(frac);
4545    let half = builder.ins().f64const(0.5);
4546    let reaches = builder
4547        .ins()
4548        .fcmp(ir::condcodes::FloatCC::GreaterThanOrEqual, mag, half);
4549    let one = builder.ins().f64const(1.0);
4550    let step = builder.ins().fcopysign(one, x);
4551    let up = builder.ins().fadd(t, step);
4552    builder.ins().select(reaches, up, t)
4553}
4554
4555/// `x.clamp(lo, hi)` as `f64::clamp` computes it: `lo` when `x < lo`,
4556/// `hi` when `x > hi`, else `x` itself, so a negative zero and a NaN
4557/// pass through as they do there (`fmax`/`fmin` would return the
4558/// bound's zero for `-0.0`).
4559fn clamp_ir(
4560    builder: &mut FunctionBuilder,
4561    x: ir::Value,
4562    lo: ir::Value,
4563    hi: ir::Value,
4564) -> ir::Value {
4565    let below = builder.ins().fcmp(ir::condcodes::FloatCC::LessThan, x, lo);
4566    let above = builder
4567        .ins()
4568        .fcmp(ir::condcodes::FloatCC::GreaterThan, x, hi);
4569    let capped = builder.ins().select(above, hi, x);
4570    builder.ins().select(below, lo, capped)
4571}
4572
4573/// `val.div_ceil(m)` for a nonzero `m`: the quotient, plus one when
4574/// the remainder is nonzero, with no sum that can overflow.
4575fn div_ceil(
4576    builder: &mut FunctionBuilder,
4577    val: ir::Value,
4578    m: ir::Value,
4579    one: ir::Value,
4580) -> ir::Value {
4581    let q = builder.ins().udiv(val, m);
4582    let r = builder.ins().urem(val, m);
4583    let zero = builder.ins().iconst(types::I64, 0);
4584    let inexact = builder.ins().icmp(ir::condcodes::IntCC::NotEqual, r, zero);
4585    let q1 = builder.ins().iadd(q, one);
4586    builder.ins().select(inexact, q1, q)
4587}
4588
4589/// The function reference declared for one helper of a group.
4590fn func_of<K: PartialEq + Copy>(refs: &[(K, ir::FuncRef)], key: K) -> ir::FuncRef {
4591    refs.iter()
4592        .find(|(k, _)| *k == key)
4593        .map(|(_, r)| *r)
4594        .expect("every helper of the group is declared")
4595}
4596
4597/// Load a step's input words in order, zero past the last, as the
4598/// arguments of a helper that takes a fixed count of words.
4599fn load_words(
4600    builder: &mut FunctionBuilder,
4601    buffer_ptr: ir::Value,
4602    input_slots: &[usize],
4603    n: usize,
4604) -> Vec<ir::Value> {
4605    (0..n)
4606        .map(|k| match input_slots.get(k) {
4607            Some(&s) => load_slot(builder, buffer_ptr, s),
4608            None => builder.ins().iconst(types::I64, 0),
4609        })
4610        .collect()
4611}
4612
4613/// Load an f64 from buffer[slot] (bitcast from i64).
4614fn load_slot_f64(builder: &mut FunctionBuilder, buffer_ptr: ir::Value, slot: usize) -> ir::Value {
4615    let i64_val = load_slot(builder, buffer_ptr, slot);
4616    builder
4617        .ins()
4618        .bitcast(types::F64, ir::MemFlags::new(), i64_val)
4619}
4620
4621/// Store an f64 to buffer[slot] (bitcast to i64).
4622fn store_slot_f64(
4623    builder: &mut FunctionBuilder,
4624    buffer_ptr: ir::Value,
4625    slot: usize,
4626    value: ir::Value,
4627) {
4628    let i64_val = builder
4629        .ins()
4630        .bitcast(types::I64, ir::MemFlags::new(), value);
4631    store_slot(builder, buffer_ptr, slot, i64_val);
4632}
4633
4634// ── Tests ──────────────────────────────────────────────────
4635
4636#[cfg(test)]
4637mod tests {
4638    use super::*;
4639
4640    #[test]
4641    fn jit_identity() {
4642        let steps = vec![(JitOp::Identity, vec![0], vec![1])];
4643        let mut output_map = HashMap::new();
4644        output_map.insert("out".into(), 1);
4645        let mut kernel = compile_jit_raw(1, 2, steps, output_map, Vec::new()).unwrap();
4646        kernel.eval(&[42]);
4647        assert_eq!(kernel.get("out"), 42);
4648    }
4649
4650    #[test]
4651    fn jit_add_const() {
4652        let steps = vec![(JitOp::AddConst(100), vec![0], vec![1])];
4653        let mut output_map = HashMap::new();
4654        output_map.insert("out".into(), 1);
4655        let mut kernel = compile_jit_raw(1, 2, steps, output_map, Vec::new()).unwrap();
4656        kernel.eval(&[5]);
4657        assert_eq!(kernel.get("out"), 105);
4658    }
4659
4660    #[test]
4661    fn jit_mul_const() {
4662        let steps = vec![(JitOp::MulConst(7), vec![0], vec![1])];
4663        let mut output_map = HashMap::new();
4664        output_map.insert("out".into(), 1);
4665        let mut kernel = compile_jit_raw(1, 2, steps, output_map, Vec::new()).unwrap();
4666        kernel.eval(&[6]);
4667        assert_eq!(kernel.get("out"), 42);
4668    }
4669
4670    #[test]
4671    fn jit_mod_const() {
4672        let steps = vec![(JitOp::ModConst(100), vec![0], vec![1])];
4673        let mut output_map = HashMap::new();
4674        output_map.insert("out".into(), 1);
4675        let mut kernel = compile_jit_raw(1, 2, steps, output_map, Vec::new()).unwrap();
4676        kernel.eval(&[542]);
4677        assert_eq!(kernel.get("out"), 42);
4678    }
4679
4680    #[test]
4681    fn jit_hash() {
4682        let steps = vec![(JitOp::Hash, vec![0], vec![1])];
4683        let mut output_map = HashMap::new();
4684        output_map.insert("out".into(), 1);
4685        let mut kernel = compile_jit_raw(1, 2, steps, output_map, Vec::new()).unwrap();
4686
4687        kernel.eval(&[42]);
4688        let v1 = kernel.get("out");
4689
4690        // Verify it matches the Rust xxh3 implementation
4691        let expected = xxhash_rust::xxh3::xxh3_64(&42u64.to_le_bytes());
4692        assert_eq!(v1, expected);
4693    }
4694
4695    #[test]
4696    fn jit_hash_deterministic() {
4697        let steps = vec![(JitOp::Hash, vec![0], vec![1])];
4698        let mut output_map = HashMap::new();
4699        output_map.insert("out".into(), 1);
4700        let mut kernel = compile_jit_raw(1, 2, steps, output_map, Vec::new()).unwrap();
4701
4702        kernel.eval(&[42]);
4703        let v1 = kernel.get("out");
4704        kernel.eval(&[42]);
4705        let v2 = kernel.get("out");
4706        assert_eq!(v1, v2);
4707    }
4708
4709    #[test]
4710    fn jit_chain_hash_mod() {
4711        // hash(cycle) → mod(result, 1000000)
4712        let steps = vec![
4713            (JitOp::Hash, vec![0], vec![1]), // slot 1 = hash(coord 0)
4714            (JitOp::ModConst(1_000_000), vec![1], vec![2]), // slot 2 = slot 1 % 1M
4715        ];
4716        let mut output_map = HashMap::new();
4717        output_map.insert("user_id".into(), 2);
4718        let mut kernel = compile_jit_raw(1, 3, steps, output_map, Vec::new()).unwrap();
4719
4720        kernel.eval(&[42]);
4721        let uid = kernel.get("user_id");
4722        assert!(uid < 1_000_000, "got {uid}");
4723    }
4724
4725    #[test]
4726    fn jit_clamp_const() {
4727        let steps = vec![(JitOp::ClampConst(10, 50), vec![0], vec![1])];
4728        let mut output_map = HashMap::new();
4729        output_map.insert("out".into(), 1);
4730        let mut kernel = compile_jit_raw(1, 2, steps, output_map, Vec::new()).unwrap();
4731
4732        kernel.eval(&[5]);
4733        assert_eq!(kernel.get("out"), 10); // below min
4734
4735        kernel.eval(&[30]);
4736        assert_eq!(kernel.get("out"), 30); // in range
4737
4738        kernel.eval(&[100]);
4739        assert_eq!(kernel.get("out"), 50); // above max
4740    }
4741
4742    #[test]
4743    fn jit_interleave() {
4744        let steps = vec![(JitOp::Interleave, vec![0, 1], vec![2])];
4745        let mut output_map = HashMap::new();
4746        output_map.insert("out".into(), 2);
4747        let mut kernel = compile_jit_raw(2, 3, steps, output_map, Vec::new()).unwrap();
4748
4749        kernel.eval(&[0b101, 0b010]);
4750        // Same as the Interleave node test: result = 0b011001
4751        assert_eq!(kernel.get("out"), 0b01_10_01);
4752    }
4753
4754    #[test]
4755    fn jit_mixed_radix() {
4756        // 100 × 1000 × unbounded
4757        let steps = vec![(
4758            JitOp::MixedRadixConst(vec![100, 1000, 0]),
4759            vec![0],
4760            vec![1, 2, 3],
4761        )];
4762        let mut output_map = HashMap::new();
4763        output_map.insert("d0".into(), 1);
4764        output_map.insert("d1".into(), 2);
4765        output_map.insert("d2".into(), 3);
4766        let mut kernel = compile_jit_raw(1, 4, steps, output_map, Vec::new()).unwrap();
4767
4768        // 4201337 → (37, 13, 42)
4769        kernel.eval(&[4_201_337]);
4770        assert_eq!(kernel.get("d0"), 37);
4771        assert_eq!(kernel.get("d1"), 13);
4772        assert_eq!(kernel.get("d2"), 42);
4773    }
4774
4775    #[test]
4776    fn jit_unit_interval() {
4777        let steps = vec![(JitOp::UnitInterval, vec![0], vec![1])];
4778        let mut output_map = HashMap::new();
4779        output_map.insert("out".into(), 1);
4780        let mut kernel = compile_jit_raw(1, 2, steps, output_map, Vec::new()).unwrap();
4781
4782        kernel.eval(&[0]);
4783        let v = f64::from_bits(kernel.get("out"));
4784        assert!((v - 0.0).abs() < 1e-10);
4785
4786        kernel.eval(&[u64::MAX]);
4787        let v = f64::from_bits(kernel.get("out"));
4788        assert!((v - 1.0).abs() < 1e-10);
4789    }
4790
4791    #[test]
4792    fn jit_f64_to_u64() {
4793        // Store 3.7 as f64 bits in coord slot, convert to u64
4794        let steps = vec![(JitOp::F64ToU64, vec![0], vec![1])];
4795        let mut output_map = HashMap::new();
4796        output_map.insert("out".into(), 1);
4797        let mut kernel = compile_jit_raw(1, 2, steps, output_map, Vec::new()).unwrap();
4798
4799        kernel.eval(&[3.7f64.to_bits()]);
4800        assert_eq!(kernel.get("out"), 3); // truncate toward zero
4801    }
4802
4803    #[test]
4804    fn jit_round_to_u64() {
4805        let steps = vec![(JitOp::RoundToU64, vec![0], vec![1])];
4806        let mut output_map = HashMap::new();
4807        output_map.insert("out".into(), 1);
4808        let mut kernel = compile_jit_raw(1, 2, steps, output_map, Vec::new()).unwrap();
4809
4810        kernel.eval(&[3.7f64.to_bits()]);
4811        assert_eq!(kernel.get("out"), 4);
4812
4813        kernel.eval(&[3.2f64.to_bits()]);
4814        assert_eq!(kernel.get("out"), 3);
4815    }
4816
4817    #[test]
4818    fn jit_clamp_f64() {
4819        let steps = vec![(
4820            JitOp::ClampF64Const(0.0f64.to_bits(), 1.0f64.to_bits()),
4821            vec![0],
4822            vec![1],
4823        )];
4824        let mut output_map = HashMap::new();
4825        output_map.insert("out".into(), 1);
4826        let mut kernel = compile_jit_raw(1, 2, steps, output_map, Vec::new()).unwrap();
4827
4828        kernel.eval(&[(-0.5f64).to_bits()]);
4829        assert_eq!(f64::from_bits(kernel.get("out")), 0.0);
4830
4831        kernel.eval(&[0.5f64.to_bits()]);
4832        assert_eq!(f64::from_bits(kernel.get("out")), 0.5);
4833
4834        kernel.eval(&[1.5f64.to_bits()]);
4835        assert_eq!(f64::from_bits(kernel.get("out")), 1.0);
4836    }
4837
4838    #[test]
4839    fn jit_lerp() {
4840        let steps = vec![(
4841            JitOp::LerpConst(10.0f64.to_bits(), 20.0f64.to_bits()),
4842            vec![0],
4843            vec![1],
4844        )];
4845        let mut output_map = HashMap::new();
4846        output_map.insert("out".into(), 1);
4847        let mut kernel = compile_jit_raw(1, 2, steps, output_map, Vec::new()).unwrap();
4848
4849        kernel.eval(&[0.0f64.to_bits()]);
4850        assert_eq!(f64::from_bits(kernel.get("out")), 10.0);
4851
4852        kernel.eval(&[1.0f64.to_bits()]);
4853        assert_eq!(f64::from_bits(kernel.get("out")), 20.0);
4854
4855        kernel.eval(&[0.5f64.to_bits()]);
4856        assert_eq!(f64::from_bits(kernel.get("out")), 15.0);
4857    }
4858
4859    #[test]
4860    fn jit_scale_range() {
4861        let steps = vec![(
4862            JitOp::ScaleRangeConst(10.0f64.to_bits(), 10.0f64.to_bits()),
4863            vec![0],
4864            vec![1],
4865        )];
4866        let mut output_map = HashMap::new();
4867        output_map.insert("out".into(), 1);
4868        let mut kernel = compile_jit_raw(1, 2, steps, output_map, Vec::new()).unwrap();
4869
4870        kernel.eval(&[0]);
4871        let v = f64::from_bits(kernel.get("out"));
4872        assert!((v - 10.0).abs() < 0.001);
4873
4874        kernel.eval(&[u64::MAX]);
4875        let v = f64::from_bits(kernel.get("out"));
4876        assert!((v - 20.0).abs() < 0.001);
4877    }
4878
4879    #[test]
4880    fn jit_quantize() {
4881        let steps = vec![(JitOp::QuantizeConst(10.0f64.to_bits()), vec![0], vec![1])];
4882        let mut output_map = HashMap::new();
4883        output_map.insert("out".into(), 1);
4884        let mut kernel = compile_jit_raw(1, 2, steps, output_map, Vec::new()).unwrap();
4885
4886        kernel.eval(&[13.0f64.to_bits()]);
4887        assert_eq!(f64::from_bits(kernel.get("out")), 10.0);
4888
4889        kernel.eval(&[17.0f64.to_bits()]);
4890        assert_eq!(f64::from_bits(kernel.get("out")), 20.0);
4891    }
4892
4893    #[test]
4894    fn jit_discretize() {
4895        let steps = vec![(
4896            JitOp::DiscretizeConst(100.0f64.to_bits(), 10),
4897            vec![0],
4898            vec![1],
4899        )];
4900        let mut output_map = HashMap::new();
4901        output_map.insert("out".into(), 1);
4902        let mut kernel = compile_jit_raw(1, 2, steps, output_map, Vec::new()).unwrap();
4903
4904        kernel.eval(&[0.0f64.to_bits()]);
4905        assert_eq!(kernel.get("out"), 0);
4906
4907        kernel.eval(&[55.0f64.to_bits()]);
4908        assert_eq!(kernel.get("out"), 5);
4909
4910        kernel.eval(&[99.0f64.to_bits()]);
4911        assert_eq!(kernel.get("out"), 9);
4912
4913        // Clamp above range
4914        kernel.eval(&[200.0f64.to_bits()]);
4915        assert_eq!(kernel.get("out"), 9);
4916    }
4917
4918    #[test]
4919    fn jit_chain_unit_interval_lerp() {
4920        // u64 → unit_interval → lerp(100, 200)
4921        let steps = vec![
4922            (JitOp::UnitInterval, vec![0], vec![1]),
4923            (
4924                JitOp::LerpConst(100.0f64.to_bits(), 200.0f64.to_bits()),
4925                vec![1],
4926                vec![2],
4927            ),
4928        ];
4929        let mut output_map = HashMap::new();
4930        output_map.insert("out".into(), 2);
4931        let mut kernel = compile_jit_raw(1, 3, steps, output_map, Vec::new()).unwrap();
4932
4933        kernel.eval(&[0]);
4934        let v = f64::from_bits(kernel.get("out"));
4935        assert!((v - 100.0).abs() < 0.001);
4936
4937        kernel.eval(&[u64::MAX]);
4938        let v = f64::from_bits(kernel.get("out"));
4939        assert!((v - 200.0).abs() < 0.001);
4940    }
4941
4942    #[test]
4943    fn jit_multi_step_chain() {
4944        // cycle → add(10) → mul(3) → mod(100)
4945        let steps = vec![
4946            (JitOp::AddConst(10), vec![0], vec![1]),
4947            (JitOp::MulConst(3), vec![1], vec![2]),
4948            (JitOp::ModConst(100), vec![2], vec![3]),
4949        ];
4950        let mut output_map = HashMap::new();
4951        output_map.insert("out".into(), 3);
4952        let mut kernel = compile_jit_raw(1, 4, steps, output_map, Vec::new()).unwrap();
4953
4954        kernel.eval(&[5]);
4955        // (5 + 10) * 3 = 45, 45 % 100 = 45
4956        assert_eq!(kernel.get("out"), 45);
4957    }
4958
4959    // ── Parameter helper predicates ────────────────────────────
4960
4961    #[test]
4962    fn jit_is_positive_check_passes_positive() {
4963        let steps = vec![(
4964            JitOp::IsPositiveCheck {
4965                name_ptr: 0,
4966                name_len: 0,
4967            },
4968            vec![0],
4969            vec![1],
4970        )];
4971        let mut output_map = HashMap::new();
4972        output_map.insert("out".into(), 1);
4973        let mut kernel = compile_jit_raw(1, 2, steps, output_map, Vec::new()).unwrap();
4974        kernel.eval(&[42]);
4975        assert_eq!(kernel.get("out"), 42);
4976        // Large values pass through unchanged — happy path is a
4977        // bare store, not a clamp.
4978        kernel.eval(&[u64::MAX]);
4979        assert_eq!(kernel.get("out"), u64::MAX);
4980    }
4981
4982    #[test]
4983    fn jit_in_range_check_passes_interior() {
4984        let steps = vec![(JitOp::InRangeCheck(10, 100), vec![0], vec![1])];
4985        let mut output_map = HashMap::new();
4986        output_map.insert("out".into(), 1);
4987        let mut kernel = compile_jit_raw(1, 2, steps, output_map, Vec::new()).unwrap();
4988        kernel.eval(&[50]);
4989        assert_eq!(kernel.get("out"), 50);
4990        // Boundaries are inclusive.
4991        kernel.eval(&[10]);
4992        assert_eq!(kernel.get("out"), 10);
4993        kernel.eval(&[100]);
4994        assert_eq!(kernel.get("out"), 100);
4995    }
4996
4997    // Violation paths longjmp back to `invoke_with_catch` and
4998    // surface as ordinary panics; the tests below catch them
4999    // in-process.
5000
5001    #[test]
5002    fn jit_is_one_of_check_passes_allowed_values() {
5003        let steps = vec![(
5004            JitOp::IsOneOfCheck {
5005                allowed: vec![1, 2, 3, 5, 8],
5006                set_ptr: 0,
5007                set_len: 0,
5008            },
5009            vec![0],
5010            vec![1],
5011        )];
5012        let mut output_map = HashMap::new();
5013        output_map.insert("out".into(), 1);
5014        let mut kernel = compile_jit_raw(1, 2, steps, output_map, Vec::new()).unwrap();
5015        // Every allowed value passes straight through.
5016        for v in [1u64, 2, 3, 5, 8] {
5017            kernel.eval(&[v]);
5018            assert_eq!(kernel.get("out"), v);
5019        }
5020    }
5021
5022    #[test]
5023    fn jit_is_one_of_check_accepts_single_element_allow_list() {
5024        // Degenerate case — one-value allow-list reduces to an
5025        // equality check with panic on mismatch.
5026        let steps = vec![(
5027            JitOp::IsOneOfCheck {
5028                allowed: vec![42],
5029                set_ptr: 0,
5030                set_len: 0,
5031            },
5032            vec![0],
5033            vec![1],
5034        )];
5035        let mut output_map = HashMap::new();
5036        output_map.insert("out".into(), 1);
5037        let mut kernel = compile_jit_raw(1, 2, steps, output_map, Vec::new()).unwrap();
5038        kernel.eval(&[42]);
5039        assert_eq!(kernel.get("out"), 42);
5040    }
5041
5042    // ── Catchable panic from JIT predicate fails ──────────────
5043    //
5044    // The extern fail helpers use `_longjmp` back to the Rust
5045    // wrapper, which then raises a Rust `panic!` carrying the
5046    // violation message. The panic originates in Rust land
5047    // (the JIT frame has already been jumped past), so its
5048    // unwind works through Rust-personality FDEs and
5049    // `std::panic::catch_unwind` catches it normally.
5050
5051    fn extract_panic_msg(payload: Box<dyn std::any::Any + Send + 'static>) -> String {
5052        payload
5053            .downcast_ref::<String>()
5054            .cloned()
5055            .or_else(|| payload.downcast_ref::<&str>().map(|s| s.to_string()))
5056            .unwrap_or_else(|| "(non-string panic)".into())
5057    }
5058
5059    #[test]
5060    fn jit_is_positive_violation_is_catchable() {
5061        let steps = vec![(
5062            JitOp::IsPositiveCheck {
5063                name_ptr: 0,
5064                name_len: 0,
5065            },
5066            vec![0],
5067            vec![1],
5068        )];
5069        let mut output_map = HashMap::new();
5070        output_map.insert("out".into(), 1);
5071        let mut kernel = compile_jit_raw(1, 2, steps, output_map, Vec::new()).unwrap();
5072        let err = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| kernel.eval(&[0])))
5073            .expect_err("JIT violation should panic");
5074        assert!(extract_panic_msg(err).contains("must be > 0"));
5075    }
5076
5077    #[test]
5078    fn jit_in_range_violation_is_catchable() {
5079        let steps = vec![(JitOp::InRangeCheck(10, 100), vec![0], vec![1])];
5080        let mut output_map = HashMap::new();
5081        output_map.insert("out".into(), 1);
5082        let mut kernel = compile_jit_raw(1, 2, steps, output_map, Vec::new()).unwrap();
5083
5084        let err = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| kernel.eval(&[5])))
5085            .expect_err("below-range should panic");
5086        assert!(extract_panic_msg(err).contains("outside [10, 100]"));
5087
5088        let err = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| kernel.eval(&[500])))
5089            .expect_err("above-range should panic");
5090        assert!(extract_panic_msg(err).contains("outside [10, 100]"));
5091    }
5092
5093    #[test]
5094    fn jit_is_one_of_violation_is_catchable() {
5095        let steps = vec![(
5096            JitOp::IsOneOfCheck {
5097                allowed: vec![1, 3, 5],
5098                set_ptr: 0,
5099                set_len: 0,
5100            },
5101            vec![0],
5102            vec![1],
5103        )];
5104        let mut output_map = HashMap::new();
5105        output_map.insert("out".into(), 1);
5106        let mut kernel = compile_jit_raw(1, 2, steps, output_map, Vec::new()).unwrap();
5107        let err = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| kernel.eval(&[2])))
5108            .expect_err("disallowed value should panic");
5109        assert!(extract_panic_msg(err).contains("not in allowed set"));
5110    }
5111
5112    #[test]
5113    fn invoke_with_catch_restores_slot_after_foreign_panic() {
5114        // A non-JIT panic from inside `f()` (simulating a bug
5115        // in a hybrid-closure step or any other non-longjmp
5116        // path that may run between setjmp and return) must
5117        // still leave the thread-local JIT_JMP_BUF slot in a
5118        // consistent state. The next `invoke_with_catch` that
5119        // actually calls into JIT code should see a clean
5120        // sentinel.
5121        let caught = std::panic::catch_unwind(|| {
5122            invoke_with_catch(|| panic!("foreign panic"));
5123        });
5124        assert!(caught.is_err(), "foreign panic should propagate out");
5125
5126        // Subsequent legitimate JIT violation is still caught.
5127        let steps = vec![(
5128            JitOp::IsPositiveCheck {
5129                name_ptr: 0,
5130                name_len: 0,
5131            },
5132            vec![0],
5133            vec![1],
5134        )];
5135        let mut output_map = HashMap::new();
5136        output_map.insert("out".into(), 1);
5137        let mut kernel = compile_jit_raw(1, 2, steps, output_map, Vec::new()).unwrap();
5138        let err = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| kernel.eval(&[0])))
5139            .expect_err("JIT violation should panic cleanly after foreign panic");
5140        assert!(extract_panic_msg(err).contains("must be > 0"));
5141
5142        // And the happy path too — no stale pointer lingering.
5143        kernel.eval(&[42]);
5144        assert_eq!(kernel.get("out"), 42);
5145    }
5146
5147    #[test]
5148    fn jit_kernel_survives_multiple_violations() {
5149        // After a caught violation the kernel remains usable —
5150        // the jmp_buf slot is correctly cleared and a
5151        // subsequent happy-path eval returns normally.
5152        let steps = vec![(
5153            JitOp::IsPositiveCheck {
5154                name_ptr: 0,
5155                name_len: 0,
5156            },
5157            vec![0],
5158            vec![1],
5159        )];
5160        let mut output_map = HashMap::new();
5161        output_map.insert("out".into(), 1);
5162        let mut kernel = compile_jit_raw(1, 2, steps, output_map, Vec::new()).unwrap();
5163
5164        for _ in 0..3 {
5165            let _ = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| kernel.eval(&[0])))
5166                .expect_err("violation should still panic");
5167        }
5168        // Happy path still works.
5169        kernel.eval(&[42]);
5170        assert_eq!(kernel.get("out"), 42);
5171    }
5172}