vj 0.2.0

vj: a SystemVerilog (IEEE 1800-2023) simulator built on slang and LLVM.
//! `$sdf_annotate` in `vj build-aot` (IEEE 1800-2023 clause 32): each call whose arguments
//! are literals is resolved at build time into an [`SdfCall`] that the built image replays when
//! the call executes (`vj_exec::timing::sdf`).
//!
//! The calls are resolved by the annotator `vj run` runs, on a copy of the design's live
//! timing cells, one call after another in lowering order; what each call changed (module path
//! delays and limits, primitive `DEVICE` delays, timing check limits, `LABEL` specparams) is the
//! difference of those cells before and after it. The `PORT`, `NETDELAY` and `INTERCONNECT`
//! annotations of port connection wires are the ones the annotator made on the design's wire
//! graph, in order (the image's interconnect delay lines replay them).
//!
//! A call this cannot resolve is refused by name: an argument that is not a string literal
//! (`sdf-annotate-dynamic-path`: the file is read at build time).

use vj_codegen::systask::SdfCallSite;
use vj_exec::timing::TimingLive;
use vj_exec::timing::sdf::{
    Change, SdfCall, SdfDelta, SdfKey, SdfOutcome, SdfPlan, SpecparamChange, hash_file,
};
use vj_runtime::sdf_annotate::SdfAnnotateCall;

use crate::interconnect::SharedTable;
use crate::sdf_apply::Annotator;

/// A call that cannot be built into an image: the refusal's feature name and its reason.
pub(crate) type SdfRefusal = (&'static str, String);

/// The cells `$sdf_annotate` writes, as a comparable value.
struct Cells {
    paths: Vec<vj_exec::timing::specify::PathTimes>,
    devices: Vec<vj_exec::timing::Delays>,
    checks: Vec<Option<vj_exec::timing::check::CheckSpec>>,
    specparams: Vec<(String, f64)>,
}

impl Cells {
    fn read(live: &TimingLive, checks: usize) -> Cells {
        Cells {
            paths: live.paths.lock().unwrap().clone(),
            devices: live
                .prim
                .device
                .iter()
                .map(|d| *d.lock().unwrap())
                .collect(),
            checks: (0..checks).map(|i| live.checks.declared_spec(i)).collect(),
            specparams: vj_runtime::specparam::snapshot(),
        }
    }
}

/// The call `site` makes at run time (an option given as `""` is an omitted one, as the
/// runtime decodes it).
fn call_of(site: &SdfCallSite, file: String) -> Option<SdfAnnotateCall> {
    let opt = |k: usize| -> Option<Option<String>> {
        site.options[k].clone().map(|o| o.filter(|s| !s.is_empty()))
    };
    Some(SdfAnnotateCall {
        file,
        scope: site.scope.clone(),
        config_file: opt(0)?,
        log_file: opt(1)?,
        mtm_spec: opt(2)?,
        scale_factors: opt(3)?,
        scale_type: opt(4)?,
    })
}

/// Resolves every call of `sites` with `annotator`, which works on `live`.
pub(crate) fn resolve(
    sites: &[SdfCallSite],
    live: &TimingLive,
    wires: &SharedTable,
    checks: usize,
    mut annotator: Annotator,
) -> Result<SdfPlan, SdfRefusal> {
    vj_runtime::specparam::clear();
    let mut calls = Vec::new();
    for site in sites {
        let dynamic = || {
            (
                "sdf-annotate-dynamic-path",
                format!(
                    "$sdf_annotate in \"{}\" has an argument that is not a string literal; \
                     build-aot reads the SDF file at build time",
                    site.scope
                ),
            )
        };
        let file = site.file.clone().ok_or_else(dynamic)?;
        let call = call_of(site, file).ok_or_else(dynamic)?;
        wires.lock().unwrap().take_recorded();
        let before = Cells::read(live, checks);
        let outcome = match annotator.apply(&call) {
            Err(msg) => SdfOutcome::Failed(msg),
            Ok(warnings) => {
                let after = Cells::read(live, checks);
                SdfOutcome::Applied(SdfDelta {
                    warnings,
                    log: annotator.take_captured_log(),
                    paths: diff(&before.paths, &after.paths),
                    devices: diff(&before.devices, &after.devices),
                    checks: before
                        .checks
                        .iter()
                        .zip(&after.checks)
                        .enumerate()
                        .filter_map(|(i, (b, a))| match (b, a) {
                            (Some(b), Some(a)) if b != a => Some(Change {
                                index: i as u32,
                                before: *b,
                                after: *a,
                            }),
                            _ => None,
                        })
                        .collect(),
                    specparams: specparam_changes(&before.specparams, &after.specparams),
                    interconnect: wires.lock().unwrap().take_recorded(),
                })
            }
        };
        calls.push(SdfCall {
            key: SdfKey {
                file: call.file.clone(),
                scope: call.scope.clone(),
                config_file: call.config_file.clone(),
                log_file: call.log_file.clone(),
                mtm_spec: call.mtm_spec.clone(),
                scale_factors: call.scale_factors.clone(),
                scale_type: call.scale_type.clone(),
            },
            file_hash: hash_file(&call.file),
            config_hash: call.config_file.as_deref().and_then(hash_file),
            outcome,
        });
    }
    vj_runtime::specparam::clear();
    Ok(SdfPlan { calls })
}

fn diff<T: Copy + PartialEq>(before: &[T], after: &[T]) -> Vec<Change<T>> {
    before
        .iter()
        .zip(after)
        .enumerate()
        .filter(|(_, (b, a))| b != a)
        .map(|(i, (b, a))| Change {
            index: i as u32,
            before: *b,
            after: *a,
        })
        .collect()
}

fn specparam_changes(before: &[(String, f64)], after: &[(String, f64)]) -> Vec<SpecparamChange> {
    after
        .iter()
        .filter_map(|(path, value)| {
            let old = before.iter().find(|(p, _)| p == path).map(|(_, v)| *v);
            (old.map(f64::to_bits) != Some(value.to_bits())).then(|| SpecparamChange {
                path: path.clone(),
                before: old.map(f64::to_bits),
                after: value.to_bits(),
            })
        })
        .collect()
}