shape-jit 0.3.0

Tiered JIT compiler (Cranelift) for the Shape virtual machine
Documentation
// Heap allocation audit (PR-9 V8 Gap Closure):
//   Category A (NaN-boxed returns): 0 sites
//   Category B (intermediate/consumed): 1 site
//     Vec::with_capacity in jit_join_init — consumed by ValueWord::from_heap_value
//   Category C (heap islands): 0 sites
//     (async ops use trampoline dispatch and ValueWord conversion, no raw JitAlloc)
//!
//! FFI Trampolines for Async Task Operations
//!
//! These extern "C" functions are called from JIT-compiled code to
//! interact with the async task scheduler (spawn, join, cancel, scopes).
//!
//! v2-boundary: All functions in this module are called from
//! translator/opcodes/async_ops.rs and registered in ffi_symbols/async_symbols.rs.
//!
//! Deleted (no callers from MirToIR):
//!   - Simulation event scheduling (__shape_schedule_event, EventQueueOpaque, etc.)
//!   - Cooperative yield/suspend (__shape_should_yield, __shape_yield, __shape_suspend, etc.)
//!   - Event queue polling (__shape_poll_event, __shape_emit_event, __shape_emit_alert)
//!   - Suspension state inspection (__shape_get_suspension_state, __shape_set_yield_threshold)

use super::super::context::JITContext;
// shape_value::ValueWord / ValueWordExt removed; the jit_join_init body
// constructed a `ValueWord::from_heap_value(HeapValue::TaskGroup{...})`
// which goes through the deleted kind-blind constructor. Per ADR-006
// §2.7.4 / §2.7.5 the rebuild target is a `KindedSlot` whose `kind =
// NativeKind::Ptr(HeapKind::TaskGroup)` plus a `ValueSlot` carrying
// `Arc::into_raw(Arc<TaskGroupData>) as u64` directly — no `ValueWord`
// constructor, no `as_future` decode of stack bits. The slot-side
// rebuild requires `from_taskgroup` (KindedSlot constructor — already
// landed for several heap arms) plus a JIT FFI shim that hands the
// kind back to the caller. That work is W11 / Phase-2c.

// ============================================================================
// Async Task Scheduling FFI (SpawnTask / JoinInit / JoinAwait / CancelTask)
// ============================================================================
//
// Design: These FFI functions use static atomic function pointers (trampoline
// pattern) to bridge from JIT-compiled code to the interpreter's task
// scheduler. The runtime registers the trampolines before JIT execution
// and clears them afterwards. This avoids cross-crate visibility issues
// (task_scheduler lives in shape-vm, which is a different crate).

/// Suspension state for async-wait (JoinAwait returns this to signal the JIT
/// execution loop should hand control back to the interpreter).
pub const SUSPENSION_ASYNC_WAIT: u32 = 3;

// ---- Static trampoline function pointers ----

/// Spawn trampoline: `fn(callable_bits: u64) -> u64` (returns Future bits)
pub static SPAWN_TASK_FN: std::sync::atomic::AtomicPtr<()> =
    std::sync::atomic::AtomicPtr::new(std::ptr::null_mut());

/// Cancel trampoline: `fn(future_bits: u64)`
pub static CANCEL_TASK_FN: std::sync::atomic::AtomicPtr<()> =
    std::sync::atomic::AtomicPtr::new(std::ptr::null_mut());

/// Async scope enter trampoline: `fn()`
pub static ASYNC_SCOPE_ENTER_FN: std::sync::atomic::AtomicPtr<()> =
    std::sync::atomic::AtomicPtr::new(std::ptr::null_mut());

/// Async scope exit trampoline: `fn()`
pub static ASYNC_SCOPE_EXIT_FN: std::sync::atomic::AtomicPtr<()> =
    std::sync::atomic::AtomicPtr::new(std::ptr::null_mut());

/// Register all async task trampolines.
///
/// # Safety
/// The function pointers must be valid for the duration of JIT execution.
pub unsafe fn register_async_task_fns(
    spawn: *mut (),
    cancel: *mut (),
    scope_enter: *mut (),
    scope_exit: *mut (),
) {
    SPAWN_TASK_FN.store(spawn, std::sync::atomic::Ordering::Release);
    CANCEL_TASK_FN.store(cancel, std::sync::atomic::Ordering::Release);
    ASYNC_SCOPE_ENTER_FN.store(scope_enter, std::sync::atomic::Ordering::Release);
    ASYNC_SCOPE_EXIT_FN.store(scope_exit, std::sync::atomic::Ordering::Release);
}

/// Clear all async task trampoline registrations.
pub fn unregister_async_task_fns() {
    SPAWN_TASK_FN.store(std::ptr::null_mut(), std::sync::atomic::Ordering::Release);
    CANCEL_TASK_FN.store(std::ptr::null_mut(), std::sync::atomic::Ordering::Release);
    ASYNC_SCOPE_ENTER_FN.store(std::ptr::null_mut(), std::sync::atomic::Ordering::Release);
    ASYNC_SCOPE_EXIT_FN.store(std::ptr::null_mut(), std::sync::atomic::Ordering::Release);
}

