vj 0.2.0

vj: a SystemVerilog (IEEE 1800-2023) simulator built on slang and LLVM.
//! Task 4 (A2-part2; ledger CG-127, RT-003): `load_aot_kernel` -- resolves
//! an AOT-linked `.so`'s currently-real ABI surface via `libloading`,
//! refusing loudly (never silently) on an ABI-version mismatch or a missing
//! symbol.
//!
//! **Scope:** [`load_aot_kernel`] resolves `sim_abi_version` (validated
//! immediately, per CG-127's "refusing ... if `sim_abi_version()` disagrees"
//! contract) and `sim_process_count`, plus a compiled kernel's own
//! per-process functions by mangled name (`get_process_fn`, mirroring
//! `vj_codegen::jit::Jit::get_process_fn`). The other ARCHITECTURE ยง5
//! entry points are not resolved here: whole-design runs go through
//! [`run_aot`], which calls the library's `vj_aot_main` (docs/CONTRACTS.md,
//! "AOT driver entry and image version"); the serialized design image is read
//! from `DESIGN_IMAGE_PTR_SYMBOL`/`DESIGN_IMAGE_LEN_SYMBOL` and checked against
//! `IMAGE_VERSION`. VPI under AOT: `build-aot --vpi-lib` writes the object model
//! and the build-time site tables into the image's `vpi` section
//! (`vj_vpi::boot`); `run-aot --vpi` loads the libraries and runs them
//! against that model inside the design library (`vpi_*` calls forward via
//! `vj_vpi::forward`).
use std::path::{Path, PathBuf};

use libloading::{Library, Symbol};
use vj_abi::{ProcessFn, SimHandle};
use vj_codegen::aot::{DESIGN_IMAGE_LEN_SYMBOL, DESIGN_IMAGE_PTR_SYMBOL};
use vj_sim::image::DesignImage;

use crate::dpi;

/// Every failure mode [`load_aot_kernel`] can hit -- a named variant, never
/// a panic and never a silently-`None`-returning success.
#[derive(Debug)]
pub enum AotLoadError {
    Open(String),
    MissingSymbol {
        symbol: &'static str,
        reason: String,
    },
    AbiVersionMismatch {
        found: u32,
        expected: u32,
    },
    /// Loading the DPI-C libraries or reading the design's DPI tables failed.
    Dpi(String),
    /// A timing-option flag on the `run-aot` command line (LRM 30.7/31.8/
    /// 31.9.4/32) disagrees with what `build-aot` baked into the library
    /// (`crate::timing_options::TimingOptions::conflicts_with`): one message
    /// per disagreeing flag, each naming it and both values.
    TimingOptionConflict(Vec<String>),
}

impl std::fmt::Display for AotLoadError {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        match self {
            AotLoadError::Open(e) => write!(f, "failed to load AOT .so: {e}"),
            AotLoadError::MissingSymbol { symbol, reason } => {
                write!(f, "missing symbol `{symbol}`: {reason}")
            }
            AotLoadError::Dpi(e) => f.write_str(e),
            AotLoadError::AbiVersionMismatch { found, expected } => write!(
                f,
                "ABI version mismatch: .so reports {found}, this loader expects {expected}"
            ),
            AotLoadError::TimingOptionConflict(msgs) => {
                write!(
                    f,
                    "run-aot timing option conflict(s) with the built library:"
                )?;
                for m in msgs {
                    write!(f, "\n  {m}")?;
                }
                Ok(())
            }
        }
    }
}

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

/// A `dlopen`'d AOT kernel `.so`, already ABI-version-checked at load time
/// (CG-127's own "refuses a mismatched kernel with a loud, named error"
/// contract) -- keeps the `Library` mapped for the caller's whole use of it
/// (unlike `AotBackend::link`'s own post-link probe, which closes its
/// `Library` immediately after reading `abi_version`/`process_count`).
pub struct LoadedKernel {
    lib: Library,
    pub process_count: u32,
}

