shape_jit/ffi/control/mod.rs
1// Heap allocation audit (PR-9 V8 Gap Closure):
2// Category A (NaN-boxed returns): 2 sites
3// jit_box(HK_ARRAY, ...) — jit_control_map, jit_control_filter
4// Category B (intermediate/consumed): 3 sites
5// Vec::with_capacity for args in jit_call_value, jit_call_foreign_impl,
6// jit_call_foreign_native_args_fixed (consumed within call, not escaped)
7// Arc::new in error path of jit_call_foreign_impl (returned as ValueWord)
8// Category C (heap islands): 0 sites (jit_control_map results — fixed via write barrier)
9//!
10//! Control Flow FFI Functions for JIT
11//!
12//! Higher-order functions (fold, reduce, map, filter, forEach) and function call helpers
13//! for JIT-compiled code.
14
15use crate::context::JITContext;
16// crate::jit_array::JitArray removed — see jit_array.rs SURFACE comment.
17// Higher-order array-walk FFI functions below now route to surface-and-stop
18// per ADR-006 §2.7.4 / W10 jit-playbook §5; the kinded rebuild reads the
19// receiver as `Arc<TypedArrayData>` per-element-kind arm (§2.7.6/Q8).
20use crate::ffi::value_ffi::*;
21#[allow(unused_imports)]
22use crate::ffi::jit_kinds::*;
23use std::ffi::c_void;
24
25// ============================================================================
26// Trampoline VM — thread-local VirtualMachine for JIT-to-VM fallback
27// ============================================================================
28
29use std::cell::Cell;
30
31thread_local! {
32 /// Pointer to a fully-initialized VirtualMachine for executing bytecode
33 /// functions that weren't JIT-compiled. Set by `execute_with_jit()` before
34 /// JIT execution and cleared after. Valid only on the executor thread.
35 static TRAMPOLINE_VM: Cell<*mut shape_vm::VirtualMachine> = const { Cell::new(std::ptr::null_mut()) };
36
37 /// `r5c-2-bz-b-jit-err-surface`: error message from the most recent
38 /// VM-trampoline FFI call (`jit_call_method`) whose VM-side handler
39 /// returned `Err`.
40 ///
41 /// When a trampoline FFI call hits a VM `Err`, it stores the error message
42 /// here and sets `JITContext.pending_call_error = 1`. The MIR emitter
43 /// loads that flag right after the FFI call and deopts (returns
44 /// `SIGNAL_TRAMPOLINE_ERROR`) — the JIT frame is abandoned before the
45 /// FFI's placeholder return value reaches a heap-kinded refcount-retain
46 /// site. `JITExecutor` then `take`s this message and surfaces it as the
47 /// program's runtime error — identical to the error the VM produces, so
48 /// VM and JIT modes agree.
49 static JIT_RUNTIME_ERROR: std::cell::RefCell<Option<String>> =
50 const { std::cell::RefCell::new(None) };
51}
52
53/// Record a VM-trampoline error message for the surrounding JIT execution to
54/// surface on deopt. Called from `jit_call_method`'s `Err` arm. Overwrites any
55/// prior message (the most recent error is the one that triggers the deopt).
56pub fn set_jit_runtime_error(message: String) {
57 JIT_RUNTIME_ERROR.with(|cell| *cell.borrow_mut() = Some(message));
58}
59
60/// Take (and clear) the recorded VM-trampoline error message. Called by
61/// `JITExecutor` when a JIT-compiled function returns a negative signal, so
62/// the clean VM error can be surfaced in place of a generic JIT error code.
63pub fn take_jit_runtime_error() -> Option<String> {
64 JIT_RUNTIME_ERROR.with(|cell| cell.borrow_mut().take())
65}
66
67/// Register the trampoline VM for use during JIT execution.
68///
69/// # Safety
70/// The pointer must remain valid for the entire duration of JIT execution.
71/// Caller must clear it with `unset_trampoline_vm()` after execution.
72pub unsafe fn set_trampoline_vm(vm: *mut shape_vm::VirtualMachine) {
73 TRAMPOLINE_VM.with(|cell| cell.set(vm));
74}
75
76/// Clear the trampoline VM pointer after JIT execution.
77pub fn unset_trampoline_vm() {
78 TRAMPOLINE_VM.with(|cell| cell.set(std::ptr::null_mut()));
79}
80
81/// Access the trampoline VM for read-only queries (schema lookups, etc.)
82pub fn with_trampoline_vm<F, R>(f: F) -> Option<R>
83where
84 F: FnOnce(&shape_vm::VirtualMachine) -> R,
85{
86 TRAMPOLINE_VM.with(|cell| {
87 let vm_ptr = cell.get();
88 if vm_ptr.is_null() {
89 None
90 } else {
91 Some(f(unsafe { &*vm_ptr }))
92 }
93 })
94}
95
96/// Execute a closure with mutable access to the trampoline VM.
97pub fn with_trampoline_vm_mut<F, R>(f: F) -> Option<R>
98where
99 F: FnOnce(&mut shape_vm::VirtualMachine) -> R,
100{
101 TRAMPOLINE_VM.with(|cell| {
102 let vm_ptr = cell.get();
103 if vm_ptr.is_null() {
104 None
105 } else {
106 Some(f(unsafe { &mut *vm_ptr }))
107 }
108 })
109}
110
111/// Dispatch a function call through the trampoline VM for functions that
112/// aren't JIT-compiled (null entries in the function table).
113///
114/// `upvalue_bits` carries the closure's captures when the callee is a
115/// closure (either VM-format heap or unified-heap `JITClosure`). When the
116/// callee is a bare function (TAG_FUNCTION inline), pass `None` to dispatch
117/// through `call_value_immediate_nb` with a plain function ValueWord.
118///
119/// When captures are present we route through `jit_trampoline_call_closure`
120/// on the interpreter side, which binds them to the callee frame's
121/// upvalues exactly as the `op_call_closure` path does. Without this
122/// path, a closure that fails JIT compilation (null entry in the function
123/// table) would be reconstructed as a bare function, losing its captures
124/// and producing `Null` on return.
125fn dispatch_call_via_trampoline_vm(
126 function_id: u32,
127 upvalue_bits: Option<&[u64]>,
128 jit_args: &[u64],
129 jit_ctx: *mut JITContext,
130) -> u64 {
131 use shape_value::NativeKind;
132
133 // r5c-2-bz-b-jit-err-surface: a VM-trampoline value-call that surfaces an
134 // `Err` must NOT continue with a value-shaped placeholder — the same
135 // SIGSEGV class as `jit_call_method` (`TAG_NULL` flowing into a heap-
136 // kinded refcount-retain). Record the error + raise `pending_call_error`
137 // so the MIR-emitted post-call check deopts the JIT frame. The closure
138 // captures the raw `jit_ctx` pointer; it is non-null on every real call
139 // path (the MIR emitter always passes `self.ctx_ptr`).
140 let raise_trampoline_error = |message: String| {
141 set_jit_runtime_error(message);
142 if !jit_ctx.is_null() {
143 unsafe { (*jit_ctx).pending_call_error = 1 };
144 }
145 };
146
147 // §2.7.5 stable-FFI raw-pair shape: each arg / capture pair is
148 // `(u64, NativeKind)`. The JIT MIR emitter widened every arg to
149 // I64 at terminators.rs:651-671 without an associated kind track;
150 // we stamp `NativeKind::UInt64` here (the §2.7.11 callee-
151 // classification kind for function-id-shaped slots, also used as
152 // the "I64-wide raw bits without further classification" carrier
153 // kind at the §2.7.5 stable-FFI boundary). This is NOT a Bool-
154 // default fallback — it is the documented function-id-class kind
155 // shape per ADR-006 §2.7.11/Q12.
156 //
157 // The kind companion is consumed by `jit_trampoline_call_closure`
158 // which wraps each pair as a `KindedSlot` and threads it into the
159 // new frame's locals via `stack_write_kinded`. The VM-side
160 // runtime-tier per-slot kind track is established by the callee's
161 // own FrameDescriptor when it begins execution; the §2.7.5 stable-
162 // FFI handoff doesn't need per-arg semantic kind, only the slot-
163 // size discipline (I64 here).
164 let arg_pairs: Vec<(u64, NativeKind)> = jit_args
165 .iter()
166 .copied()
167 .map(|bits| (bits, NativeKind::UInt64))
168 .collect();
169
170 with_trampoline_vm_mut(|vm| {
171 let func_id = function_id as u16;
172 match upvalue_bits {
173 Some(caps) => {
174 // Shape 2 / 3: closure-with-captures. Route through
175 // `jit_trampoline_call_closure` which materializes a
176 // fresh `OwnedClosureBlock` from `upvalue_bits` and
177 // dispatches via `call_closure_with_nb_args_keepalive`.
178 let capture_pairs: Vec<(u64, NativeKind)> = caps
179 .iter()
180 .copied()
181 .map(|bits| (bits, NativeKind::UInt64))
182 .collect();
183 match vm.jit_trampoline_call_closure(func_id, &capture_pairs, &arg_pairs, None) {
184 Ok(bits) => bits,
185 Err(e) => {
186 raise_trampoline_error(e.to_string());
187 TAG_NULL
188 }
189 }
190 }
191 None => {
192 // Shape 1: bare function callee (no captures). Use the
193 // VM's `call_value_immediate_nb` with a `NativeKind::
194 // UInt64` callee — the §2.7.11 callee-classification
195 // kind for function-id-shaped callees (per
196 // `call_convention.rs:853-877` UInt64 arm).
197 use shape_value::{KindedSlot, ValueSlot};
198 let callee = KindedSlot::new(
199 ValueSlot::from_raw(func_id as u64),
200 NativeKind::UInt64,
201 );
202 let kinded_args: Vec<KindedSlot> = arg_pairs
203 .iter()
204 .map(|(bits, kind)| {
205 KindedSlot::new(ValueSlot::from_raw(*bits), *kind)
206 })
207 .collect();
208 match vm.call_value_immediate_nb(&callee, &kinded_args, None) {
209 Ok(result) => {
210 let bits = result.slot.raw();
211 // The result's strong-count share transfers to
212 // the JIT-side stack slot via the return path.
213 // `mem::forget` prevents the KindedSlot's Drop
214 // from retiring the share — the caller's stack
215 // slot now owns it (same pattern as the runtime
216 // tier's `dispatch_call_value_immediate` per
217 // §2.7.11/Q12).
218 std::mem::forget(result);
219 // The callee KindedSlot was constructed with
220 // raw bits (no Arc share); its Drop is a no-op
221 // for UInt64 kind. Same for the arg
222 // KindedSlots — JIT pre-incremented each share
223 // before crossing the FFI boundary, and the VM
224 // already consumed them by transferring into
225 // the new frame's locals.
226 std::mem::forget(callee);
227 std::mem::forget(kinded_args);
228 bits
229 }
230 Err(e) => {
231 raise_trampoline_error(e.to_string());
232 TAG_NULL
233 }
234 }
235 }
236 }
237 })
238 .unwrap_or_else(|| {
239 // `TRAMPOLINE_VM` is null — the JIT-compiled callee could not be
240 // dispatched. Raise `pending_call_error` so the MIR-emitted check
241 // deopts rather than continuing with a value-shaped placeholder.
242 raise_trampoline_error(format!(
243 "JIT value-call for function {} could not reach the interpreter \
244 trampoline",
245 function_id,
246 ));
247 TAG_NULL
248 })
249}
250
251/// Dispatch a native module function call through the trampoline VM.
252fn dispatch_module_fn_call(
253 _module_fn_id: u32,
254 _jit_args: &[u64],
255 _ctx: *mut JITContext,
256) -> u64 {
257 todo!(
258 "phase-2c §2.7.10/Q11: JIT-side kinded handler ABI rebuild — \
259 dispatch_module_fn_call. ModuleFunction callee construction and \
260 the call_value_immediate_nb dispatch shell now take &KindedSlot \
261 per ADR-006 §2.7.10/Q11; the deleted ValueWord::from_module_function \
262 constructor needs a kinded replacement at the producing call \
263 signature per §2.7.5. See \
264 docs/cluster-audits/wave-10-jit-playbook.md §5."
265 )
266}
267
268/// Call a function by function_id
269/// Stack reads args from ctx.stack before the call
270pub extern "C" fn jit_call_function(
271 ctx: *mut JITContext,
272 function_id: u16,
273 _args: *const u64, // deprecated, pass null
274 _arg_count: usize,
275) -> u64 {
276 unsafe {
277 if ctx.is_null() {
278 return TAG_NULL;
279 }
280 let ctx_ref = &mut *ctx;
281
282 // Check if we have a function table
283 if ctx_ref.function_table.is_null() || (function_id as usize) >= ctx_ref.function_table_len
284 {
285 return TAG_NULL;
286 }
287
288 // Get the function pointer
289 let fn_ptr = *ctx_ref.function_table.add(function_id as usize);
290
291 // The function reads its args from the stack (already pushed by caller)
292 // and returns result on the stack
293 let _result_code = fn_ptr(ctx);
294
295 // Pop result from stack
296 if ctx_ref.stack_ptr > 0 {
297 ctx_ref.stack_ptr -= 1;
298 ctx_ref.stack[ctx_ref.stack_ptr]
299 } else {
300 TAG_NULL
301 }
302 }
303}
304
305/// Call a closure or function value through the trampoline VM.
306///
307/// Stack layout (set by MIR `TerminatorKind::Call` lowering in
308/// `mir_compiler/terminators.rs`):
309/// ```text
310/// [..., callee_bits, arg0_bits, arg1_bits, ..., argN-1_bits, arg_count]
311/// ^ ctx.stack_ptr
312/// ```
313/// `arg_count` is a raw `i64` (not NaN-boxed) per the MIR-side
314/// `iconst(types::I64, args.len() as i64)` push at terminators.rs:681.
315///
316/// ## Callee classification (JIT-internal NaN-box, NOT deleted ValueWord)
317///
318/// Per ADR-006 §2.7.5 the JIT-internal NaN-box scheme in
319/// `crates/shape-jit/src/ffi/value_ffi.rs` is the JIT's own value
320/// representation — it is NOT the deleted runtime-tier `tag_bits`
321/// dispatch (CLAUDE.md "Forbidden Patterns" #4 enumerates the deleted
322/// ValueWord synthesizer / `is_tagged()` runtime handlers / runtime
323/// return-kind stamp family). The JIT-internal predicates
324/// (`is_inline_function`, `is_heap_kind`) operate on the JIT's own
325/// slot encoding and are intentionally preserved.
326///
327/// Two callee shapes flow through `jit_call_value` today:
328///
329/// 1. **Inline function** (`box_function(fn_id)` → `TAG_FUNCTION_BITS`
330/// tag): classified by `is_inline_function(callee_bits)`, function-
331/// id recovered by `unbox_function_id(callee_bits)`. The JIT MIR
332/// emitter pushes this shape when the callee operand is a bare
333/// `FunctionRef` constant.
334///
335/// 2. **Deprecated `unified_box(HK_CLOSURE, JITClosure)` callees**:
336/// classified by `is_heap_kind(callee_bits, HK_CLOSURE)`. This is
337/// the legacy `jit_make_closure` FFI return shape. New code goes
338/// through `jit_finalize_heap_closure` which returns a raw
339/// `Arc::into_raw(Arc<HeapValue::ClosureRaw>)` (no NaN-box) — see
340/// "kind-source gap" below.
341///
342/// ## Kind-source gap (§2.7.5 surface)
343///
344/// `jit_finalize_heap_closure` (the current preferred closure path)
345/// returns `Arc::into_raw(Arc::new(HeapValue::ClosureRaw(owned))) as u64`
346/// — a raw Arc pointer, not a NaN-boxed value. There is no tag-bit
347/// signature on the bits themselves; the callee's `NativeKind::Ptr(
348/// HeapKind::Closure)` is supplied by the producing site at JIT compile
349/// time and lives in a separate side-table the MIR emitter would have
350/// to thread through the call signature.
351///
352/// Under the current `extern "C" fn(*mut JITContext)` signature, the
353/// callee kind is NOT recoverable from `callee_bits` alone — and per
354/// §2.7.7 #4 / #7 / CLAUDE.md "Forbidden Patterns" we MUST NOT probe
355/// `is_heap()` / `is_tagged()` on the bits to classify (those predicates
356/// are JIT-internal NaN-box checks, valid for the *NaN-boxed* shapes
357/// above, but NOT for raw Arc pointers — a heap pointer with bit-63=0
358/// reads as "not tagged" and the predicate returns false; a heap pointer
359/// that happens to alias a tag pattern is a wrong-shape match).
360///
361/// Per the §2.7.5 stamp-at-compile-time discipline, the principled fix
362/// is for the JIT MIR emitter to extend the call signature to carry a
363/// parallel kind track (or per-callee kind side-table) — that is an
364/// ADR-006 §2.7.5 follow-up and an architectural extension beyond this
365/// sub-cluster's scope. For raw-Arc closure callees today, we
366/// surface-and-stop: return TAG_NULL after popping the stack frame, so
367/// the calling MIR continues with a null result rather than crashing
368/// via `extern "C" todo!()` SIGABRT. The shape mirrors the W11-round-1
369/// close's `jit_join_init` surface — graceful surface, audible via
370/// `--trace-jit=shape_jit=debug` (cluster-2 closure-wave-F tracing-crate
371/// migration 2026-05-16), no silent leak (the Arc share remains owned by
372/// the stack slot per the §2.7.7 retain-on-read discipline).
373///
374/// ## Argument kind sourcing
375///
376/// JIT MIR widens args to I64 at terminators.rs:651-671 without an
377/// associated kind track — the same §2.7.5 gap. We pass raw `u64` bits
378/// through to `jit_trampoline_call_closure` paired with `NativeKind::
379/// UInt64` companions (the §2.7.11 callee-classification kind for
380/// function-id callees) ONLY when we can prove the callee is a function
381/// (case 1 above). The VM-side trampoline does not currently consume
382/// per-arg kinds beyond function dispatch; per `call_convention.rs:
383/// jit_trampoline_call_closure` the args are wrapped as
384/// `KindedSlot::new(ValueSlot::from_raw(bits), kind)` and threaded into
385/// the new frame's locals without inspecting kind on the read side. For
386/// heap-bearing args, the W11-round-1 retain-on-read discipline on the
387/// runtime tier handles refcount; the JIT side has already retained
388/// each share before pushing per the §2.7.7 retain semantics. No
389/// fabrication: `NativeKind::UInt64` is the documented function-id
390/// classification kind, not a Bool-default fallback.
391///
392/// ## Forbidden alternatives (refuse on sight)
393///
394/// - **Decoding callee kind from `callee_bits` via tag-bit probe** —
395/// §2.7.7 #4 / #7 / CLAUDE.md "Forbidden Patterns" #4.
396/// - **Bool-default kind for args/callee** — §2.7.7 #9 / CLAUDE.md
397/// "Forbidden rationalizations" ("Soft-fail counter for now, harden
398/// later" — the W11 round-1 walk-back precedent).
399/// - **Silent no-op of the function-id call path** — the supervisor
400/// explicitly refused the W11 round-1 walk-back of `jit_arc_retain` /
401/// `jit_arc_release` to silent no-ops; the same discipline applies
402/// here (ADR-006 §2.7.14 "Reopen amendment").
403/// - **Resurrecting `ValueWord::clone_from_bits` /
404/// `value_word_drop::vw_drop`** — CLAUDE.md "Forbidden Patterns" #1.
405pub extern "C" fn jit_call_value(ctx: *mut JITContext) -> u64 {
406 use crate::ffi::jit_kinds::unified_unbox;
407 use crate::ffi::stack_kind_code;
408 use crate::context::JITClosure;
409 use shape_value::{HeapKind, NativeKind, heap_value::HeapValue};
410 use std::sync::Arc;
411
412 unsafe {
413 if ctx.is_null() {
414 return TAG_NULL;
415 }
416 let ctx_ref = &mut *ctx;
417
418 // Pop arg_count (raw i64 per the MIR-side `iconst(I64,
419 // args.len() as i64)` push at terminators.rs). The parallel-kind
420 // track byte at this slot is `NativeKind::UInt64` (the documented
421 // §2.7.11 / §2.7.5 I64-wide raw bits carrier kind for FFI
422 // scalar sentinels) per the producing emit_kind_track_write call.
423 if ctx_ref.stack_ptr == 0 {
424 tracing::debug!(
425 target: "shape_jit",
426 "jit-call-value BAIL: stack_ptr=0 at arg_count pop",
427 );
428 return TAG_NULL;
429 }
430 ctx_ref.stack_ptr -= 1;
431 let arg_count = ctx_ref.stack[ctx_ref.stack_ptr] as usize;
432 // Reset the kind byte sentinel for hygiene (matches the VM
433 // `pop_kinded` "write Bool sentinel on dead slot" discipline at
434 // `vm_impl/stack.rs:706`).
435 ctx_ref.stack_kinds[ctx_ref.stack_ptr] = stack_kind_code::SENTINEL;
436
437 // Pop args together with their parallel-track kinds (reverse
438 // stack order, then reverse to source order). The §2.7.7 / Q9
439 // lockstep invariant: each `(bits, kind)` pair is read from the
440 // same slot index.
441 let mut arg_pairs: Vec<(u64, NativeKind)> = Vec::with_capacity(arg_count);
442 for _ in 0..arg_count {
443 if ctx_ref.stack_ptr == 0 {
444 return TAG_NULL;
445 }
446 ctx_ref.stack_ptr -= 1;
447 let bits = ctx_ref.stack[ctx_ref.stack_ptr];
448 let code = ctx_ref.stack_kinds[ctx_ref.stack_ptr];
449 ctx_ref.stack_kinds[ctx_ref.stack_ptr] = stack_kind_code::SENTINEL;
450 // Decode the kind from the parallel track. `None` is a
451 // kind-source gap (§2.7.7 #9) — surface, do not Bool-default.
452 let kind = match stack_kind_code::decode(code) {
453 Some(k) => k,
454 None => {
455 tracing::debug!(
456 target: "shape_jit",
457 code,
458 stack_ptr = ctx_ref.stack_ptr,
459 "jit-call-value SURFACE \u{a7}2.7.7 / Q9: arg \
460 kind-byte is SENTINEL / reserved. The producing \
461 call site at `mir_compiler/terminators.rs` must \
462 stamp a concrete NativeKind for every push (no \
463 Bool-default fallback per \u{a7}2.7.7 #9).",
464 );
465 return TAG_NULL;
466 }
467 };
468 arg_pairs.push((bits, kind));
469 }
470 arg_pairs.reverse();
471
472 // Pop callee together with its parallel-track kind. The kind IS
473 // the §2.7.11/Q12 callee-classification discriminator — no tag-
474 // bit decode on `callee_bits`, no `is_heap()` probe (§2.7.7 #4 /
475 // #7 forbidden).
476 if ctx_ref.stack_ptr == 0 {
477 return TAG_NULL;
478 }
479 ctx_ref.stack_ptr -= 1;
480 let callee_bits = ctx_ref.stack[ctx_ref.stack_ptr];
481 let callee_code = ctx_ref.stack_kinds[ctx_ref.stack_ptr];
482 ctx_ref.stack_kinds[ctx_ref.stack_ptr] = stack_kind_code::SENTINEL;
483 let callee_kind = match stack_kind_code::decode(callee_code) {
484 Some(k) => k,
485 None => {
486 tracing::debug!(
487 target: "shape_jit",
488 callee_code,
489 stack_ptr = ctx_ref.stack_ptr,
490 "jit-call-value SURFACE \u{a7}2.7.7 / Q9: callee kind-byte \
491 is SENTINEL / reserved. The producing call site must \
492 stamp the callee's NativeKind from `operand_slot_kind` \
493 per ADR-006 \u{a7}2.7.11 / Q12. No Bool-default fallback \
494 (\u{a7}2.7.7 #9).",
495 );
496 return TAG_NULL;
497 }
498 };
499
500 // ── Dispatch on callee kind (§2.7.11 / Q12) ──────────────────────
501 //
502 // Mirror of the VM-side `dispatch_call_value_immediate` in
503 // `crates/shape-vm/src/executor/control_flow/mod.rs:389`. The
504 // callee kind classifies the dispatch shape:
505 //
506 // - `Ptr(HeapKind::Closure)`: raw `Arc::into_raw(Arc<HeapValue::
507 // ClosureRaw>)` slot bits (the `jit_finalize_heap_closure`
508 // return shape). Recover the `OwnedClosureBlock` via the
509 // `Arc<HeapValue>` slot-tier convention and pass through to
510 // `jit_trampoline_call_closure`, which decodes the closure
511 // captures kinded.
512 //
513 // - `UInt64` / `Int64` / `IntSize` / `UIntSize`: function-id
514 // class kind (the §2.7.5 I64-wide raw bits carrier kind also
515 // used for inline function refs whose bits hold a NaN-boxed
516 // `TAG_FUNCTION` value). Pass through to the trampoline VM's
517 // `call_value_immediate_nb` function-id path.
518 //
519 // - Anything else: surface — the language doesn't have other
520 // callable kinds at the indirect-call entry yet.
521 //
522 // Cases 1 and 2 below are the legacy bit-shape predicates we
523 // preserved through W11-jit-carrier-conversion. They fire only
524 // when the stamped kind is the generic `UInt64` / `Int64`
525 // carrier kind (so the producing site didn't stamp a specific
526 // closure or function-ref kind), and the bits themselves are a
527 // JIT-internal NaN-box pattern (per `value_ffi.rs`). They're
528 // shrunk to a narrow legacy compatibility surface; the principled
529 // dispatch is by kind.
530 let function_id: u16;
531 let mut vm_captures: Option<Vec<u64>> = None;
532
533 match callee_kind {
534 NativeKind::Ptr(HeapKind::Closure) => {
535 // Case 3 (closed): raw `Arc::into_raw(Arc<HeapValue::
536 // ClosureRaw(OwnedClosureBlock)>)` callee bits. Per the
537 // §2.7.11/Q12 slot-tier convention (W7 Round-2.5 close
538 // `5fa4b19`), `clone_with_kind` / `drop_with_kind` for
539 // `HeapKind::Closure` retain/release at the
540 // `Arc<HeapValue>` shape; recover the `OwnedClosureBlock`
541 // by going through `HeapValue::ClosureRaw`.
542 if callee_bits == 0 {
543 tracing::debug!(
544 target: "shape_jit",
545 "jit-call-value BAIL \u{a7}2.7.11/Q12: callee \
546 stamped Ptr(HeapKind::Closure) but bits=0 \u{2014} \
547 producing site emitted a null callee.",
548 );
549 return TAG_NULL;
550 }
551 // W15.2-LANG-4 jit-filter-predicate close (2026-05-18).
552 // Function-typed parameter slots (e.g. `apply(p: (int)=>bool,
553 // ...)`'s `p`) are stamped `Ptr(HeapKind::Closure)` per
554 // the declared `ConcreteType::Function` mapping in
555 // `native_kind_from_concrete_type`. The producing call
556 // signature `apply(pred, ...)` may deliver either runtime
557 // carrier shape per the closure-zero-captures
558 // optimization in the bytecode compiler:
559 //
560 // (a) `Arc::into_raw(Arc<HeapValue::ClosureRaw(block)>)`
561 // — the §2.7.11/Q12 canonical heap-closure carrier
562 // (escaping closure with captures OR escaping
563 // closure without captures routed through
564 // `emit_heap_closure` + `jit_finalize_heap_closure`).
565 //
566 // (b) `box_function(fn_id)` — the §2.7.11 NaN-box
567 // function-ref carrier (the bytecode compiler can
568 // emit `Operand::Function(fid)` for a `let pred =
569 // |x| x > 24` shape where `x > 24` has no captures
570 // AND the binding storage class permits the
571 // fn-ref-as-callable optimization).
572 //
573 // The carrier shape is determined at producing-site
574 // codegen time but the declared-type-based kind
575 // classification (`ConcreteType::Function` →
576 // `Ptr(HeapKind::Closure)`) can't statically project the
577 // carrier — the kind is the slot's *semantic* type, not
578 // the runtime carrier discriminator. Dispatch on the
579 // bit-shape predicate before falling through to the
580 // `Arc::from_raw` deref to avoid UB on the NaN-box
581 // carrier (which would deref random memory).
582 if is_inline_function(callee_bits) {
583 function_id = unbox_function_id(callee_bits);
584 // No captures — bare function ref path.
585 let args: Vec<u64> = arg_pairs.iter().map(|(b, _)| *b).collect();
586 if !ctx_ref.function_table.is_null()
587 && (function_id as usize) < ctx_ref.function_table_len
588 {
589 let raw_fn_ptr =
590 *(ctx_ref.function_table as *const *const u8)
591 .add(function_id as usize);
592 if !raw_fn_ptr.is_null() {
593 ctx_ref.stack_ptr = 0;
594 let _signal = call_jit_fn_with_args(raw_fn_ptr, ctx, &args);
595 if ctx_ref.stack_ptr > 0 {
596 ctx_ref.stack_ptr -= 1;
597 let ret_bits = ctx_ref.stack[ctx_ref.stack_ptr];
598 ctx_ref.stack_kinds[ctx_ref.stack_ptr] =
599 stack_kind_code::SENTINEL;
600 return ret_bits;
601 }
602 return TAG_NULL;
603 }
604 }
605 // Fall through to trampoline VM for the bare-fn case.
606 return dispatch_call_via_trampoline_vm(
607 function_id as u32,
608 None,
609 &args,
610 ctx,
611 );
612 }
613 // Borrow the `Arc<HeapValue>` (use `from_raw` + `into_raw`
614 // to avoid taking the share — the share stays in the
615 // stack slot per §2.7.11 / Q12 the dispatch shell borrow
616 // contract).
617 let arc = Arc::<HeapValue>::from_raw(callee_bits as *const HeapValue);
618 let extracted: Option<(u16, Vec<u64>)> = match &*arc {
619 HeapValue::ClosureRaw(block) => {
620 // §2.7.11/Q12: read the function_id from the
621 // TypedClosureHeader prefix at offset 8 (per
622 // `closure_raw.rs` `TypedClosureHeader` layout).
623 let fid = shape_value::v2::closure_raw::typed_closure_function_id(
624 block.as_ptr(),
625 );
626 let cap_count = block.layout().capture_count();
627 let mut caps: Vec<u64> = Vec::with_capacity(cap_count);
628 for idx in 0..cap_count {
629 // §2.7.8/Q10 read_capture_kinded returns
630 // `(bits, kind)`. `read_capture_kinded` is a
631 // RAW bit read — it does NOT bump the
632 // capture's refcount.
633 //
634 // γ-CP5 7b (jit-typedarray-ptr,
635 // v2-raw-heap-aliasing class): retain each
636 // heap-typed capture before handing it to
637 // `jit_trampoline_call_closure`. That
638 // trampoline builds a FRESH `OwnedClosureBlock`
639 // from these bits (`write_capture_raw_u64` —
640 // no bump) and the fresh block's `Drop`
641 // (`release_typed_closure`) WILL release each
642 // heap capture via the layout's capture masks.
643 // Its doc-comment states the contract
644 // explicitly: "the JIT pre-incremented each
645 // share before crossing the FFI boundary."
646 // Without this retain every closure call
647 // retires one share of each captured heap
648 // value — the original binding's + the
649 // closure block's shares are consumed within
650 // a few calls and the next access
651 // dereferences freed memory (`malloc():
652 // unaligned tcache chunk` SIGABRT).
653 //
654 // The retain is the §2.7.8/Q10 kind-driven
655 // bump via `KindedSlot::clone` — same dispatch
656 // table as the VM's per-capture
657 // `clone_with_kind` at frame setup
658 // (`executor/call_convention.rs:776`). Inline
659 // scalars are a no-op (`KindedSlot::clone`'s
660 // scalar arms). `mem::forget` hands the bumped
661 // share to the fresh block; the borrow-view
662 // `KindedSlot` must NOT run its `Drop` (that
663 // would cancel the bump) so it is forgotten
664 // too.
665 let (cap_bits, cap_kind) = block.read_capture_kinded(idx);
666 let borrow_view = shape_value::KindedSlot::new(
667 shape_value::ValueSlot::from_raw(cap_bits),
668 cap_kind,
669 );
670 let retained = borrow_view.clone();
671 std::mem::forget(borrow_view);
672 std::mem::forget(retained);
673 caps.push(cap_bits);
674 }
675 Some((fid, caps))
676 }
677 other => {
678 // Wrong HeapValue arm under the stamped kind —
679 // a producing-site bug, not a tag-decode gap.
680 // Surface with diagnostic.
681 tracing::debug!(
682 target: "shape_jit",
683 heap_kind = ?other.kind(),
684 "jit-call-value SURFACE \u{a7}2.7.6/Q8: callee \
685 stamped Ptr(HeapKind::Closure) but HeapValue \
686 arm is not ClosureRaw. Producing site \
687 mislabeled the slot kind.",
688 );
689 None
690 }
691 };
692 // Restore the `Arc` raw pointer — the slot share is
693 // still owned by whoever pushed it (the call signature
694 // borrow contract leaves the share with the producer).
695 let _ = Arc::into_raw(arc);
696 match extracted {
697 Some((f, c)) => {
698 function_id = f;
699 vm_captures = Some(c);
700 }
701 None => return TAG_NULL,
702 }
703 }
704 NativeKind::Ptr(HeapKind::ModuleFn) => {
705 // ModuleFn callees flow through the comptime dispatch —
706 // the §2.7.26 path. Not yet supported in the JIT-side
707 // value-call surface; the bytecode compiler shouldn't
708 // emit a top-level module-fn callee through this opcode
709 // at present. Surface.
710 tracing::debug!(
711 target: "shape_jit",
712 "jit-call-value SURFACE \u{a7}2.7.26: ModuleFn callee \
713 not implemented in jit_call_value.",
714 );
715 return TAG_NULL;
716 }
717 NativeKind::UInt64
718 | NativeKind::Int64
719 | NativeKind::IntSize
720 | NativeKind::UIntSize
721 | NativeKind::NullableUInt64
722 | NativeKind::NullableInt64
723 | NativeKind::NullableIntSize
724 | NativeKind::NullableUIntSize => {
725 // Generic I64-wide raw bits carrier kind (§2.7.5 / §2.7.11).
726 // The bits hold either (a) a NaN-boxed inline function
727 // ref (the JIT MIR emitter pushes `box_function(fn_id)`
728 // when the callee is a `FunctionRef` constant), or (b)
729 // a NaN-boxed `HK_CLOSURE` legacy unified-heap
730 // `JITClosure` allocation. The JIT-internal NaN-box
731 // predicates `is_inline_function` and
732 // `is_heap_kind(_, HK_CLOSURE)` are intentionally
733 // preserved here per ADR-006 §2.7.5 — they operate on
734 // the JIT's own value representation, NOT on the
735 // deleted runtime-tier `tag_bits` dispatch (CLAUDE.md
736 // "Forbidden Patterns" #4 enumerates the deleted runtime
737 // synthesizer / `is_tagged()` handlers; the JIT-internal
738 // NaN-box checks in `value_ffi.rs` are a different
739 // surface and remain valid).
740 if is_inline_function(callee_bits) {
741 function_id = unbox_function_id(callee_bits);
742 } else if is_heap_kind(callee_bits, HK_CLOSURE) {
743 let closure = unified_unbox::<JITClosure>(callee_bits);
744 function_id = closure.function_id;
745 let count = closure.captures_count as usize;
746 let mut caps: Vec<u64> = Vec::with_capacity(count);
747 for i in 0..count {
748 caps.push(*closure.captures_ptr.add(i));
749 }
750 vm_captures = Some(caps);
751 } else {
752 tracing::debug!(
753 target: "shape_jit",
754 callee_bits,
755 "jit-call-value SURFACE \u{a7}2.7.5: callee_bits \
756 stamped UInt64 but is neither inline function \
757 (TAG_FUNCTION) nor unified-heap HK_CLOSURE. \
758 Producing site stamped the carrier kind but \
759 emitted bits that don't match either UInt64-class \
760 shape.",
761 );
762 return TAG_NULL;
763 }
764 }
765 other => {
766 tracing::debug!(
767 target: "shape_jit",
768 kind = ?other,
769 "jit-call-value SURFACE \u{a7}2.7.11/Q12: callee kind \
770 is not a recognized callable kind. The \u{a7}2.7.11/Q12 \
771 callee-classification kinds at the indirect-call entry \
772 are Ptr(HeapKind::Closure) (raw-Arc closure shape), \
773 Ptr(HeapKind::ModuleFn) (deferred), and UInt64/Int64-\
774 family (function-id and JIT-internal NaN-box shapes).",
775 );
776 return TAG_NULL;
777 }
778 }
779
780 // Extract the raw arg bits for dispatch. Per-arg kinds are
781 // already paired into `arg_pairs` and consumed inside the
782 // trampoline VM as `KindedSlot` carriers (see
783 // `dispatch_call_via_trampoline_vm`); we keep raw bits here for
784 // the JIT function-table fast path which uses native Cranelift
785 // call signatures (uniformly I64) and has no kind dependency.
786 let args: Vec<u64> = arg_pairs.iter().map(|(b, _)| *b).collect();
787
788 // ── Dispatch ─────────────────────────────────────────────────────
789
790 // Try the JIT function table fast path first (no trampoline
791 // hop). Only the bare-function shape can use this path —
792 // closures need the trampoline VM for the captures-binding
793 // semantics.
794 if vm_captures.is_none()
795 && !ctx_ref.function_table.is_null()
796 && (function_id as usize) < ctx_ref.function_table_len
797 {
798 let raw_fn_ptr =
799 *(ctx_ref.function_table as *const *const u8).add(function_id as usize);
800 if !raw_fn_ptr.is_null() {
801 // Reset ctx.stack_ptr so the callee starts with a clean
802 // stack frame. The kind track is naturally re-initialized
803 // by the callee's own push sequence — the §2.7.7 / Q9
804 // lockstep invariant only constrains the live region of
805 // the stack (`stack[..stack_ptr]`), not the dead region
806 // beyond.
807 ctx_ref.stack_ptr = 0;
808 let _signal = call_jit_fn_with_args(raw_fn_ptr, ctx, &args);
809 // Result is on ctx.stack[0..sp]; pop the top slot.
810 if ctx_ref.stack_ptr > 0 {
811 ctx_ref.stack_ptr -= 1;
812 let ret_bits = ctx_ref.stack[ctx_ref.stack_ptr];
813 // Return-slot kind is consumed implicitly by the
814 // executor's RETURN_TAG_* dispatch (see
815 // `executor.rs::execute_with_jit`); we don't need
816 // to thread it back through `stack_kinds` because
817 // the calling MIR slot's kind is set by the
818 // destination write via `write_place`.
819 ctx_ref.stack_kinds[ctx_ref.stack_ptr] = stack_kind_code::SENTINEL;
820 return ret_bits;
821 }
822 return TAG_NULL;
823 }
824 }
825
826 // Fallback: route through the trampoline VM. This handles:
827 // - JIT-untranslated function bodies (null function-table entry).
828 // - HK_CLOSURE callees (captures threaded into the new frame).
829 // - Raw-Arc HeapKind::Closure callees (Case 3 closed via the
830 // §2.7.11/Q12 kind dispatch above).
831 let upvalues: Option<&[u64]> = vm_captures.as_deref();
832 dispatch_call_via_trampoline_vm(
833 function_id as u32,
834 upvalues,
835 &args,
836 ctx,
837 )
838 }
839}
840
841/// Call a JIT-compiled function pointer with the right number of native arguments.
842/// The function has Cranelift signature: fn(ctx_ptr: i64, arg0: i64, ...) -> i32
843///
844/// `pub(crate)` visibility: shared with `ffi/call_method/mod.rs::try_call_user_method`
845/// per W14.2-E-followup soundness fix (2026-05-19) — trait-method UFCS user-callee
846/// dispatch must invoke the JIT-compiled callee through the same native-ABI path
847/// as `jit_call_value`'s bare-function fast path (line ~717). The prior shape that
848/// called `fn_ptr(ctx)` directly under the `JittedStrategyFn` typedef silently
849/// dropped every receiver/arg slot since the callee's extended Cranelift signature
850/// `fn(ctx_ptr, arg0, ..., argN) -> i32` reads its params via System V register/
851/// stack convention, NOT from `ctx.stack`. Per ADR-006 §2.7.10/Q11 the dispatch
852/// shell sources every kind from the §2.7.7/Q9 parallel-kind track; the data half
853/// flows through this helper's typed-fn transmute selector.
854pub(crate) unsafe fn call_jit_fn_with_args(
855 fn_ptr: *const u8,
856 ctx: *mut JITContext,
857 args: &[u64],
858) -> i32 {
859 type F0 = unsafe extern "C" fn(*mut JITContext) -> i32;
860 type F1 = unsafe extern "C" fn(*mut JITContext, u64) -> i32;
861 type F2 = unsafe extern "C" fn(*mut JITContext, u64, u64) -> i32;
862 type F3 = unsafe extern "C" fn(*mut JITContext, u64, u64, u64) -> i32;
863 type F4 = unsafe extern "C" fn(*mut JITContext, u64, u64, u64, u64) -> i32;
864 type F5 = unsafe extern "C" fn(*mut JITContext, u64, u64, u64, u64, u64) -> i32;
865 type F6 = unsafe extern "C" fn(*mut JITContext, u64, u64, u64, u64, u64, u64) -> i32;
866 type F7 = unsafe extern "C" fn(*mut JITContext, u64, u64, u64, u64, u64, u64, u64) -> i32;
867 type F8 = unsafe extern "C" fn(*mut JITContext, u64, u64, u64, u64, u64, u64, u64, u64) -> i32;
868
869 let result = match args.len() {
870 0 => std::mem::transmute::<_, F0>(fn_ptr)(ctx),
871 1 => std::mem::transmute::<_, F1>(fn_ptr)(ctx, args[0]),
872 2 => std::mem::transmute::<_, F2>(fn_ptr)(ctx, args[0], args[1]),
873 3 => std::mem::transmute::<_, F3>(fn_ptr)(ctx, args[0], args[1], args[2]),
874 4 => std::mem::transmute::<_, F4>(fn_ptr)(ctx, args[0], args[1], args[2], args[3]),
875 5 => std::mem::transmute::<_, F5>(fn_ptr)(ctx, args[0], args[1], args[2], args[3], args[4]),
876 6 => std::mem::transmute::<_, F6>(fn_ptr)(ctx, args[0], args[1], args[2], args[3], args[4], args[5]),
877 7 => std::mem::transmute::<_, F7>(fn_ptr)(ctx, args[0], args[1], args[2], args[3], args[4], args[5], args[6]),
878 8 => std::mem::transmute::<_, F8>(fn_ptr)(ctx, args[0], args[1], args[2], args[3], args[4], args[5], args[6], args[7]),
879 _ => {
880 // Too many args for direct dispatch — fall back to trampoline
881 -1
882 }
883 };
884 result
885}
886
887/// fold(array, initial, fn) - left fold over array
888///
889/// SURFACE (W10 jit-playbook §5 / ADR-006 §2.7.4): walked the deleted
890/// `JitArray` heap layout (`from_heap_bits`). Kinded rebuild reads
891/// `Arc<TypedArrayData>` per-element-kind arm (§2.7.6/Q8) and threads
892/// the per-element kind into the callback dispatch per §2.7.5.
893pub extern "C" fn jit_control_fold(_ctx: *mut JITContext) -> u64 {
894 todo!(
895 "phase-2c §2.7.4 / W10 jit-playbook §5: JitArray rebuild — \
896 jit_control_fold. The deleted UnifiedArray-walk decoded element \
897 bits without per-element NativeKind tracking; the kinded rebuild \
898 reads Arc<TypedArrayData> per ADR-006 §2.7.6/Q8 and dispatches \
899 the callback through the §2.7.10/Q11 kinded handler ABI."
900 )
901}
902
903/// reduce(array, fn, initial) - reduce array to single value
904pub extern "C" fn jit_control_reduce(ctx: *mut JITContext) -> u64 {
905 // reduce is the same as fold
906 jit_control_fold(ctx)
907}
908
909/// map(array, fn) - transform each element
910///
911/// SURFACE (W10 jit-playbook §5 / ADR-006 §2.7.4): same JitArray
912/// deletion as `jit_control_fold` plus the result allocation goes
913/// through the deleted `JitArray::from_vec(...).heap_box()`. Kinded
914/// rebuild allocates a `TypedArray<T>` for the inferred element kind.
915pub extern "C" fn jit_control_map(_ctx: *mut JITContext) -> u64 {
916 todo!(
917 "phase-2c §2.7.4 / W10 jit-playbook §5: JitArray rebuild — \
918 jit_control_map. Receiver decode + result allocation both \
919 block on the kinded TypedArray<T> rebuild per ADR-006 §2.7.6/Q8."
920 )
921}
922
923/// filter(array, predicate) - keep elements where predicate returns true
924pub extern "C" fn jit_control_filter(_ctx: *mut JITContext) -> u64 {
925 todo!(
926 "phase-2c §2.7.4 / W10 jit-playbook §5: JitArray rebuild — \
927 jit_control_filter. Same kinded-TypedArray<T> rebuild as \
928 jit_control_map."
929 )
930}
931
932/// forEach(array, fn, count) - execute fn for each element (side effects)
933pub extern "C" fn jit_control_foreach(_ctx: *mut JITContext, _count: usize) -> u64 {
934 todo!(
935 "phase-2c §2.7.4 / W10 jit-playbook §5: JitArray rebuild — \
936 jit_control_foreach. Same kinded-TypedArray<T> rebuild as \
937 jit_control_map."
938 )
939}
940
941/// find(array, predicate) - find first element matching predicate
942pub extern "C" fn jit_control_find(_ctx: *mut JITContext) -> u64 {
943 todo!(
944 "phase-2c §2.7.4 / W10 jit-playbook §5: JitArray rebuild — \
945 jit_control_find. Same kinded-TypedArray<T> rebuild as \
946 jit_control_map."
947 )
948}
949
950unsafe fn jit_callable_invoker(
951 _ctx: *mut c_void,
952 _callable: &u64,
953 _args: &[u64],
954) -> Result<u64, String> {
955 // Phase-2c §2.7.10/Q11 + §2.7.11/Q12: the kinded value-call ABI
956 // rebuild applies here too — the native-callback re-entry path
957 // pushes the callable + args back onto the JIT stack and dispatches
958 // through `jit_call_value`. Both ends are now kinded surfaces; the
959 // RawCallableInvoker signature must thread `KindedSlot` through
960 // once the kinded JIT-FFI consumer waves land. See
961 // docs/cluster-audits/wave-10-jit-playbook.md §5.
962 Err(
963 "phase-2c §2.7.10/Q11: jit_callable_invoker is a kinded-ABI \
964 surface awaiting the value-call kind-companion lowering"
965 .to_string(),
966 )
967}
968
969/// Invoke a linked foreign function from JIT code.
970///
971/// Args are read from `ctx.stack` (already materialized by lowering):
972/// `[... arg0, arg1, ..., argN-1]` with `arg_count` provided out-of-band.
973enum ForeignInvokeMode {
974 Any,
975 NativeOnly,
976 DynamicOnly,
977}
978
979unsafe fn jit_call_foreign_impl(
980 _ctx: *mut JITContext,
981 _foreign_idx: u32,
982 _arg_count: usize,
983 _mode: ForeignInvokeMode,
984) -> u64 {
985 todo!(
986 "phase-2c §2.7.10/Q11: JIT-side kinded foreign-call ABI rebuild — \
987 jit_call_foreign_impl. The foreign_bridge invoke / invoke_native / \
988 invoke_dynamic surfaces still take &[ValueWord]; once that crate's \
989 own kinded-ABI migration lands, args flow as &[KindedSlot] per \
990 ADR-006 §2.7.10/Q11 and the Err() arm constructs the Result::Err \
991 carrier through the kinded HeapKind::Err producer per §2.7.6/Q8. \
992 See docs/cluster-audits/wave-10-jit-playbook.md §5."
993 )
994}
995
996pub extern "C" fn jit_call_foreign(
997 ctx: *mut JITContext,
998 foreign_idx: u32,
999 arg_count: usize,
1000) -> u64 {
1001 unsafe { jit_call_foreign_impl(ctx, foreign_idx, arg_count, ForeignInvokeMode::Any) }
1002}
1003
1004pub extern "C" fn jit_call_foreign_native(
1005 ctx: *mut JITContext,
1006 foreign_idx: u32,
1007 arg_count: usize,
1008) -> u64 {
1009 unsafe { jit_call_foreign_impl(ctx, foreign_idx, arg_count, ForeignInvokeMode::NativeOnly) }
1010}
1011
1012pub extern "C" fn jit_call_foreign_dynamic(
1013 ctx: *mut JITContext,
1014 foreign_idx: u32,
1015 arg_count: usize,
1016) -> u64 {
1017 unsafe { jit_call_foreign_impl(ctx, foreign_idx, arg_count, ForeignInvokeMode::DynamicOnly) }
1018}
1019
1020unsafe fn jit_call_foreign_native_args_fixed<const N: usize>(
1021 _ctx: *mut JITContext,
1022 _foreign_idx: u32,
1023 _args: [u64; N],
1024) -> u64 {
1025 todo!(
1026 "phase-2c §2.7.10/Q11: JIT-side kinded foreign-call ABI rebuild — \
1027 jit_call_foreign_native_args_fixed<N>. Same gating as \
1028 jit_call_foreign_impl: foreign_bridge invoke_native still takes \
1029 &[ValueWord]; once that crate's own kinded-ABI migration lands, \
1030 the fixed-arity boxed_args array becomes [KindedSlot; N] per \
1031 ADR-006 §2.7.10/Q11. See \
1032 docs/cluster-audits/wave-10-jit-playbook.md §5."
1033 )
1034}
1035
1036macro_rules! define_jit_call_foreign_native_fixed {
1037 ($name:ident, [$($arg:ident),*]) => {
1038 pub extern "C" fn $name(
1039 ctx: *mut JITContext,
1040 foreign_idx: u32,
1041 $($arg: u64),*
1042 ) -> u64 {
1043 unsafe { jit_call_foreign_native_args_fixed(ctx, foreign_idx, [$($arg),*]) }
1044 }
1045 };
1046}
1047
1048define_jit_call_foreign_native_fixed!(jit_call_foreign_native_0, []);
1049define_jit_call_foreign_native_fixed!(jit_call_foreign_native_1, [arg0]);
1050define_jit_call_foreign_native_fixed!(jit_call_foreign_native_2, [arg0, arg1]);
1051define_jit_call_foreign_native_fixed!(jit_call_foreign_native_3, [arg0, arg1, arg2]);
1052define_jit_call_foreign_native_fixed!(jit_call_foreign_native_4, [arg0, arg1, arg2, arg3]);
1053define_jit_call_foreign_native_fixed!(jit_call_foreign_native_5, [arg0, arg1, arg2, arg3, arg4]);
1054define_jit_call_foreign_native_fixed!(
1055 jit_call_foreign_native_6,
1056 [arg0, arg1, arg2, arg3, arg4, arg5]
1057);
1058define_jit_call_foreign_native_fixed!(
1059 jit_call_foreign_native_7,
1060 [arg0, arg1, arg2, arg3, arg4, arg5, arg6]
1061);
1062define_jit_call_foreign_native_fixed!(
1063 jit_call_foreign_native_8,
1064 [arg0, arg1, arg2, arg3, arg4, arg5, arg6, arg7]
1065);
1066
1067/// Trampoline placeholder for mixed-table VM fallback paths.
1068///
1069/// When implemented, this will dispatch to the VM interpreter for functions
1070/// that weren't JIT-compiled. The return value from the VM is in ValueWord
1071/// format, so it must be converted to JIT format via `vm_result_to_jit`.
1072pub unsafe extern "C" fn jit_vm_fallback_trampoline(
1073 _ctx: *mut std::ffi::c_void,
1074 _function_id: u32,
1075 _args_ptr: *const u64,
1076 _args_len: u32,
1077) -> u64 {
1078 // TODO: when implemented, convert result via vm_result_to_jit():
1079 // let vm_result = /* dispatch to VM interpreter */;
1080 // crate::ffi::object::conversion::vm_result_to_jit(vm_result)
1081 TAG_NULL
1082}
1083
1084/// findIndex(array, predicate) - find index of first element matching predicate
1085pub extern "C" fn jit_control_find_index(_ctx: *mut JITContext) -> u64 {
1086 todo!(
1087 "phase-2c §2.7.4 / W10 jit-playbook §5: JitArray rebuild — \
1088 jit_control_find_index. Same kinded-TypedArray<T> rebuild as \
1089 jit_control_map."
1090 )
1091}
1092
1093/// some(array, predicate) - true if any element matches predicate
1094pub extern "C" fn jit_control_some(_ctx: *mut JITContext) -> u64 {
1095 todo!(
1096 "phase-2c §2.7.4 / W10 jit-playbook §5: JitArray rebuild — \
1097 jit_control_some. Same kinded-TypedArray<T> rebuild as \
1098 jit_control_map."
1099 )
1100}
1101
1102/// every(array, predicate) - true if all elements match predicate
1103pub extern "C" fn jit_control_every(_ctx: *mut JITContext) -> u64 {
1104 todo!(
1105 "phase-2c §2.7.4 / W10 jit-playbook §5: JitArray rebuild — \
1106 jit_control_every. Same kinded-TypedArray<T> rebuild as \
1107 jit_control_map."
1108 )
1109}
1110
1111#[cfg(test)]
1112mod tests {
1113 use super::*;
1114
1115 // jit_call_value_decodes_arg_count_as_raw_i64 — removed. The
1116 // function under test is now SURFACE per ADR-006 §2.7.11/Q12 (kinded
1117 // value-call ABI rebuild); the behavioural decode-arg_count
1118 // regression test belongs to the kinded ABI rebuild wave (W11 /
1119 // deeper Phase-2c) where the call signature exposes the kind
1120 // companion explicitly.
1121
1122 // ── r5c-2-bz-b-jit-err-surface: VM-trampoline error channel ─────────
1123
1124 /// `set_jit_runtime_error` then `take_jit_runtime_error` round-trips the
1125 /// message exactly once; the take clears it so a later, unrelated JIT
1126 /// execution on the same thread does not inherit a stale error.
1127 #[test]
1128 fn jit_runtime_error_channel_round_trips_and_clears() {
1129 // Clear any residue from a prior test on this thread.
1130 let _ = take_jit_runtime_error();
1131 assert_eq!(take_jit_runtime_error(), None);
1132
1133 set_jit_runtime_error("Set.add(): key must be a string".to_string());
1134 assert_eq!(
1135 take_jit_runtime_error().as_deref(),
1136 Some("Set.add(): key must be a string"),
1137 );
1138 // The take cleared it — a second take sees None.
1139 assert_eq!(take_jit_runtime_error(), None);
1140 }
1141
1142 /// The most recent `set_jit_runtime_error` wins — the message that
1143 /// triggers the deopt is the one surfaced.
1144 #[test]
1145 fn jit_runtime_error_channel_keeps_most_recent() {
1146 let _ = take_jit_runtime_error();
1147 set_jit_runtime_error("first error".to_string());
1148 set_jit_runtime_error("second error".to_string());
1149 assert_eq!(take_jit_runtime_error().as_deref(), Some("second error"));
1150 let _ = take_jit_runtime_error();
1151 }
1152
1153 #[test]
1154 #[ignore = "SURFACE: jit_call_foreign_native_0 is extern \"C\" todo!() pending kinded foreign-call ABI rebuild (ADR-006 §2.7.10/Q11, docs/cluster-audits/wave-10-jit-playbook.md §5); extern C can't unwind, so #[should_panic] aborts the test process. Re-enable via `cargo test -- --ignored` once the underlying SURFACE closes."]
1155 fn native_fixed_arity_helpers_surface_pending_kinded_abi() {
1156 // SURFACE: jit_call_foreign_native_args_fixed routes to todo!()
1157 // pending the kinded foreign-call ABI rebuild (§2.7.10/Q11).
1158 // Can't use #[should_panic] on extern "C" functions: Rust 1.93+
1159 // aborts the process (SIGABRT) on a non-unwinding panic instead of
1160 // reporting a clean test failure. Same constraint as
1161 // ffi/v2/mod.rs:1060 `test_array_get_oob_returns_none_via_typed_array`.
1162 let _ = jit_call_foreign_native_0(std::ptr::null_mut(), 0);
1163 }
1164
1165 // Suppress the unused-helpers lint for the moved `native_fixed_arity_helpers_return_null_for_null_context`.
1166 #[allow(dead_code)]
1167 fn native_fixed_arity_helpers_return_null_for_null_context() {
1168 assert_eq!(jit_call_foreign_native_0(std::ptr::null_mut(), 0), TAG_NULL);
1169 assert_eq!(
1170 jit_call_foreign_native_1(std::ptr::null_mut(), 0, TAG_NULL),
1171 TAG_NULL
1172 );
1173 assert_eq!(
1174 jit_call_foreign_native_2(std::ptr::null_mut(), 0, TAG_NULL, TAG_NULL),
1175 TAG_NULL
1176 );
1177 assert_eq!(
1178 jit_call_foreign_native_3(std::ptr::null_mut(), 0, TAG_NULL, TAG_NULL, TAG_NULL),
1179 TAG_NULL
1180 );
1181 assert_eq!(
1182 jit_call_foreign_native_4(
1183 std::ptr::null_mut(),
1184 0,
1185 TAG_NULL,
1186 TAG_NULL,
1187 TAG_NULL,
1188 TAG_NULL
1189 ),
1190 TAG_NULL
1191 );
1192 assert_eq!(
1193 jit_call_foreign_native_5(
1194 std::ptr::null_mut(),
1195 0,
1196 TAG_NULL,
1197 TAG_NULL,
1198 TAG_NULL,
1199 TAG_NULL,
1200 TAG_NULL
1201 ),
1202 TAG_NULL
1203 );
1204 assert_eq!(
1205 jit_call_foreign_native_6(
1206 std::ptr::null_mut(),
1207 0,
1208 TAG_NULL,
1209 TAG_NULL,
1210 TAG_NULL,
1211 TAG_NULL,
1212 TAG_NULL,
1213 TAG_NULL
1214 ),
1215 TAG_NULL
1216 );
1217 assert_eq!(
1218 jit_call_foreign_native_7(
1219 std::ptr::null_mut(),
1220 0,
1221 TAG_NULL,
1222 TAG_NULL,
1223 TAG_NULL,
1224 TAG_NULL,
1225 TAG_NULL,
1226 TAG_NULL,
1227 TAG_NULL
1228 ),
1229 TAG_NULL
1230 );
1231 assert_eq!(
1232 jit_call_foreign_native_8(
1233 std::ptr::null_mut(),
1234 0,
1235 TAG_NULL,
1236 TAG_NULL,
1237 TAG_NULL,
1238 TAG_NULL,
1239 TAG_NULL,
1240 TAG_NULL,
1241 TAG_NULL,
1242 TAG_NULL
1243 ),
1244 TAG_NULL
1245 );
1246 }
1247}