vj 0.2.0

vj: a SystemVerilog (IEEE 1800-2023) simulator built on slang and LLVM.
//! The AOT-dynamic loader: resolves a compiled `<design>.so`'s ABI entry
//! points by exact symbol name via `libloading::Library`/`libloading::
//! Symbol` (AB-003, AB-004). Calls `sim_abi_version()` first, immediately
//! after symbol resolution and before invoking any other entry point
//! (AB-076), and refuses a mismatched-version kernel with a loud, named
//! error -- never a silent ABI skew, never calling into the kernel's other
//! entry points once a mismatch is detected (AB-077). Also declares the
//! narrow-scalar half of the accessor ABI (AB-078; the wide/`svLogicVecVal`-
//! shaped accessors are out of this task's scope -- see `docs/CONTRACTS.md`'s
//! AB-046 entry).
//!
//! `vj-abi` is `#![forbid(unsafe_code)]` (AB-038/AB-040's zero-logic
//! invariant): every `unsafe` block this loader needs -- resolving a raw
//! `dlsym` symbol and trusting its declared type -- lives here instead, each
//! justified with its own SAFETY comment, per this crate's own correctness
//! posture.

use std::ffi::CString;
use std::path::Path;

use libloading::{Library, Symbol};

use vj_abi::{EvalResult, Region, SimHandle, SimTime, SlotId};

/// AB-077's loud, named refusal -- never a silent ABI skew, never a panic
/// that loses the reason.
#[derive(Debug)]
pub enum LoaderError {
    /// `libloading::Library::new` itself failed (missing file, not a valid
    /// shared object, unresolved dependency, ...). Carries the path and the
    /// underlying `libloading`/`dlopen` error text.
    LoadFailed(String),
    /// A required ABI symbol was not found by its exact, fixed name.
    SymbolNotFound(&'static str),
    /// AB-076/AB-077's exact contract: `sim_abi_version()` returned a value
    /// other than this loader's own `vj_abi::VJ_ABI_VERSION`. No
    /// other symbol on this kernel is ever resolved or called once this is
    /// detected.
    AbiVersionMismatch { expected: u32, found: u32 },
}

impl std::fmt::Display for LoaderError {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        match self {
            LoaderError::LoadFailed(msg) => write!(f, "failed to load kernel: {msg}"),
            LoaderError::SymbolNotFound(sym) => {
                write!(f, "kernel is missing required ABI symbol {sym:?}")
            }
            LoaderError::AbiVersionMismatch { expected, found } => write!(
                f,
                "kernel ABI version mismatch: this loader expects VJ_ABI_VERSION={expected}, \
                 kernel's sim_abi_version() reported {found} -- refusing to load (never calling \
                 any other entry point on a version-mismatched kernel)"
            ),
        }
    }
}

impl std::error::Error for LoaderError {}

/// A dynamically loaded `<design>.so`, with every ABI entry point this
/// loader resolves already looked up by exact symbol name. Holds the
/// backing `libloading::Library` alive for its own lifetime -- every
/// resolved function pointer's code lives inside that library's mapped
/// memory, so the two must never be separated.
///
/// Every method takes `&mut self`, not `&self`: this is the `SimHandle`
/// single-thread-at-a-time discipline this loader enforces at the type
/// level (only one `&mut LoadedKernel` can exist at a time, so only one
/// call into the loaded kernel can be in flight through this loader at
/// once).
pub struct LoadedKernel {
    // Kept alive for as long as any resolved function pointer below might
    // be called -- never read directly after construction, so `_`-prefixed;
    // dropping it would unmap the code the fields below point into.
    _lib: Library,
    sim_init: vj_abi::SimInitFn,
    sim_settle: vj_abi::SimSettleFn,
    sim_eval_region: vj_abi::SimEvalRegionFn,
    sim_commit_nba: vj_abi::SimCommitNbaFn,
    sim_step_time: vj_abi::SimStepTimeFn,
    sim_final: vj_abi::SimFinalFn,
    sim_process_count: vj_abi::SimProcessCountFn,
    sv_get_scalar: vj_abi::SvGetScalarFn,
    sv_set_scalar: vj_abi::SvSetScalarFn,
}

