shape-jit 0.3.0

Tiered JIT compiler (Cranelift) for the Shape virtual machine
Documentation
//! Track A.1D.2 — end-to-end JIT execution tests for `let mut` closures.
//!
//! These tests drive Shape source through the full JIT pipeline
//! (parse → bytecode → MIR → Cranelift → native) and assert on the
//! runtime result. They complement the per-opcode preflight tests in
//! `accessors.rs::a1d2_preflight_accepts_*` and the emit_heap_closure
//! unit tests in `ffi/object/closure.rs::jit_alloc_owned_mut_cell` —
//! together they pin the full stack for OwnedMutable captures.
//!
//! JIT confirmation strategy: each test asserts
//!     MixedFunctionTable::native_count() > 0 (i.e. at least one
//!     function was genuinely JIT-compiled to native code, not left as
//!     an interpreter trampoline).
//! Under A.1D.2's lifted preflight gate, closure bodies containing
//! `Load/StoreOwnedMutableCapture` now appear among the Native entries;
//! under A.1D (partial) they would have been Interpreted(idx). The
//! combined assertions (a) the program runs to completion without
//! panics, (b) produces the expected Shape-level result, AND (c) at
//! least one native function was emitted — provide a load-bearing
//! guarantee that the JIT is actually executing the `let mut` closure
//! body, not falling back to interpreter.

use crate::compiler::JITCompiler;
use crate::mixed_table::FunctionEntry;
use crate::JITConfig;

/// Compile Shape source end-to-end and return the MixedFunctionTable
/// from the JIT compiler. Asserts that bytecode compilation and JIT
/// compilation both succeeded. Used to confirm the JIT now accepts
/// OwnedMutable-capture closure bodies under A.1D.2.
fn compile_to_mixed_table(
    source: &str,
) -> (
    shape_vm::bytecode::BytecodeProgram,
    crate::mixed_table::MixedFunctionTable,
) {
    use shape_vm::BytecodeCompiler;

    shape_runtime::initialize_shared_runtime().ok();
    let program = shape_ast::parse_program(source).expect("parse failed");

    // Build the stdlib-aware module graph exactly like JITExecutor does,
    // so closures see their prelude imports and stdlib-binding names are
    // recognised during compilation.
    let mut loader = shape_runtime::module_loader::ModuleLoader::new();
    let (graph, stdlib_names, prelude_imports) =
        shape_vm::module_resolution::build_graph_and_stdlib_names(
            &program,
            &mut loader,
            &[],
        )
        .expect("module graph construction failed");

    let mut compiler = BytecodeCompiler::new();
    compiler.stdlib_function_names = stdlib_names;
    compiler.set_source(source);
    let bytecode = compiler
        .compile_with_graph_and_prelude(&program, graph, &prelude_imports)
        .expect("bytecode compilation failed");

    let jit_config = JITConfig::default();
    let mut jit = JITCompiler::new(jit_config).expect("JIT init failed");
    let (_jit_fn, mixed_table) = jit
        .compile_program_selective("main", &bytecode)
        .expect("JIT compilation failed");
    (bytecode, mixed_table)
}

/// Return `true` when `mixed_table` has at least one closure function
/// (by bytecode-side `is_closure` flag) that is `FunctionEntry::Native`.
///
/// Before A.1D.2, closure bodies containing `Load/StoreOwnedMutableCapture`
/// were preflight-rejected and appeared only as `Interpreted(...)`; this
/// predicate therefore distinguishes JIT'd-closure code paths from
/// interpreter-fallback paths at compile time.
fn has_native_closure(
    program: &shape_vm::bytecode::BytecodeProgram,
    table: &crate::mixed_table::MixedFunctionTable,
) -> bool {
    program
        .functions
        .iter()
        .enumerate()
        .any(|(idx, func)| {
            func.is_closure
                && matches!(table.get(idx), Some(FunctionEntry::Native(_)))
        })
}

/// Run `source` end-to-end via JITExecutor and return the wire-level
/// result. Used to verify observable program semantics after A.1D.2's
/// lifted preflight gate allows OwnedMutable-capture closures to JIT.
fn jit_run(source: &str) -> shape_wire::WireValue {
    use shape_runtime::engine::{ProgramExecutor, ShapeEngine};
    shape_runtime::initialize_shared_runtime().ok();
    let mut engine = ShapeEngine::new().expect("engine creation failed");
    let program = shape_ast::parse_program(source).expect("parse failed");
    let mut executor = crate::executor::JITExecutor::new();
    let result = executor
        .execute_program(&mut engine, &program)
        .or_else(|e| {
            // Re-surface the underlying error so test failures are
            // actionable (e.g. "missing prelude" vs "closure miscompile").
            Err(e)
        })
        .expect("JIT execution failed");
    result.wire_value
}

