Skip to main content

shape_jit/ffi/
async_ops.rs

1// Heap allocation audit (PR-9 V8 Gap Closure):
2//   Category A (NaN-boxed returns): 0 sites
3//   Category B (intermediate/consumed): 1 site
4//     Vec::with_capacity in jit_join_init — consumed by ValueWord::from_heap_value
5//   Category C (heap islands): 0 sites
6//     (async ops use trampoline dispatch and ValueWord conversion, no raw JitAlloc)
7//!
8//! FFI Trampolines for Async Task Operations
9//!
10//! These extern "C" functions are called from JIT-compiled code to
11//! interact with the async task scheduler (spawn, join, cancel, scopes).
12//!
13//! v2-boundary: All functions in this module are called from
14//! translator/opcodes/async_ops.rs and registered in ffi_symbols/async_symbols.rs.
15//!
16//! Deleted (no callers from MirToIR):
17//!   - Simulation event scheduling (__shape_schedule_event, EventQueueOpaque, etc.)
18//!   - Cooperative yield/suspend (__shape_should_yield, __shape_yield, __shape_suspend, etc.)
19//!   - Event queue polling (__shape_poll_event, __shape_emit_event, __shape_emit_alert)
20//!   - Suspension state inspection (__shape_get_suspension_state, __shape_set_yield_threshold)
21
22use super::super::context::JITContext;
23// shape_value::ValueWord / ValueWordExt removed; the jit_join_init body
24// constructed a `ValueWord::from_heap_value(HeapValue::TaskGroup{...})`
25// which goes through the deleted kind-blind constructor. Per ADR-006
26// §2.7.4 / §2.7.5 the rebuild target is a `KindedSlot` whose `kind =
27// NativeKind::Ptr(HeapKind::TaskGroup)` plus a `ValueSlot` carrying
28// `Arc::into_raw(Arc<TaskGroupData>) as u64` directly — no `ValueWord`
29// constructor, no `as_future` decode of stack bits. The slot-side
30// rebuild requires `from_taskgroup` (KindedSlot constructor — already
31// landed for several heap arms) plus a JIT FFI shim that hands the
32// kind back to the caller. That work is W11 / Phase-2c.
33
34// ============================================================================
35// Async Task Scheduling FFI (SpawnTask / JoinInit / JoinAwait / CancelTask)
36// ============================================================================
37//
38// Design: These FFI functions use static atomic function pointers (trampoline
39// pattern) to bridge from JIT-compiled code to the interpreter's task
40// scheduler. The runtime registers the trampolines before JIT execution
41// and clears them afterwards. This avoids cross-crate visibility issues
42// (task_scheduler lives in shape-vm, which is a different crate).
43
44/// Suspension state for async-wait (JoinAwait returns this to signal the JIT
45/// execution loop should hand control back to the interpreter).
46pub const SUSPENSION_ASYNC_WAIT: u32 = 3;
47
48// ---- Static trampoline function pointers ----
49
50/// Spawn trampoline: `fn(callable_bits: u64) -> u64` (returns Future bits)
51pub static SPAWN_TASK_FN: std::sync::atomic::AtomicPtr<()> =
52    std::sync::atomic::AtomicPtr::new(std::ptr::null_mut());
53
54/// Cancel trampoline: `fn(future_bits: u64)`
55pub static CANCEL_TASK_FN: std::sync::atomic::AtomicPtr<()> =
56    std::sync::atomic::AtomicPtr::new(std::ptr::null_mut());
57
58/// Async scope enter trampoline: `fn()`
59pub static ASYNC_SCOPE_ENTER_FN: std::sync::atomic::AtomicPtr<()> =
60    std::sync::atomic::AtomicPtr::new(std::ptr::null_mut());
61
62/// Async scope exit trampoline: `fn()`
63pub static ASYNC_SCOPE_EXIT_FN: std::sync::atomic::AtomicPtr<()> =
64    std::sync::atomic::AtomicPtr::new(std::ptr::null_mut());
65
66/// Register all async task trampolines.
67///
68/// # Safety
69/// The function pointers must be valid for the duration of JIT execution.
70pub unsafe fn register_async_task_fns(
71    spawn: *mut (),
72    cancel: *mut (),
73    scope_enter: *mut (),
74    scope_exit: *mut (),
75) {
76    SPAWN_TASK_FN.store(spawn, std::sync::atomic::Ordering::Release);
77    CANCEL_TASK_FN.store(cancel, std::sync::atomic::Ordering::Release);
78    ASYNC_SCOPE_ENTER_FN.store(scope_enter, std::sync::atomic::Ordering::Release);
79    ASYNC_SCOPE_EXIT_FN.store(scope_exit, std::sync::atomic::Ordering::Release);
80}
81
82/// Clear all async task trampoline registrations.
83pub fn unregister_async_task_fns() {
84    SPAWN_TASK_FN.store(std::ptr::null_mut(), std::sync::atomic::Ordering::Release);
85    CANCEL_TASK_FN.store(std::ptr::null_mut(), std::sync::atomic::Ordering::Release);
86    ASYNC_SCOPE_ENTER_FN.store(std::ptr::null_mut(), std::sync::atomic::Ordering::Release);
87    ASYNC_SCOPE_EXIT_FN.store(std::ptr::null_mut(), std::sync::atomic::Ordering::Release);
88}
89
90/// Spawn a new async task.
91///
92/// Delegates to the registered trampoline which has access to the VM's task
93/// scheduler.
94///
95/// # Arguments
96/// * `ctx` - JIT execution context (unused directly, but kept for ABI consistency)
97/// * `callable_bits` - NaN-boxed callable value (function or closure)
98///
99/// # Returns
100/// NaN-boxed Future(task_id) on success, TAG_NULL if no trampoline registered.
101#[unsafe(no_mangle)]
102pub extern "C" fn jit_spawn_task(_ctx: *mut JITContext, callable_bits: u64) -> u64 {
103    let f = SPAWN_TASK_FN.load(std::sync::atomic::Ordering::Acquire);
104    if f.is_null() {
105        return crate::ffi::value_ffi::TAG_NULL;
106    }
107    let spawn: fn(u64) -> u64 = unsafe { std::mem::transmute(f) };
108    spawn(callable_bits)
109}
110
111/// Initialize a join group from task futures.
112///
113/// Collects `arity` Future values from the JIT stack into a TaskGroup.
114///
115/// # Arguments
116/// * `ctx` - JIT execution context
117/// * `packed` - High 2 bits = join kind (all/race/any/settle), low 14 bits = arity
118///
119/// # Returns
120/// NaN-boxed TaskGroup value, or TAG_NULL on failure.
121#[unsafe(no_mangle)]
122pub extern "C" fn jit_join_init(ctx: *mut JITContext, packed: u16) -> u64 {
123    if ctx.is_null() {
124        return crate::ffi::value_ffi::TAG_NULL;
125    }
126
127    let ctx = unsafe { &mut *ctx };
128
129    let kind = ((packed >> 14) & 0x03) as u8;
130    let arity = (packed & 0x3FFF) as usize;
131
132    // PHASE_2C / SURFACE (ADR-006 §2.7.4 / §2.7.5): pre-strict-typing
133    // the body popped `arity` Future bits from the JIT stack, decoded
134    // each via `ValueWord::as_future()` (a `tag_bits` decode hidden
135    // inside the deleted `ValueWord` API), constructed a
136    // `HeapValue::TaskGroup{kind, task_ids}`, wrapped via
137    // `ValueWord::from_heap_value` (deleted kind-blind constructor),
138    // and re-encoded the result via `nanboxed_to_jit_bits`. The W-series
139    // defection-attractor list forbids both ends of that pipeline.
140    //
141    // Strict-typing rebuild target: pop `arity` `KindedSlot` from a
142    // §2.7.7 parallel `(stack: &mut [u64], kinds: &mut [NativeKind])`
143    // pair, dispatch on each `kind == NativeKind::Future` to extract
144    // the task id, build `Arc<TaskGroupData>`, push back through the
145    // same parallel pair with `kind = NativeKind::Ptr(HeapKind::TaskGroup)`.
146    // Requires the JIT-FFI parallel-kind track threading (W11 / deeper
147    // Phase-2c) plus the §2.7.10/Q11 dispatch-shell pattern at the JIT
148    // FFI boundary.
149    let _ = ctx;
150    let _ = kind;
151    let _ = arity;
152    crate::ffi::value_ffi::TAG_NULL
153}
154
155/// Await a task group, suspending JIT execution.
156///
157/// Sets suspension_state = SUSPENSION_ASYNC_WAIT to signal the JIT execution
158/// loop should exit and hand control back to the interpreter. The task group
159/// value is left on the JIT stack for the interpreter to pick up.
160///
161/// # Arguments
162/// * `ctx` - JIT execution context
163/// * `task_group_bits` - NaN-boxed TaskGroup value
164///
165/// # Returns
166/// TAG_NULL (caller checks suspension_state to detect suspension).
167#[unsafe(no_mangle)]
168pub extern "C" fn jit_join_await(ctx: *mut JITContext, task_group_bits: u64) -> u64 {
169    if ctx.is_null() {
170        return crate::ffi::value_ffi::TAG_NULL;
171    }
172
173    let ctx = unsafe { &mut *ctx };
174
175    // Push the task group onto the JIT stack so the interpreter can pick it up
176    // after the JIT function returns with the suspension signal.
177    if ctx.stack_ptr < ctx.stack.len() {
178        ctx.stack[ctx.stack_ptr] = task_group_bits;
179        ctx.stack_ptr += 1;
180    }
181
182    // Signal suspension — the JIT execution loop checks this and exits
183    ctx.suspension_state = SUSPENSION_ASYNC_WAIT;
184
185    crate::ffi::value_ffi::TAG_NULL
186}
187
188/// Cancel a running task by its future ID.
189///
190/// # Arguments
191/// * `ctx` - JIT execution context (unused directly)
192/// * `future_bits` - NaN-boxed Future(task_id) value
193///
194/// # Returns
195/// 0 on success, -1 on failure.
196#[unsafe(no_mangle)]
197pub extern "C" fn jit_cancel_task(_ctx: *mut JITContext, _future_bits: u64) -> i32 {
198    // SURFACE (W10 jit-playbook §5 / ADR-006 §2.7.4 / §2.7.5):
199    // pre-strict-typing this called `vw.as_future()` on the
200    // ValueWord-shaped output of `jit_bits_to_nanboxed`, decoding
201    // the future kind from the deleted ValueWord tag bits. The
202    // §2.7.5 carrier returns a `(u64, NativeKind)` JitFfiCarrier,
203    // not a ValueWord — `as_future()` no longer exists. Kinded
204    // rebuild: dispatch on the carrier's `NativeKind ==
205    // NativeKind::Ptr(HeapKind::Future)` arm (§2.7.6/Q8) with the
206    // kind threaded from the JIT-emitted call signature per
207    // §2.7.5; until the FFI signature widens to carry the kind
208    // companion, surface-and-stop.
209    todo!(
210        "phase-2c §2.7.4/§2.7.5 / W10 jit-playbook §5: kinded \
211         future-classification — jit_cancel_task. The deleted \
212         ValueWord::as_future decode is gone; the JIT FFI \
213         signature must widen to carry a NativeKind companion per \
214         ADR-006 §2.7.5 / §2.7.6 / Q8."
215    )
216}
217
218/// Enter an async scope (structured concurrency boundary).
219///
220/// Pushes a new empty task list onto the VM's async_scope_stack via trampoline.
221///
222/// # Returns
223/// 0 on success, -1 if no trampoline registered.
224#[unsafe(no_mangle)]
225pub extern "C" fn jit_async_scope_enter(_ctx: *mut JITContext) -> i32 {
226    let f = ASYNC_SCOPE_ENTER_FN.load(std::sync::atomic::Ordering::Acquire);
227    if f.is_null() {
228        return -1;
229    }
230    let enter: fn() = unsafe { std::mem::transmute(f) };
231    enter();
232    0
233}
234
235/// Exit an async scope (structured concurrency boundary).
236///
237/// Pops the current scope from the async_scope_stack and cancels all
238/// tasks spawned within it that are still pending, in LIFO order.
239///
240/// # Returns
241/// 0 on success, -1 if no trampoline registered.
242#[unsafe(no_mangle)]
243pub extern "C" fn jit_async_scope_exit(_ctx: *mut JITContext) -> i32 {
244    let f = ASYNC_SCOPE_EXIT_FN.load(std::sync::atomic::Ordering::Acquire);
245    if f.is_null() {
246        return -1;
247    }
248    let exit: fn() = unsafe { std::mem::transmute(f) };
249    exit();
250    0
251}
252
253#[cfg(test)]
254mod tests {
255    use super::*;
256
257    #[test]
258    fn test_spawn_task_null_trampoline() {
259        let mut ctx = JITContext::default();
260        // No trampoline registered — should return TAG_NULL
261        let result = jit_spawn_task(&mut ctx, 0);
262        assert_eq!(result, crate::ffi::value_ffi::TAG_NULL);
263    }
264
265    // `test_join_init_empty` DELETED (W12-deleted-valuewordshape-tests-
266    // rewrite, 2026-05-12). The test asserted `jit_join_init(ctx, 0) !=
267    // TAG_NULL` — i.e., that initializing a join over an empty arity
268    // produces a non-null TaskGroup reference. Under ADR-006 §2.7.4 the
269    // `jit_join_init` body itself is a production-code SURFACE returning
270    // TAG_NULL pending the kinded TaskGroup FFI rebuild (source-side
271    // comment at `jit_join_init` body cites §2.7.4 / §2.7.5 + the
272    // deleted ValueWord::from_heap_value / nanboxed_to_jit_bits pipeline
273    // forbidden under the W-series defection-attractor list).
274    //
275    // The test premise cannot pass while the production-code SURFACE
276    // remains. The strict-typed analog at the VM tier:
277    // `KindedSlot::new(ValueSlot::from_raw(Arc::into_raw(Arc::new(
278    // TaskGroupData { ... })) as u64), NativeKind::Ptr(HeapKind::TaskGroup))`
279    // — but constructing a `TaskGroupData` directly in a JIT test
280    // bypasses the actual function under test (`jit_join_init`) and would
281    // give zero coverage of the FFI body. The principled response is to
282    // leave the test deleted until the §2.7.5 kinded TaskGroup FFI rebuild
283    // lands, then re-create it against the new function body in the same
284    // sub-cluster that lands the rebuild.
285
286    // test_join_await_sets_suspension removed: the test constructed a
287    // TaskGroup via the deleted `ValueWord::from_heap_value` /
288    // `nanboxed_to_jit_bits` pair (W-series defection-attractor
289    // pipeline). It will be rewritten against the §2.7.5 / §2.7.7
290    // parallel-kind JIT FFI shape once that lands (W11 / Phase-2c) —
291    // see ADR-006 §2.7.4. Removing the test rather than fabricating
292    // `(bits, NativeKind)` here avoids a Bool-default fallback for the
293    // synthesized task-group payload.
294
295    #[test]
296    #[ignore = "SURFACE: jit_cancel_task is extern \"C\" todo!() pending kinded future-classification (ADR-006 §2.7.4/§2.7.5, W10 jit-playbook §5); extern C can't unwind, so the todo!() body aborts the test process (SIGABRT) before the null-trampoline branch ever runs. Pre-strict-typing this test exercised the early `vw.as_future()` decode of a TAG_NULL future_bits=0, but the unconditional todo!() at the top of jit_cancel_task makes that branch unreachable. Re-enable via `cargo test -- --ignored` once the underlying SURFACE closes. Same constraint as ffi/control/mod.rs `native_fixed_arity_helpers_surface_pending_kinded_abi`."]
297    fn test_cancel_task_null_trampoline() {
298        let mut ctx = JITContext::default();
299        let result = jit_cancel_task(&mut ctx, 0);
300        assert_eq!(result, -1); // No trampoline
301    }
302
303    #[test]
304    fn test_async_scope_enter_null_trampoline() {
305        let mut ctx = JITContext::default();
306        let result = jit_async_scope_enter(&mut ctx);
307        assert_eq!(result, -1); // No trampoline
308    }
309
310    #[test]
311    fn test_async_scope_exit_null_trampoline() {
312        let mut ctx = JITContext::default();
313        let result = jit_async_scope_exit(&mut ctx);
314        assert_eq!(result, -1); // No trampoline
315    }
316}