/// Builds a NUL-terminated symbol name for `libloading::Library::get`.
/// Every ABI symbol name this loader looks up is one of `vj-abi`'s own
/// fixed `&'static str` constants, never caller-supplied, so a `CString`
/// construction failure (an embedded NUL byte) here would be an internal
/// invariant violation of this crate's own constants, not a possible
/// real-world input -- `expect` is therefore the correct loud failure mode,
/// not a silently swallowed `Result`.
fn nul_terminated(name: &'static str) -> CString {
    CString::new(name).unwrap_or_else(|e| {
        panic!("ABI symbol name constant {name:?} contains an interior NUL: {e}")
    })
}

/// Resolves one ABI symbol from `lib` by its exact, fixed name, and copies
/// out its raw `extern "C"` function-pointer value (fn pointers are
/// `Copy`, so this detaches the returned value from the `Symbol`'s own
/// borrow of `lib` -- required so every symbol can be resolved from the
/// same `&Library` in sequence, and so the resolved pointers can outlive
/// this function and be stored in a `LoadedKernel` alongside `lib` itself).
///
/// # Safety
/// The caller must supply `T` as the exact ABI-frozen `extern "C"`
/// function-pointer type `vj_abi` pins for the symbol named `name`
/// (e.g. `vj_abi::SimInitFn` for `SIM_INIT_SYMBOL`). `dlsym`/
/// `libloading` cannot check this -- a real, correctly-built kernel is
/// trusted to export exactly the frozen ABI shape for each fixed symbol
/// name; a mismatched real export is a build-system/kernel bug outside
/// what a name-based symbol lookup can ever detect, the same limit every
/// `dlopen`-based ABI loader has.
unsafe fn resolve<T: Copy>(lib: &Library, name: &'static str) -> Result<T, LoaderError> {
    let cname = nul_terminated(name);
    // SAFETY: see this function's own top-level SAFETY note -- `T` is
    // asserted by the caller to be the correct ABI type for `name`.
    let sym: Symbol<'_, T> = unsafe { lib.get(cname.as_bytes_with_nul()) }
        .map_err(|_| LoaderError::SymbolNotFound(name))?;
    Ok(*sym)
}

