vj 0.2.0

vj: a SystemVerilog (IEEE 1800-2023) simulator built on slang and LLVM.
//! Task 11 (`docs/superpowers/plans/2026-09-23-wave-A-abi.md`; ledger
//! AB-006): AOT-static packaging build script.
//!
//! Off by default -- this whole script is a no-op unless this package's own
//! `static-test-kernel` Cargo feature is enabled (cargo sets
//! `CARGO_FEATURE_STATIC_TEST_KERNEL` in the build script's own environment
//! whenever that feature is active for the current compilation). When
//! active, this:
//!
//! 1. Reuses Task 10's hand-written fixture-kernel source template
//!    (`tests/fixtures/test_kernel.rs`, embedded at THIS file's own compile
//!    time via `include_str!` -- never read at runtime) -- same rationale
//!    as that file's own doc comment: only the ABI's binary layout matters
//!    across a link boundary, never Rust nominal-type identity, so a plain
//!    rustc-compiled object stands in for a real codegen-emitted kernel.
//!    Substitutes its two template tokens with a literal, always-matching
//!    `VJ_ABI_VERSION` and an empty `__ABORT__` -- this static path
//!    never exercises the ABI-version-mismatch/abort behavior Task 10's
//!    dynamic loader tests use that token for.
//! 2. Compiles the substituted source directly to a native static archive
//!    via `rustc --crate-type=staticlib` -- no dynamic-loading crate, no
//!    `dlopen`, no `.so` anywhere in this path (a dedicated `xtask` lint
//!    pins that this file never references Task 10's dynamic-loading
//!    dependency by name).
//! 3. Emits `cargo:rustc-link-arg-bins`/`cargo:rustc-link-arg-tests` (see
//!    the deviation note at their call site below) so cargo statically
//!    links that archive's path directly into EVERY bin and test target of
//!    this package (the `vj` binary, and any integration test target)
//!    -- "no separate manual link step" (AB-006's acceptance).
//! 4. Writes a small generated Rust shim of `extern "C"` declarations to
//!    `$OUT_DIR/kernel_shim.rs`, `include!`d by `src/main.rs`'s
//!    feature-gated smoke path and by `tests/static_link.rs` directly.

use std::env;
use std::path::PathBuf;
use std::process::Command;

/// Task 10's own fixture-kernel source template, reused verbatim (never
/// re-derived) -- see that file's own doc comment for the substitution
/// contract (`__ABI_VERSION__`, `__ABORT__`).
const FIXTURE_TEMPLATE: &str = include_str!("tests/fixtures/test_kernel.rs");