/// Spawn a new async task.
///
/// Delegates to the registered trampoline which has access to the VM's task
/// scheduler.
///
/// # Arguments
/// * `ctx` - JIT execution context (unused directly, but kept for ABI consistency)
/// * `callable_bits` - NaN-boxed callable value (function or closure)
///
/// # Returns
/// NaN-boxed Future(task_id) on success, TAG_NULL if no trampoline registered.
#[unsafe(no_mangle)]
pub extern "C" fn jit_spawn_task(_ctx: *mut JITContext, callable_bits: u64) -> u64 {
    let f = SPAWN_TASK_FN.load(std::sync::atomic::Ordering::Acquire);
    if f.is_null() {
        return crate::ffi::value_ffi::TAG_NULL;
    }
    let spawn: fn(u64) -> u64 = unsafe { std::mem::transmute(f) };
    spawn(callable_bits)
}

/// Initialize a join group from task futures.
///
/// Collects `arity` Future values from the JIT stack into a TaskGroup.
///
/// # Arguments
/// * `ctx` - JIT execution context
/// * `packed` - High 2 bits = join kind (all/race/any/settle), low 14 bits = arity
///
/// # Returns
/// NaN-boxed TaskGroup value, or TAG_NULL on failure.
#[unsafe(no_mangle)]
pub extern "C" fn jit_join_init(ctx: *mut JITContext, packed: u16) -> u64 {
    if ctx.is_null() {
        return crate::ffi::value_ffi::TAG_NULL;
    }

    let ctx = unsafe { &mut *ctx };

    let kind = ((packed >> 14) & 0x03) as u8;
    let arity = (packed & 0x3FFF) as usize;

    // PHASE_2C / SURFACE (ADR-006 §2.7.4 / §2.7.5): pre-strict-typing
    // the body popped `arity` Future bits from the JIT stack, decoded
    // each via `ValueWord::as_future()` (a `tag_bits` decode hidden
    // inside the deleted `ValueWord` API), constructed a
    // `HeapValue::TaskGroup{kind, task_ids}`, wrapped via
    // `ValueWord::from_heap_value` (deleted kind-blind constructor),
    // and re-encoded the result via `nanboxed_to_jit_bits`. The W-series
    // defection-attractor list forbids both ends of that pipeline.
    //
    // Strict-typing rebuild target: pop `arity` `KindedSlot` from a
    // §2.7.7 parallel `(stack: &mut [u64], kinds: &mut [NativeKind])`
    // pair, dispatch on each `kind == NativeKind::Future` to extract
    // the task id, build `Arc<TaskGroupData>`, push back through the
    // same parallel pair with `kind = NativeKind::Ptr(HeapKind::TaskGroup)`.
    // Requires the JIT-FFI parallel-kind track threading (W11 / deeper
    // Phase-2c) plus the §2.7.10/Q11 dispatch-shell pattern at the JIT
    // FFI boundary.
    let _ = ctx;
    let _ = kind;
    let _ = arity;
    crate::ffi::value_ffi::TAG_NULL
}

/// Await a task group, suspending JIT execution.
///
/// Sets suspension_state = SUSPENSION_ASYNC_WAIT to signal the JIT execution
/// loop should exit and hand control back to the interpreter. The task group
/// value is left on the JIT stack for the interpreter to pick up.
///
/// # Arguments
/// * `ctx` - JIT execution context
/// * `task_group_bits` - NaN-boxed TaskGroup value
///
/// # Returns
/// TAG_NULL (caller checks suspension_state to detect suspension).
#[unsafe(no_mangle)]
pub extern "C" fn jit_join_await(ctx: *mut JITContext, task_group_bits: u64) -> u64 {
    if ctx.is_null() {
        return crate::ffi::value_ffi::TAG_NULL;
    }

    let ctx = unsafe { &mut *ctx };

    // Push the task group onto the JIT stack so the interpreter can pick it up
    // after the JIT function returns with the suspension signal.
    if ctx.stack_ptr < ctx.stack.len() {
        ctx.stack[ctx.stack_ptr] = task_group_bits;
        ctx.stack_ptr += 1;
    }

    // Signal suspension — the JIT execution loop checks this and exits
    ctx.suspension_state = SUSPENSION_ASYNC_WAIT;

    crate::ffi::value_ffi::TAG_NULL
}