impl LoadedKernel {
    /// Resolves a compiled process kernel's callable function pointer by
    /// its mangled (LLVM) name -- `Some(fn_ptr)` when `mangled` names a
    /// function the linked `.so` actually exports, `None` for an unknown
    /// name. Mirrors `vj_codegen::jit::Jit::get_process_fn`'s exact
    /// contract (never a panic on an unknown name), so a caller can treat
    /// an AOT-loaded kernel and a JIT-compiled one identically.
    pub fn get_process_fn(&self, mangled: &str) -> Option<ProcessFn> {
        // SAFETY: `mangled` names, at most, a real `ProcessFn`-shaped
        // symbol (`extern "C" fn(SimHandle, *mut u8, u32) -> EvalResult`)
        // this crate's own codegen emitted into the object this `.so` was
        // linked from -- the exact shape every `VJ_PROCESS_TABLE` entry
        // already carries. An unresolvable name yields `Err` from
        // `Library::get`, turned into `None` below, never dereferenced.
        unsafe {
            self.lib
                .get::<ProcessFn>(mangled.as_bytes())
                .ok()
                .map(|s| *s)
        }
    }
}

/// `vj run-aot`: loads the design library at `path` and runs it to
/// completion through its own `vj_aot_main`. `args` are the run options
/// (`+plusarg`, `--seed N`, `--x-initial zero|x`); a timing option (LRM
/// 30.7/31.8/31.9.4/32, same flags as `vj run`/`build-aot`) is checked
/// against the library's own baked choice -- `Err(TimingOptionConflict)` on
/// a disagreement, never silently re-run under the new one -- and otherwise
/// consumed, never forwarded. The library's name is its `argv[0]`. Returns
/// the design's exit code.
pub fn run_aot(path: &Path, args: &[String]) -> Result<i32, AotLoadError> {
    use std::ffi::{CString, c_char};

    // DPI-C: `--sv-lib LIB` / `-sv_lib LIB` / a `.so` path / a C source file
    // are the host code the design calls (as for `vj run`); a timing
    // option (LRM 30.7/31.8/31.9.4/32, `crate::timing_options::TimingOptions`)
    // is checked against the library's own baked choice below and never
    // forwarded (the design's own argv parser has no concept of it -- these
    // are a BUILD-time decision, already in the machine code `build-aot`
    // emitted); everything else is a run option for the design.
    let mut libs: Vec<PathBuf> = Vec::new();
    let mut c_sources: Vec<PathBuf> = Vec::new();
    let mut run_args: Vec<String> = Vec::new();
    let mut vpi_libs: Vec<PathBuf> = Vec::new();
    let mut timing = crate::timing_options::TimingOptions::default();
    let mut it = args.iter();
    while let Some(a) = it.next() {
        if a == "--sv-lib" || a == "-sv_lib" {
            match it.next() {
                Some(lib) => libs.push(dpi::sv_lib_path(lib)),
                None => return Err(AotLoadError::Dpi(format!("{a} needs a library"))),
            }
        } else if a == "--vpi" || a == "-vpi" {
            match it.next() {
                Some(lib) => vpi_libs.push(vpi_library(lib)?),
                None => return Err(AotLoadError::Dpi(format!("{a} needs a library"))),
            }
        } else if let Some(list) = a.strip_prefix("+vpi=") {
            // Still a plusarg, as for `vj run`: the design may test for it.
            run_args.push(a.clone());
            for lib in list.split(',').filter(|l| !l.is_empty()) {
                vpi_libs.push(vpi_library(lib)?);
            }
        } else if dpi::is_c_source(Path::new(a)) {
            c_sources.push(PathBuf::from(a));
        } else if a.ends_with(".so") {
            libs.push(PathBuf::from(a));
        } else {
            match timing.parse_arg(a) {
                Ok(true) => {}
                Ok(false) => run_args.push(a.clone()),
                Err(e) => return Err(AotLoadError::TimingOptionConflict(vec![e])),
            }
        }
    }
    if !c_sources.is_empty() {
        libs.push(dpi::compile_sources(&c_sources).map_err(AotLoadError::Dpi)?);
    }
    // The VPI libraries load inside the design library (its own VPI engine,
    // `vj_aot::vpi_prepare`), so they travel as its `--vpi` options.
    for lib in &vpi_libs {
        run_args.push("--vpi".to_string());
        run_args.push(lib.display().to_string());
    }
    let args = &run_args[..];

    // SAFETY: `path` is a design library `vj build-aot` produced; loading
    // it runs its constructors, which register its design image. It binds
    // lazily, so its undefined DPI import symbols may stay unresolved until
    // the user libraries are loaded below.
    let lib = unsafe { Library::new(path) }
        .map_err(|e| AotLoadError::Open(format!("{}: {e}", path.display())))?;
    // SAFETY: `sim_abi_version` is `extern "C" fn() -> u32` in every library
    // this loader accepts.
    let abi_version: u32 = unsafe {
        let sym: Symbol<extern "C" fn() -> u32> = lib
            .get(vj_abi::version::SIM_ABI_VERSION_SYMBOL.as_bytes())
            .map_err(|e| AotLoadError::MissingSymbol {
                symbol: "sim_abi_version",
                reason: e.to_string(),
            })?;
        sym()
    };
    if abi_version != vj_abi::VJ_ABI_VERSION {
        return Err(AotLoadError::AbiVersionMismatch {
            found: abi_version,
            expected: vj_abi::VJ_ABI_VERSION,
        });
    }
    // The design's DPI exports (from its embedded image) get their trampolines
    // before the user libraries, which link against them; then the host table.
    // SAFETY: both accessors are `extern "C"` functions the design library
    // defines (`emit_image_globals`), and the bytes live as long as it does.
    let image = unsafe {
        let ptr: Symbol<extern "C" fn() -> *const u8> = lib
            .get(DESIGN_IMAGE_PTR_SYMBOL.as_bytes())
            .map_err(|e| AotLoadError::MissingSymbol {
                symbol: "vj_design_image_ptr",
                reason: e.to_string(),
            })?;
        let len: Symbol<extern "C" fn() -> u64> = lib
            .get(DESIGN_IMAGE_LEN_SYMBOL.as_bytes())
            .map_err(|e| AotLoadError::MissingSymbol {
                symbol: "vj_design_image_len",
                reason: e.to_string(),
            })?;
        DesignImage::decode(std::slice::from_raw_parts(ptr(), len() as usize))
            .map_err(|e| AotLoadError::Dpi(format!("the design image: {e}")))?
    };
    let conflicts = timing.conflicts_with(&image.timing.options);
    if !conflicts.is_empty() {
        return Err(AotLoadError::TimingOptionConflict(conflicts));
    }
    let export_names: Vec<String> = image.dpi.exports.iter().map(|(n, _)| n.clone()).collect();
    dpi::load_export_trampolines(&export_names).map_err(AotLoadError::Dpi)?;
    if !libs.is_empty() {
        dpi::load_svdpi_support().map_err(AotLoadError::Dpi)?;
    }
    dpi::load_libraries(&libs).map_err(AotLoadError::Dpi)?;
    // SAFETY: `vj_aot_set_dpi_host` takes the table `AotDpiHost` mirrors,
    // which is a static and so outlives the library.
    unsafe {
        let set: Symbol<unsafe extern "C" fn(*const dpi::AotDpiHost)> = lib
            .get(b"vj_aot_set_dpi_host")
            .map_err(|e| AotLoadError::MissingSymbol {
                symbol: "vj_aot_set_dpi_host",
                reason: e.to_string(),
            })?;
        set(&dpi::AOT_DPI_HOST);
    }
    if !vpi_libs.is_empty() {
        // The VPI libraries bind `vpi_*` to this process's definitions, so
        // those must reach the design library's VPI state (`forward`).
        // SAFETY: `vj_aot_vpi_table` takes no arguments and returns a
        // table of the same type, built by the same crate, that lives for
        // the process.
        unsafe {
            let table: Symbol<extern "C" fn() -> *const crate::vpi::forward::ApiTable> = lib
                .get(b"vj_aot_vpi_table")
                .map_err(|e| AotLoadError::MissingSymbol {
                    symbol: "vj_aot_vpi_table",
                    reason: e.to_string(),
                })?;
            crate::vpi::forward::install(table());
        }
    }
    let argv: Vec<CString> = std::iter::once(path.display().to_string())
        .chain(args.iter().cloned())
        .map(|a| CString::new(a.replace('\0', "")).expect("NULs were removed"))
        .collect();
    let ptrs: Vec<*const c_char> = argv.iter().map(|a| a.as_ptr()).collect();
    // SAFETY: `vj_aot_main(argc, argv)` takes `argc` NUL-terminated
    // strings, which `ptrs` is, and `argv` outlives the call.
    let code = unsafe {
        let main: Symbol<unsafe extern "C" fn(i32, *const *const c_char) -> i32> = lib
            .get(b"vj_aot_main")
            .map_err(|e| AotLoadError::MissingSymbol {
                symbol: "vj_aot_main",
                reason: e.to_string(),
            })?;
        main(ptrs.len() as i32, ptrs.as_ptr())
    };
    // The runner's thread-locals and atexit state live in the library; the
    // process exits right after, so it is never unloaded.
    std::mem::forget(lib);
    Ok(code)
}

