vj 0.3.0

vj: a SystemVerilog (IEEE 1800-2023) simulator built on slang and LLVM.
//! The legacy single-top kernel path (what `vj build-aot <file.sv>
//! <runtime_rlib> <out.so>` was; the CLI now builds whole designs through
//! `crate::aot::build_aot_design`). It stays as the library entry the ABI
//! entry-point tests drive against the legacy `libvj_runtime.a`.
//!
//! Task 4 (A2-part2, `docs/superpowers/plans/2026-09-26-wave-A2-process-
//! kernels.md`; ledger CG-127, RT-003): compiles a design's every process into one
//! AOT object, links it against `vj-runtime`'s staticlib, and reports
//! the produced [`vj_codegen::aot::AotKernelHandle`]. Deliberately does
//! NOT run the design (no `StateEngine`/`sched::Engine`/`$display` drain) --
//! object emission, linking, and `.so` packaging are this task's own scope;
//! driving a linked `.so` to completion needs Task 5's ABI entry points
//! (`sim_init`/`sim_settle`/...), which have no real body anywhere in this
//! tree yet (see `vj_codegen::aot`'s own module doc comment).
//!
//! **Single-top, no-sub-instance scope (deliberate, matches `run_design`'s
//! own original Task-S7-era restriction, before Task F3 added multi-
//! instance support):** instance hierarchy/port-wiring (`PortWireProcess`,
//! `run.rs`) is a run-time/STATE concern, orthogonal to object emission --
//! this subcommand reuses `run.rs`'s own already-tested helpers
//! (`signal_slots_of`, `loop_kind_of`, `elab_to_run_error`,
//! `sim_lowering_to_run_error`, `instance_body_of`, bumped to `pub(crate)`
//! for this file) for the ONE top instance, rather than re-deriving a
//! second, possibly-divergent per-process discovery walk.
use std::path::{Path, PathBuf};

use vj_codegen::aot::{AotBackend, AotKernelHandle};
use vj_codegen::error::LoweringError;
use vj_codegen::frontend::compile_source_named;
use vj_codegen::module::{build_comb_sensitivity_index, discover_processes};
use vj_codegen::pipeline::compile_to_object;
use vj_codegen::process_lower::build_lowering_plan;

use vj_sim::netlist::{self, NetlistView};
use vj_sim::process::cfg::{build_cfg, contains_unschedulable_fork_join, lower_to_segments};
use vj_sim::process::liveness::compute_liveness;

use crate::run::{
    RunError, elab_to_run_error, instance_body_of, loop_kind_of, signal_slots_of,
    sim_lowering_to_run_error,
};

/// Compiles `path`'s design into an AOT object at `out_obj`, links it
/// (against `runtime_rlib`) into `out_so`, and returns the resulting
/// [`AotKernelHandle`]. See this module's own doc comment for the
/// single-top, no-hierarchy scope restriction.
pub fn build_aot_so(
    path: &Path,
    out_obj: &Path,
    runtime_rlib: &Path,
    out_so: &Path,
) -> Result<AotKernelHandle, RunError> {
    let src = std::fs::read_to_string(path).map_err(RunError::Io)?;
    let src =
        crate::randseq::expand(&src, &path.to_string_lossy()).map_err(RunError::Diagnostics)?;
    let mut design =
        compile_source_named(&src, &path.to_string_lossy()).map_err(RunError::Compile)?;
    vj_codegen::let_order::check_let_definition_order(&design.syntax_trees())
        .map_err(RunError::Diagnostics)?;
    vj_sim::process::cfg::set_source_text(Some(&src));
    vj_sim::process::cfg::set_source_name(Some(&path.display().to_string()));
    vj_sim::process::cfg::set_source_session(Some(design.session().clone()));
    let comb_sensitivity = build_comb_sensitivity_index(&design).map_err(RunError::Lowering)?;

    let eval = design.eval_session();
    let elaborated = netlist::elaborate(&eval).map_err(|e| elab_to_run_error(e, path))?;
    if elaborated.top.len() != 1 {
        return Err(RunError::Lowering(LoweringError::unsupported(
            &format!(
                "design has {} top-level module instances; build-aot supports exactly one",
                elaborated.top.len()
            ),
            path.display().to_string().as_str(),
        )));
    }
    let registration_order = elaborated.registration_order();
    let top_id = elaborated.top[0];
    if registration_order.len() != 1 || registration_order[0] != top_id {
        return Err(RunError::Lowering(LoweringError::unsupported(
            "build-aot does not support sub-instance hierarchy (port-wiring is a run-time/ \
             STATE concern, out of this AOT-emission task's own scope -- see this module's \
             doc comment)",
            path.display().to_string().as_str(),
        )));
    }

    let body = instance_body_of(&elaborated, top_id, path)?;
    let processes = discover_processes(&body, &comb_sensitivity).map_err(RunError::Lowering)?;
    let signals = signal_slots_of(&body)?;

    let mut plans = Vec::with_capacity(processes.len());
    for proc in &processes {
        let root = vj_sim::process::cfg::proc_body(&proc.sym).ok_or_else(|| {
            RunError::Lowering(LoweringError::unsupported_at(
                "procedural block has no body statement",
                &proc.sym,
            ))
        })?;
        let cfg = build_cfg(&root, loop_kind_of(proc.kind))
            .map_err(|e| sim_lowering_to_run_error(e, path))?;
        if contains_unschedulable_fork_join(&cfg) {
            return Err(RunError::Lowering(LoweringError::unsupported(
                "fork/join has no scheduling semantics yet -- construction-only this wave",
                path.display().to_string().as_str(),
            )));
        }
        let frame = compute_liveness(&cfg).map_err(|e| sim_lowering_to_run_error(e, path))?;
        let segments = lower_to_segments(&cfg, &frame);
        let mut plan = build_lowering_plan(
            format!("vj_proc_{}", plans.len()),
            proc.sym,
            segments,
            frame,
            signals.clone(),
        );
        // Review fix (Task 9, CG-101 r2): `top_id` is this function's own
        // real (and, per the check above, only) elaborated `InstanceId` --
        // every process shares it, which is exactly what
        // `pipeline::side_tables_for_plans` groups by to derive
        // `canonical_process_id`.
        plan.owning_instance = top_id;
        plans.push(plan);
    }

    let (_names, _count) = compile_to_object(&plans, out_obj).map_err(RunError::Codegen)?;

    AotBackend::for_host()
        .map_err(RunError::Codegen)?
        .link(&[out_obj.to_path_buf()], runtime_rlib, out_so)
        .map_err(RunError::Codegen)
}

/// Convenience wrapper over [`build_aot_so`]: writes the object file to a
/// throwaway path next to `out_so` (`<out_so>.o`) -- the CLI's own
/// `build-aot` subcommand never needs to name the intermediate object
/// itself.
pub fn build_aot_so_default_obj(
    path: &Path,
    runtime_rlib: &Path,
    out_so: &Path,
) -> Result<AotKernelHandle, RunError> {
    let mut obj_path: PathBuf = out_so.to_path_buf();
    obj_path.set_extension("o");
    build_aot_so(path, &obj_path, runtime_rlib, out_so)
}