impl LoadedKernel {
    /// Loads `path` as a compiled vj kernel (AOT-dynamic `.so`).
    ///
    /// Order of operations (AB-076/AB-077, load-bearing -- do not reorder):
    /// 1. `libloading::Library::new(path)`.
    /// 2. Resolve `sim_abi_version` ONLY, call it, compare against
    ///    [`vj_abi::VJ_ABI_VERSION`]. On mismatch, return
    ///    immediately -- no other symbol on this kernel is ever resolved or
    ///    called.
    /// 3. Only once the version matches: resolve every other entry point
    ///    and the two narrow-scalar accessors this loader exposes.
    pub fn load_dynamic(path: &Path) -> Result<Self, LoaderError> {
        // SAFETY: `Library::new`'s only documented unsafety is that a
        // shared object's static initializers/destructors run arbitrary
        // code when loaded/unloaded -- an accepted, unavoidable consequence
        // of dynamically loading any code at all (there is no way to
        // `dlopen` a `.so` without this). `path` is a caller-supplied
        // location the caller intends to load and treat as a compiled
        // vj kernel.
        let lib = unsafe { Library::new(path) }
            .map_err(|e| LoaderError::LoadFailed(format!("{}: {e}", path.display())))?;

        // AB-076: sim_abi_version is the ONLY symbol resolved and called
        // before the version check below decides whether anything else may
        // ever be touched.
        // SAFETY: SimAbiVersionFn (`extern "C" fn() -> u32`) is
        // vj_abi::version's own frozen type for SIM_ABI_VERSION_SYMBOL.
        let sim_abi_version: vj_abi::SimAbiVersionFn =
            unsafe { resolve(&lib, vj_abi::version::SIM_ABI_VERSION_SYMBOL) }?;
        let found = sim_abi_version();
        if found != vj_abi::VJ_ABI_VERSION {
            // AB-077: loud, named refusal. `lib` (and every symbol it could
            // resolve) is dropped here, unused -- no other entry point on
            // this kernel is ever called.
            return Err(LoaderError::AbiVersionMismatch {
                expected: vj_abi::VJ_ABI_VERSION,
                found,
            });
        }

        // Version matched: safe, per AB-076/AB-077's own contract, to
        // resolve every other entry point and accessor now.
        // SAFETY (all nine `resolve` calls below): each `T` is the exact
        // ABI-frozen fn-pointer type `vj_abi` pins for the symbol name
        // passed alongside it -- see `resolve`'s own SAFETY note.
        let sim_init: vj_abi::SimInitFn = unsafe { resolve(&lib, vj_abi::entry::SIM_INIT_SYMBOL) }?;
        let sim_settle: vj_abi::SimSettleFn =
            unsafe { resolve(&lib, vj_abi::entry::SIM_SETTLE_SYMBOL) }?;
        let sim_eval_region: vj_abi::SimEvalRegionFn =
            unsafe { resolve(&lib, vj_abi::entry::SIM_EVAL_REGION_SYMBOL) }?;
        let sim_commit_nba: vj_abi::SimCommitNbaFn =
            unsafe { resolve(&lib, vj_abi::entry::SIM_COMMIT_NBA_SYMBOL) }?;
        let sim_step_time: vj_abi::SimStepTimeFn =
            unsafe { resolve(&lib, vj_abi::entry::SIM_STEP_TIME_SYMBOL) }?;
        let sim_final: vj_abi::SimFinalFn =
            unsafe { resolve(&lib, vj_abi::entry::SIM_FINAL_SYMBOL) }?;
        let sim_process_count: vj_abi::SimProcessCountFn =
            unsafe { resolve(&lib, vj_abi::entry::SIM_PROCESS_COUNT_SYMBOL) }?;
        let sv_get_scalar: vj_abi::SvGetScalarFn =
            unsafe { resolve(&lib, vj_abi::accessor::SV_GET_SCALAR_SYMBOL) }?;
        let sv_set_scalar: vj_abi::SvSetScalarFn =
            unsafe { resolve(&lib, vj_abi::accessor::SV_SET_SCALAR_SYMBOL) }?;

        Ok(LoadedKernel {
            _lib: lib,
            sim_init,
            sim_settle,
            sim_eval_region,
            sim_commit_nba,
            sim_step_time,
            sim_final,
            sim_process_count,
            sv_get_scalar,
            sv_set_scalar,
        })
    }

    // Calling an already-resolved `extern "C" fn` value needs no `unsafe`
    // block: the fn-pointer's own type fully declares its ABI. The only
    // unsafety in this whole loader is resolving the raw symbol in the
    // first place (`resolve`, above).
    pub fn sim_init(&mut self, design_id: u32) -> SimHandle {
        (self.sim_init)(design_id)
    }

    pub fn sim_settle(&mut self, h: SimHandle) -> EvalResult {
        (self.sim_settle)(h)
    }

    pub fn sim_eval_region(&mut self, h: SimHandle, region: Region, t: SimTime) -> EvalResult {
        (self.sim_eval_region)(h, region, t)
    }

    pub fn sim_commit_nba(&mut self, h: SimHandle, t: SimTime) -> EvalResult {
        (self.sim_commit_nba)(h, t)
    }

    pub fn sim_step_time(&mut self, h: SimHandle, t: SimTime) -> EvalResult {
        (self.sim_step_time)(h, t)
    }

    pub fn sim_final(&mut self, h: SimHandle) {
        (self.sim_final)(h)
    }

    pub fn sim_process_count(&mut self, h: SimHandle) -> u32 {
        (self.sim_process_count)(h)
    }

    pub fn sv_get_scalar(&mut self, h: SimHandle, slot: SlotId) -> u128 {
        (self.sv_get_scalar)(h, slot)
    }

    pub fn sv_set_scalar(&mut self, h: SimHandle, slot: SlotId, v: u128) {
        (self.sv_set_scalar)(h, slot, v)
    }
}