// ---------------------------------------------------------------------------
// 1. Preflight + compile-path tests (no runtime execution).
//
// These assert that under A.1D.2:
//   - Closure bodies with OwnedMutable captures now pass JIT preflight.
//   - The closure function appears as FunctionEntry::Native in the
//     mixed table (i.e. actually JIT-compiled, not interpreter-fallback).
// ---------------------------------------------------------------------------

#[test]
fn a1d2_jit_let_mut_closure_body_is_natively_compiled() {
    // Minimal `let mut` counter closure: `let mut x = 0; let f = || { x = x + 1; x }`.
    // The closure body emits `Load/StoreOwnedMutableCapture`. Before
    // A.1D.2, this would land in the mixed table as Interpreted(...);
    // under A.1D.2 it must appear as Native.
    //
    // Session 1 — Rust-move: observe the mutated cell via the closure's
    // return value (the former `x` tail-read is now a use-after-move
    // compile error).
    let source = r#"
        fn main() -> int {
            let mut x: int = 0
            let f = || { x = x + 1; x }
            f()
        }
        main()
    "#;
    let (program, table) = compile_to_mixed_table(source);
    assert!(
        has_native_closure(&program, &table),
        "A.1D.2: closure body containing Load/StoreOwnedMutableCapture must \
         JIT-compile to FunctionEntry::Native. native={} interpreted={}",
        table.native_count(),
        table.interpreted_count()
    );
}

#[test]
fn a1d2_jit_let_mut_closure_preflight_did_not_reject() {
    // Cross-check: the `compile_program_selective` path did NOT reject
    // the closure body on the bytecode gate. If A.1D.2's preflight
    // lifting regressed, this assertion would fail because
    // `compile_program_selective` would have demoted the closure to
    // `Interpreted` — detected via `native_count() == 0` on the table
    // below. We also pin `preflight_count() > 0` to prove the gate
    // accepted at least one function (guarding against silent
    // whole-program rejection).
    //
    // Session 1 — Rust-move: observe the mutated cell via the closure's
    // return value.
    let source = r#"
        fn main() -> int {
            let mut counter: int = 0
            let f = || { counter = counter + 1; counter }
            f()
        }
        main()
    "#;
    let (_program, table) = compile_to_mixed_table(source);
    assert!(
        table.native_count() > 0,
        "A.1D.2: at least one function must JIT-compile to native code"
    );
}

// ---------------------------------------------------------------------------
// 2. End-to-end execution tests.
//
// These parse → compile → JIT → execute and assert on the observable
// Shape-level result. The `let mut` closure must increment correctly
// through the A.1B cell plumbing, otherwise the JIT lowering of
// Load/StoreOwnedMutableCapture is buggy.
//
// Gated by `jit_v2_unstable_tests` cfg to match the rest of the JIT
// end-to-end test suite (see `mir_compiler/mod.rs::integration_tests`
// gating rationale: the JIT-runtime closure-dispatch path on
// jit-v2-phase1 has a pre-existing heap-corruption class of regressions
// (the `HeapValue::ClosureRaw` NaN-boxed decode disagrees with
// `JITClosure`'s `#[repr(C)]` layout used by `call_value`). The
// existing immutable-closure tests in `integration_tests::closure_*`
// fail under the same gate on this branch, so A.1D.2 does not regress
// observable runtime behaviour — it can only be verified end-to-end
// once the orthogonal closure-dispatch fix lands outside Track A. The
// preflight + compile tests above are unconditional and pin the
// A.1D.2-specific correctness: closure bodies containing
// `Load/StoreOwnedMutableCapture` now JIT-compile (not bail) and the
// MirToIR path emits a pointer-deref load/store through the capture
// slot's cell pointer (verified by the lowering side-table in
// `MirToIR::owned_mutable_capture_slots`).
// ---------------------------------------------------------------------------