/// Cancel a running task by its future ID.
///
/// # Arguments
/// * `ctx` - JIT execution context (unused directly)
/// * `future_bits` - NaN-boxed Future(task_id) value
///
/// # Returns
/// 0 on success, -1 on failure.
#[unsafe(no_mangle)]
pub extern "C" fn jit_cancel_task(_ctx: *mut JITContext, _future_bits: u64) -> i32 {
    // SURFACE (W10 jit-playbook §5 / ADR-006 §2.7.4 / §2.7.5):
    // pre-strict-typing this called `vw.as_future()` on the
    // ValueWord-shaped output of `jit_bits_to_nanboxed`, decoding
    // the future kind from the deleted ValueWord tag bits. The
    // §2.7.5 carrier returns a `(u64, NativeKind)` JitFfiCarrier,
    // not a ValueWord — `as_future()` no longer exists. Kinded
    // rebuild: dispatch on the carrier's `NativeKind ==
    // NativeKind::Ptr(HeapKind::Future)` arm (§2.7.6/Q8) with the
    // kind threaded from the JIT-emitted call signature per
    // §2.7.5; until the FFI signature widens to carry the kind
    // companion, surface-and-stop.
    todo!(
        "phase-2c §2.7.4/§2.7.5 / W10 jit-playbook §5: kinded \
         future-classification — jit_cancel_task. The deleted \
         ValueWord::as_future decode is gone; the JIT FFI \
         signature must widen to carry a NativeKind companion per \
         ADR-006 §2.7.5 / §2.7.6 / Q8."
    )
}

/// Enter an async scope (structured concurrency boundary).
///
/// Pushes a new empty task list onto the VM's async_scope_stack via trampoline.
///
/// # Returns
/// 0 on success, -1 if no trampoline registered.
#[unsafe(no_mangle)]
pub extern "C" fn jit_async_scope_enter(_ctx: *mut JITContext) -> i32 {
    let f = ASYNC_SCOPE_ENTER_FN.load(std::sync::atomic::Ordering::Acquire);
    if f.is_null() {
        return -1;
    }
    let enter: fn() = unsafe { std::mem::transmute(f) };
    enter();
    0
}

/// Exit an async scope (structured concurrency boundary).
///
/// Pops the current scope from the async_scope_stack and cancels all
/// tasks spawned within it that are still pending, in LIFO order.
///
/// # Returns
/// 0 on success, -1 if no trampoline registered.
#[unsafe(no_mangle)]
pub extern "C" fn jit_async_scope_exit(_ctx: *mut JITContext) -> i32 {
    let f = ASYNC_SCOPE_EXIT_FN.load(std::sync::atomic::Ordering::Acquire);
    if f.is_null() {
        return -1;
    }
    let exit: fn() = unsafe { std::mem::transmute(f) };
    exit();
    0
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn test_spawn_task_null_trampoline() {
        let mut ctx = JITContext::default();
        // No trampoline registered — should return TAG_NULL
        let result = jit_spawn_task(&mut ctx, 0);
        assert_eq!(result, crate::ffi::value_ffi::TAG_NULL);
    }

    // `test_join_init_empty` DELETED (W12-deleted-valuewordshape-tests-
    // rewrite, 2026-05-12). The test asserted `jit_join_init(ctx, 0) !=
    // TAG_NULL` — i.e., that initializing a join over an empty arity
    // produces a non-null TaskGroup reference. Under ADR-006 §2.7.4 the
    // `jit_join_init` body itself is a production-code SURFACE returning
    // TAG_NULL pending the kinded TaskGroup FFI rebuild (source-side
    // comment at `jit_join_init` body cites §2.7.4 / §2.7.5 + the
    // deleted ValueWord::from_heap_value / nanboxed_to_jit_bits pipeline
    // forbidden under the W-series defection-attractor list).
    //
    // The test premise cannot pass while the production-code SURFACE
    // remains. The strict-typed analog at the VM tier:
    // `KindedSlot::new(ValueSlot::from_raw(Arc::into_raw(Arc::new(
    // TaskGroupData { ... })) as u64), NativeKind::Ptr(HeapKind::TaskGroup))`
    // — but constructing a `TaskGroupData` directly in a JIT test
    // bypasses the actual function under test (`jit_join_init`) and would
    // give zero coverage of the FFI body. The principled response is to
    // leave the test deleted until the §2.7.5 kinded TaskGroup FFI rebuild
    // lands, then re-create it against the new function body in the same
    // sub-cluster that lands the rebuild.

    // test_join_await_sets_suspension removed: the test constructed a
    // TaskGroup via the deleted `ValueWord::from_heap_value` /
    // `nanboxed_to_jit_bits` pair (W-series defection-attractor
    // pipeline). It will be rewritten against the §2.7.5 / §2.7.7
    // parallel-kind JIT FFI shape once that lands (W11 / Phase-2c) —
    // see ADR-006 §2.7.4. Removing the test rather than fabricating
    // `(bits, NativeKind)` here avoids a Bool-default fallback for the
    // synthesized task-group payload.

    #[test]
    #[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`."]
    fn test_cancel_task_null_trampoline() {
        let mut ctx = JITContext::default();
        let result = jit_cancel_task(&mut ctx, 0);
        assert_eq!(result, -1); // No trampoline
    }

    #[test]
    fn test_async_scope_enter_null_trampoline() {
        let mut ctx = JITContext::default();
        let result = jit_async_scope_enter(&mut ctx);
        assert_eq!(result, -1); // No trampoline
    }

    #[test]
    fn test_async_scope_exit_null_trampoline() {
        let mut ctx = JITContext::default();
        let result = jit_async_scope_exit(&mut ctx);
        assert_eq!(result, -1); // No trampoline
    }
}