fn main() {
    // Always emitted (even on the early-return path below) so toggling the
    // feature on/off reliably reruns this script instead of reusing a stale
    // no-op run.
    println!("cargo:rerun-if-env-changed=CARGO_FEATURE_STATIC_TEST_KERNEL");

    // DPI-C: a user library loaded by `vj run` (`src/dpi.rs`) links
    // against the `svdpi.h` functions this executable defines, so they must
    // be in its dynamic symbol table.
    println!("cargo:rustc-link-arg-bins=-rdynamic");
    // A published package ships no integration tests, and Cargo rejects
    // `rustc-link-arg-tests` for a package without a test target.
    let has_tests = has_integration_tests();
    if has_tests {
        println!("cargo:rustc-link-arg-tests=-rdynamic");
    }

    // VPI: the C-variadic entry points (`vpi_printf`, ...) are compiled by
    // `vj-vpi`'s own build script; `-rdynamic` above exports them.

    if env::var("CARGO_FEATURE_STATIC_TEST_KERNEL").is_err() {
        // The default, no-feature build of vj is unaffected (Task
        // 11's own acceptance) -- do nothing at all.
        return;
    }

    println!("cargo:rerun-if-changed=tests/fixtures/test_kernel.rs");

    let out_dir = PathBuf::from(
        env::var("OUT_DIR").expect("OUT_DIR is always set for a crate with a build.rs"),
    );

    // Always the real, matching ABI version -- read from the real
    // vj_abi::VJ_ABI_VERSION constant (the new [build-dependencies]
    // edge), never hand-typed as a bare `1`. Never aborts (the static path
    // has no "loader" to prove a refusal against; this fixture exists to
    // prove static linking works, not to re-prove AB-076/AB-077, already
    // covered by Task 10's dynamic-loader tests).
    let source = FIXTURE_TEMPLATE
        .replace("__ABI_VERSION__", &vj_abi::VJ_ABI_VERSION.to_string())
        .replace("__ABORT__", "");

    let src_path = out_dir.join("static_test_kernel.rs");
    std::fs::write(&src_path, &source)
        .unwrap_or_else(|e| panic!("failed to write {src_path:?}: {e}"));

    let archive_path = out_dir.join("libkernel_prologue.a");
    let output = Command::new("rustc")
        .arg("--crate-type=staticlib")
        .arg("--crate-name=static_test_kernel")
        .arg("--edition=2024")
        .arg("-o")
        .arg(&archive_path)
        .arg(&src_path)
        .output()
        .unwrap_or_else(|e| {
            panic!("failed to spawn rustc to compile the static fixture kernel: {e}")
        });
    assert!(
        output.status.success(),
        "rustc failed compiling the AOT-static fixture kernel; stderr:\n{}",
        String::from_utf8_lossy(&output.stderr)
    );
    assert!(
        archive_path.exists(),
        "rustc reported success but {archive_path:?} does not exist"
    );

    // "no separate manual link step": pass the archive's path directly as a
    // linker argument to every bin AND test target of this package.
    //
    // Deviation from the plan's literal text: the plan describes wiring
    // this via `cargo:rustc-link-search` + `cargo:rustc-link-lib=static=
    // kernel_prologue` (the usual `-L`/`-l` pair). Verified directly against
    // this workspace's real build: that pair DOES reach the `vj` bin
    // target's link line (confirmed via `nm` on the built binary -- both
    // `sim_abi_version` and `sim_init` appear defined) but does NOT reach
    // this package's own integration *test* targets' link line (confirmed
    // via `--verbose`: the `static_link` test's real `rustc` invocation
    // carried the `-L native=.../out` search path from this same build
    // script run but no matching `-l` flag at all, so the test failed to
    // link with "undefined symbol: sim_abi_version"). `cargo:
    // rustc-link-arg-bins`/`cargo:rustc-link-arg-tests` are Cargo's own
    // documented, kind-specific directives for exactly this "add a link
    // argument to every target of a given kind in this package" case, and
    // passing the archive's own path directly (rather than through `-l`/
    // `-L` name resolution) sidesteps whatever unit-graph distinction the
    // plain pair was tripping over. Verified this now reaches BOTH the bin
    // and every test target (Task 11's own three tests all link and pass).
    let archive_arg = archive_path.display().to_string();
    println!("cargo:rustc-link-arg-bins={archive_arg}");
    if has_tests {
        println!("cargo:rustc-link-arg-tests={archive_arg}");
    }

    let shim = r#"// GENERATED by crates/vj/build.rs (Task 11, AB-006). Declares the
// statically-linked fixture kernel's ABI surface as ordinary `extern "C"`
// symbols -- no dynamic-loading crate, no dlopen. `include!`d, never a module (so each
// including crate gets its own private copy of these type/fn declarations,
// exactly mirroring the raw #[repr(C)] shapes
// crates/vj/tests/fixtures/test_kernel.rs's own doc comment
// documents for a dlopen'd kernel -- only the binary LAYOUT crosses this
// link boundary, never Rust nominal-type identity).

// #[allow(dead_code)] throughout: each consumer of this generated file
// (src/main.rs's smoke path, tests/static_link.rs) only actually calls
// sim_abi_version/sim_init -- the remaining seven entry points are declared
// for AB-006's "the exact 9 entry points" completeness (mirroring
// vj_abi::entry's own full set) even though no test here exercises them
// beyond proving they link (Task 10's dynamic-loader tests already cover
// their runtime behavior against the same fixture template).
#[allow(dead_code)]
#[repr(C)]
pub struct EvalResultRaw {
    pub outcome: u32,
    pub reschedule: [u8; 32],
}

#[allow(dead_code)]
#[repr(C)]
pub struct SimTimeRaw {
    pub sim_units: i64,
    pub delta: u32,
}

#[allow(dead_code)]
unsafe extern "C" {
    pub fn sim_abi_version() -> u32;
    pub fn sim_init(design_id: u32) -> *mut core::ffi::c_void;
    pub fn sim_settle(h: *mut core::ffi::c_void) -> EvalResultRaw;
    pub fn sim_eval_region(h: *mut core::ffi::c_void, region: u32, t: SimTimeRaw) -> EvalResultRaw;
    pub fn sim_commit_nba(h: *mut core::ffi::c_void, t: SimTimeRaw) -> EvalResultRaw;
    pub fn sim_save(h: *mut core::ffi::c_void) -> *mut core::ffi::c_void;
    pub fn sim_restore(h: *mut core::ffi::c_void, checkpoint: *mut core::ffi::c_void);
    pub fn sim_step_time(h: *mut core::ffi::c_void, t: SimTimeRaw) -> EvalResultRaw;
    pub fn sim_final(h: *mut core::ffi::c_void);
    pub fn sim_process_count(h: *mut core::ffi::c_void) -> u32;
    // Task 16 (AB-009): the two narrow-scalar accessors, added to this
    // generated shim so `tests/cross_mode_differential.rs`'s AOT-static leg
    // can read/write the fixture's CELL/REQUEST_CELL slots the same way
    // Task 10's dynamic loader already does -- additive only, the original
    // 9 entry-point declarations above are unchanged.
    pub fn sv_get_scalar(h: *mut core::ffi::c_void, slot: u32) -> u128;
    pub fn sv_set_scalar(h: *mut core::ffi::c_void, slot: u32, v: u128);
}
"#;
    let shim_path = out_dir.join("kernel_shim.rs");
    std::fs::write(&shim_path, shim)
        .unwrap_or_else(|e| panic!("failed to write {shim_path:?}: {e}"));
}

/// Whether this package has integration test targets (`tests/*.rs`).
fn has_integration_tests() -> bool {
    std::fs::read_dir("tests").is_ok_and(|dir| {
        dir.flatten()
            .any(|e| e.path().extension().is_some_and(|x| x == "rs"))
    })
}