/// A `--vpi`/`+vpi=` entry as a library path (as `vj run` reads it): a C
/// source is compiled against the bundled headers, otherwise the path as
/// given, a bare `NAME` meaning `NAME.so`.
fn vpi_library(arg: &str) -> Result<PathBuf, AotLoadError> {
    let path = Path::new(arg);
    if dpi::is_c_source(path) {
        dpi::compile_sources(&[path.to_path_buf()]).map_err(AotLoadError::Dpi)
    } else {
        Ok(dpi::sv_lib_path(arg))
    }
}

/// Loads `path` (an `AotBackend::link`-produced `.so`), immediately
/// resolving and calling `sim_abi_version` -- refusing loudly
/// (`AotLoadError::AbiVersionMismatch`) if it disagrees with this crate's
/// own `vj_abi::VJ_ABI_VERSION` -- then resolving `sim_process_count`.
pub fn load_aot_kernel(path: &Path) -> Result<LoadedKernel, AotLoadError> {
    // SAFETY: `path` is a `.so` this workspace's own `AotBackend::link`
    // produced (this function's only real caller so far); loading it runs
    // the shared object's ordinary dynamic-loader initialization, the same
    // accepted risk `AotBackend::link`'s own post-link probe already takes.
    let lib = unsafe { Library::new(path) }
        .map_err(|e| AotLoadError::Open(format!("{}: {e}", path.display())))?;

    let abi_version: u32 = unsafe {
        let sym: Symbol<extern "C" fn() -> u32> = lib
            .get(vj_abi::version::SIM_ABI_VERSION_SYMBOL.as_bytes())
            .map_err(|e| AotLoadError::MissingSymbol {
                symbol: "sim_abi_version",
                reason: e.to_string(),
            })?;
        sym()
    };
    if abi_version != vj_abi::VJ_ABI_VERSION {
        return Err(AotLoadError::AbiVersionMismatch {
            found: abi_version,
            expected: vj_abi::VJ_ABI_VERSION,
        });
    }

    let process_count: u32 = unsafe {
        let sym: Symbol<extern "C" fn(SimHandle) -> u32> = lib
            .get(vj_abi::entry::SIM_PROCESS_COUNT_SYMBOL.as_bytes())
            .map_err(|e| AotLoadError::MissingSymbol {
                symbol: "sim_process_count",
                reason: e.to_string(),
            })?;
        sym(SimHandle(std::ptr::null_mut()))
    };

    Ok(LoadedKernel { lib, process_count })
}