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}