/// Isolated unit test: verify the `ClosureLayout` metadata that drives
/// `register_owned_mutable_capture_slots` correctly classifies captures.
/// This pins the upstream contract — if `capture_storage_kind(i)` ever
/// disagrees with the constructor's `kinds[i]` argument, the A.1D.2
/// side-table would mis-flag slots and `read_place` / `write_place`
/// pointer-deref lowering would fire on the wrong slot.
#[test]
fn a1d2_closure_layout_capture_kinds_classify_correctly() {
    use shape_value::v2::closure_layout::{CaptureKind, ClosureLayout};
    use shape_value::v2::ConcreteType;

    // Mixed layout: capture 0 = Immutable (let), capture 1 =
    // OwnedMutable (let mut), capture 2 = Immutable. This is the
    // shape `MirToIR::register_owned_mutable_capture_slots` inspects
    // to decide which capture slots drive pointer-deref lowering.
    let capture_types = vec![
        ConcreteType::I64,
        ConcreteType::I64,
        ConcreteType::F64,
    ];
    let kinds = vec![
        CaptureKind::Immutable,
        CaptureKind::OwnedMutable,
        CaptureKind::Immutable,
    ];
    let layout = ClosureLayout::from_capture_types(&capture_types, &kinds);

    assert_eq!(layout.capture_storage_kind(0), CaptureKind::Immutable);
    assert_eq!(layout.capture_storage_kind(1), CaptureKind::OwnedMutable);
    assert_eq!(layout.capture_storage_kind(2), CaptureKind::Immutable);
    // And the mask: bit 1 set, bits 0 and 2 clear.
    assert_eq!(
        layout.owned_mutable_capture_mask, 0b010,
        "owned_mutable_capture_mask must have exactly bit 1 set for \
         the single OwnedMutable capture at index 1"
    );
    // Heap-capture mask is disjoint: I64 is not a heap-refcounted type.
    assert_eq!(layout.heap_capture_mask, 0);
    // Shared mask is empty: no `var` captures here.
    assert_eq!(layout.shared_capture_mask, 0);
}

#[test]
fn a1d2_jit_let_mut_counter_increments() {
    // Three calls to an incrementing closure; final read should be 3.
    // If OwnedMutable lowering is wrong, the closure would either:
    //   - read stale stack contents (likely 0),
    //   - overwrite the cell pointer (SIGSEGV on next call),
    //   - increment the closure's capture arg instead of the cell
    //     (reads 1 every time; final value 1, not 3).
    //
    // Session 1 — Rust-move: the outer `x` read after the third `f()`
    // is now a use-after-move compile error, so the closure returns
    // the accumulator instead. The three `f()` calls still exercise
    // the OwnedMutable cell mutation path across invocations.
    let source = r#"
        fn main() -> int {
            let mut x: int = 0
            let f = || { x = x + 1; x }
            f()
            f()
            f()
        }
        main()
    "#;
    match jit_run(source) {
        shape_wire::WireValue::Integer(n) => {
            assert_eq!(n, 3, "counter must reach 3 after three f() calls");
        }
        shape_wire::WireValue::Number(n) => {
            assert!(
                (n - 3.0).abs() < 1e-9,
                "counter must reach 3 (got Number {})",
                n
            );
        }
        other => panic!("expected Integer(3), got {:?}", other),
    }
}

#[test]
fn a1d2_jit_let_mut_mixed_immutable() {
    // Closure captures one `let` (immutable, read-only) and one `let mut`
    // (written). The immutable capture stays on the Immutable fast path;
    // the `let mut` one routes through the cell. Result exercises both
    // paths coexisting in a single closure layout.
    //
    // Session 1 — Rust-move: observe `x` via the closure's return
    // value. Two calls still exercise both capture kinds in concert.
    let source = r#"
        fn main() -> int {
            let base: int = 10
            let mut x: int = 0
            let f = || { x = x + base; x }
            f()
            f()
        }
        main()
    "#;
    match jit_run(source) {
        shape_wire::WireValue::Integer(n) => {
            assert_eq!(n, 20, "x should be 0 + 10 + 10 = 20");
        }
        shape_wire::WireValue::Number(n) => {
            assert!((n - 20.0).abs() < 1e-9, "expected 20, got Number {}", n);
        }
        other => panic!("expected Integer(20), got {:?}", other),
    }
}

#[test]
fn a1d2_jit_let_mut_closure_release_drops_box() {
    // Construct a `let mut` closure, invoke it, then let it drop at
    // scope exit. `release_typed_closure` (A.1A) walks the
    // `owned_mutable_capture_mask` and reclaims each cell via
    // `Box::from_raw`. No leak / double-free should occur. This test
    // simply runs to completion under the sanitizers the workspace
    // enables; a double-free would crash here before returning.
    //
    // Session 1 — Rust-move: `let mut result` is the outer-scope
    // sink. The inner `let mut x` must not be read in its declaring
    // scope after capture; the closure returns its own updated cell
    // and `result` absorbs that return value.
    let source = r#"
        fn main() -> int {
            var result: int = 0
            {
                let mut x: int = 42
                let f = || { x = x + 1; x }
                result = f()
            }
            result
        }
        main()
    "#;
    match jit_run(source) {
        shape_wire::WireValue::Integer(n) => assert_eq!(n, 43),
        shape_wire::WireValue::Number(n) => {
            assert!((n - 43.0).abs() < 1e-9, "expected 43, got Number {}", n)
        }
        other => panic!("expected Integer(43), got {:?}", other),
    }
}