use std::collections::{HashMap, HashSet};
use sv_lang::kinds::{ExpressionKind, SymbolKind};
use sv_lang::{ConstantValue, EvalSession, Expression, ScriptSession, Symbol, SyntaxTree};
use vj_sdf::ast::DeltaMode;
use vj_sim::netlist::InstanceId;
use vj_sim::netlist::specify::{CheckArgs, TimingModel};
use vj_sim::symid::{SymbolId, SymbolUid};
#[derive(Clone, Copy, Debug, PartialEq)]
pub(crate) enum Source {
Real(f64),
Integer(i64),
}
impl Source {
fn as_f64(self) -> f64 {
match self {
Source::Real(f) => f,
Source::Integer(i) => i as f64,
}
}
}
#[derive(Clone, Debug)]
pub(crate) struct Specparam {
pub(crate) name: String,
pub(crate) uid: SymbolId,
key: Option<String>,
source: Source,
refused: Option<String>,
annotated: Option<f64>,
derived_from: Vec<String>,
}
impl Specparam {
pub(crate) fn current(&self) -> f64 {
self.annotated.unwrap_or_else(|| self.source.as_f64())
}
pub(crate) fn annotate(&mut self, mode: DeltaMode, value: f64) -> Result<f64, String> {
if let Some(why) = &self.refused {
return Err(format!("`{}` {why}", self.name));
}
let new = match mode {
DeltaMode::Absolute => value,
DeltaMode::Increment => self.current() + value,
};
if !new.is_finite() {
return Err(format!(
"`{}` would become {new}, which is not finite",
self.name
));
}
let key = self
.key
.as_deref()
.ok_or_else(|| format!("`{}` has no run-time key (internal)", self.name))?;
vj_runtime::specparam::set(key, new);
self.annotated = Some(new);
Ok(new)
}
pub(crate) fn uid(&self) -> &SymbolId {
&self.uid
}
pub(crate) fn derived_from(&self) -> &[String] {
&self.derived_from
}
fn declaration(&self) -> Option<String> {
if let Some(f) = self.annotated {
return real_declaration(&self.name, f);
}
match self.source {
Source::Real(f) => real_declaration(&self.name, f),
Source::Integer(i) => Some(integer_declaration(&self.name, i)),
}
}
}
fn real_declaration(name: &str, f: f64) -> Option<String> {
f.is_finite().then(|| format!("real {name} = {f:?};"))
}
fn integer_declaration(name: &str, i: i64) -> String {
format!("longint {name} = 64'sh{:x};", i as u64)
}
#[derive(Clone, Debug)]
pub(crate) struct Dependent {
pub(crate) texts: Result<Vec<String>, String>,
pub(crate) refs: Vec<String>,
}
impl Dependent {
pub(crate) fn reads_any(&self, names: &[String]) -> bool {
self.refs.iter().any(|r| names.contains(r))
}
}
#[derive(Clone, Debug)]
pub(crate) struct PathDep {
pub(crate) indices: Vec<usize>,
pub(crate) dep: Dependent,
}
#[derive(Clone, Debug)]
pub(crate) struct LimitDep {
pub(crate) entry: usize,
pub(crate) slot: Option<usize>,
pub(crate) dep: Dependent,
}
#[derive(Clone, Debug)]
pub(crate) struct InstLabels {
pub(crate) unit_exp: i32,
params: Vec<(String, ConstantValue)>,
pub(crate) specparams: Vec<Specparam>,
pub(crate) paths: Vec<PathDep>,
pub(crate) limits: Vec<LimitDep>,
planned_reads: HashSet<String>,
}
impl InstLabels {
pub(crate) fn specparam_mut(&mut self, name: &str) -> Option<&mut Specparam> {
self.specparams.iter_mut().find(|s| s.name == name)
}
pub(crate) fn read_by_planned_delay(&self, name: &str) -> bool {
self.planned_reads.contains(name)
}
#[cfg(test)]
pub(crate) fn evaluate(&self, texts: &[String]) -> Result<Vec<f64>, String> {
self.evaluate_all(&[texts])?.remove(0)
}
pub(crate) fn evaluate_all(
&self,
jobs: &[&[String]],
) -> Result<Vec<Result<Vec<f64>, String>>, String> {
let mut session = ScriptSession::new().map_err(|e| format!("{e:?}"))?;
for (name, v) in &self.params {
let decl = match v {
ConstantValue::Real(f) => real_declaration(name, *f),
other => other.as_i64().map(|i| integer_declaration(name, i)),
};
if let Some(decl) = decl {
session.eval(&decl);
}
}
for sp in &self.specparams {
if let Some(decl) = sp.declaration() {
session.eval(&decl);
}
}
Ok(jobs
.iter()
.map(|texts| {
texts
.iter()
.map(|text| {
let value = session.eval(&format!("(\n{text}\n)")).ok_or_else(|| {
format!(
"`{}` is not a constant expression over literals, \
specparams and parameters",
text.trim()
)
})?;
match value {
ConstantValue::Real(f) => Ok(f),
other => other
.as_i64()
.map(|i| i as f64)
.ok_or_else(|| format!("`{}` is not a number", text.trim())),
}
})
.collect()
})
.collect())
}
}
#[derive(Default)]
pub(crate) struct LabelPlan {
instances: HashMap<InstanceId, InstLabels>,
}
impl LabelPlan {
pub(crate) fn get_mut(&mut self, id: InstanceId) -> Option<&mut InstLabels> {
self.instances.get_mut(&id)
}
#[cfg(test)]
pub(crate) fn insert(&mut self, id: InstanceId, labels: InstLabels) {
self.instances.insert(id, labels);
}
}
#[cfg(test)]
impl InstLabels {
pub(crate) fn for_test(unit_exp: i32, specparams: &[(&str, f64)]) -> InstLabels {
InstLabels {
unit_exp,
params: Vec::new(),
specparams: specparams
.iter()
.map(|&(name, v)| Specparam {
name: name.to_string(),
uid: SymbolId::synthetic(&format!("test.{name}")),
key: Some(format!("test.{name}")),
source: Source::Real(v),
refused: None,
annotated: None,
derived_from: Vec::new(),
})
.collect(),
paths: Vec::new(),
limits: Vec::new(),
planned_reads: HashSet::new(),
}
}
pub(crate) fn specparam_uid(&self, name: &str) -> SymbolId {
self.specparams
.iter()
.find(|s| s.name == name)
.expect("a specparam of this instance")
.uid
.clone()
}
pub(crate) fn reading_in_planned_delay(mut self, name: &str) -> Self {
self.planned_reads.insert(name.to_string());
self
}
pub(crate) fn derived(mut self, name: &str, refs: &[&str]) -> Self {
let sp = self
.specparam_mut(name)
.expect("a specparam of this instance");
sp.derived_from = refs.iter().map(|r| r.to_string()).collect();
self
}
pub(crate) fn refused(mut self, name: &str, why: &str) -> Self {
let sp = self
.specparam_mut(name)
.expect("a specparam of this instance");
sp.refused = Some(why.to_string());
self
}
pub(crate) fn keyed(mut self, prefix: &str) -> Self {
for sp in &mut self.specparams {
sp.key = Some(format!("{prefix}.{}", sp.name));
}
self
}
pub(crate) fn with_path(mut self, indices: Vec<usize>, texts: &[&str], refs: &[&str]) -> Self {
self.paths.push(PathDep {
indices,
dep: Dependent {
texts: Ok(texts.iter().map(|t| t.to_string()).collect()),
refs: refs.iter().map(|r| r.to_string()).collect(),
},
});
self
}
pub(crate) fn with_limit(
mut self,
entry: usize,
slot: Option<usize>,
text: &str,
refs: &[&str],
) -> Self {
self.limits.push(LimitDep {
entry,
slot,
dep: Dependent {
texts: Ok(vec![text.to_string()]),
refs: refs.iter().map(|r| r.to_string()).collect(),
},
});
self
}
}
fn typical<'d>(e: Expression<'d>) -> Option<Expression<'d>> {
if e.kind() == ExpressionKind::MinTypMax {
e.children()
.into_iter()
.filter_map(|n| n.as_expression())
.nth(1)
} else {
Some(e)
}
}
fn dependent<'d>(
exprs: impl Iterator<Item = Expression<'d>>,
trees: &[SyntaxTree],
) -> Option<Dependent> {
let mut refs: Vec<String> = Vec::new();
let mut texts: Result<Vec<String>, String> = Ok(Vec::new());
for e in exprs {
let Some(e) = typical(e) else {
texts = Err("a min:typ:max value with no typical member".to_string());
continue;
};
for sp in vj_codegen::timescale::specparam_refs(e) {
if !refs.iter().any(|r| r == sp.name()) {
refs.push(sp.name().to_string());
}
}
let text = trees.iter().find_map(|t| e.syntax(t)).map(|n| n.text());
match (&mut texts, text) {
(Ok(v), Some(t)) => v.push(t),
(Ok(_), None) => texts = Err("the expression has no source text".to_string()),
(Err(_), _) => {}
}
}
(!refs.is_empty()).then_some(Dependent { texts, refs })
}
fn source_of(v: Option<ConstantValue>) -> Result<Source, String> {
match v {
Some(ConstantValue::Real(f)) if f.is_finite() => Ok(Source::Real(f)),
Some(ConstantValue::Real(_)) => Err("is not a finite number".to_string()),
Some(other) => match other.as_i64() {
Some(i) => Ok(Source::Integer(i)),
None => Err(
"is not a number of at most 64 bits with no x or z (a string, a wide or a \
4-state specparam cannot be annotated)"
.to_string(),
),
},
None => Err("has no constant value".to_string()),
}
}
pub(crate) fn collect<'d>(
eval: &EvalSession<'d>,
infos: &[(InstanceId, Symbol<'d>)],
model: &TimingModel,
) -> LabelPlan {
let trees = eval.design().syntax_trees();
let path_symbols = crate::specify::timing_path_symbols(infos);
let check_symbols = crate::timingcheck::timing_check_symbols(infos);
let mut instances = HashMap::new();
for &(id, body) in infos {
let mut params = Vec::new();
let mut specparams: Vec<Specparam> = Vec::new();
let mut planned_reads: HashSet<String> = HashSet::new();
let note_specparam = |s: Symbol<'d>, specparams: &mut Vec<Specparam>| {
let name = s.name().to_string();
let key = vj_codegen::timescale::specparam_key(&s);
let value = model
.specparam(id, &s.uid())
.cloned()
.or_else(|| s.constant_value());
let (source, mut refused) = match source_of(value) {
Ok(src) => (src, None),
Err(why) => (Source::Integer(0), Some(why)),
};
if name.starts_with("PATHPULSE$") {
refused = Some(
"is a PATHPULSE$ specparam, whose pulse limits are not re-evaluated"
.to_string(),
);
} else if key.is_none() {
refused = Some("is outside any module instance".to_string());
}
let mut derived_from = Vec::new();
if let Some(init) = s.initializer() {
derived_from.extend(
vj_codegen::timescale::specparam_refs(init)
.into_iter()
.map(|r| r.name().to_string()),
);
}
specparams.push(Specparam {
name,
uid: s.uid(),
key,
source,
refused,
annotated: None,
derived_from,
});
};
for m in vj_sim::netlist::module_items(&body) {
match m.kind() {
SymbolKind::Specparam => note_specparam(m, &mut specparams),
SymbolKind::SpecifyBlock => {
for inner in m.members() {
if inner.kind() == SymbolKind::Specparam {
note_specparam(inner, &mut specparams);
}
}
}
SymbolKind::Parameter => {
if let Some(v) = m.constant_value() {
params.push((m.name().to_string(), v));
}
}
SymbolKind::PrimitiveInstance => {
note_planned_reads(m.primitive_instance_delay(), &mut planned_reads)
}
SymbolKind::ContinuousAssign => {
note_planned_reads(m.continuous_assign_delay(), &mut planned_reads)
}
SymbolKind::Net => note_planned_reads(m.net_delay(), &mut planned_reads),
_ => {}
}
}
let mut paths = Vec::new();
for &(pid, sym) in &path_symbols {
if pid != id {
continue;
}
let Some(dep) = dependent(sym.timing_path_delays(), &trees) else {
continue;
};
let indices: Vec<usize> = model
.paths
.iter()
.enumerate()
.filter(|(_, p)| p.instance == id && p.path_uid == sym.uid())
.map(|(k, _)| k)
.collect();
paths.push(PathDep { indices, dep });
}
let mut limits = Vec::new();
for (entry, e) in model.timing_checks.iter().enumerate() {
if e.instance != id {
continue;
}
let Some(sym) = check_symbols.get(&e.check_uid) else {
continue;
};
let args: Vec<_> = sym.system_timing_check_arguments().collect();
let is_width = matches!(e.args, CheckArgs::Width(_));
let mut owned = e.args.clone();
for (k, limit) in owned.limits_mut().iter().enumerate() {
let Some(expr) = args.get(limit.arg_index as usize).and_then(|a| a.expr) else {
continue;
};
if let Some(dep) = dependent(std::iter::once(expr), &trees) {
let slot = (!(is_width && k == 1)).then_some(k);
limits.push(LimitDep { entry, slot, dep });
}
}
}
instances.insert(
id,
InstLabels {
unit_exp: vj_codegen::timescale::unit_exp(&body),
params,
specparams,
paths,
limits,
planned_reads,
},
);
}
LabelPlan { instances }
}
fn note_planned_reads(delay: Option<sv_lang::SemNode<'_>>, out: &mut HashSet<String>) {
let Some(tc) = delay.and_then(|d| d.as_timing_control()) else {
return;
};
for e in tc.children().into_iter().filter_map(|n| n.as_expression()) {
for sp in vj_codegen::timescale::specparam_refs(e) {
out.insert(sp.name().to_string());
}
}
}
#[cfg(test)]
mod tests {
use super::*;
fn labels(unit_exp: i32, sp: &[(&str, f64)]) -> InstLabels {
InstLabels::for_test(unit_exp, sp)
}
#[test]
fn absolute_replaces_and_increment_adds() {
let mut l = labels(-9, &[("cap", 0.0)]);
let sp = l.specparam_mut("cap").unwrap();
assert_eq!(sp.annotate(DeltaMode::Absolute, 2.5).unwrap(), 2.5);
assert_eq!(sp.annotate(DeltaMode::Increment, 0.5).unwrap(), 3.0);
assert_eq!(sp.current(), 3.0);
assert_eq!(sp.annotate(DeltaMode::Absolute, 1.0).unwrap(), 1.0);
vj_runtime::specparam::clear();
}
#[test]
fn annotation_is_published_to_the_kernels_under_the_key() {
vj_runtime::specparam::clear();
let mut l = labels(-9, &[("dhigh", 0.0)]);
assert!(vj_runtime::specparam::get("test.dhigh").is_none());
l.specparam_mut("dhigh")
.unwrap()
.annotate(DeltaMode::Absolute, 60.0)
.unwrap();
assert_eq!(vj_runtime::specparam::get("test.dhigh"), Some(60.0));
vj_runtime::specparam::clear();
}
#[test]
fn a_non_finite_result_is_refused_and_leaves_the_value() {
let mut l = labels(-9, &[("a", 1.0)]);
let sp = l.specparam_mut("a").unwrap();
assert!(sp.annotate(DeltaMode::Absolute, f64::INFINITY).is_err());
assert!(sp.annotate(DeltaMode::Increment, f64::MAX * 2.0).is_err());
assert_eq!(sp.current(), 1.0);
}
#[test]
fn a_refused_specparam_says_why() {
let mut l = labels(-9, &[("a", 1.0)]);
let sp = l.specparam_mut("a").unwrap();
sp.refused = Some("is a string".to_string());
let err = sp.annotate(DeltaMode::Absolute, 1.0).unwrap_err();
assert!(err.contains("`a` is a string"), "{err}");
assert_eq!(sp.current(), 1.0);
}
#[test]
fn evaluation_sees_the_annotated_value() {
let mut l = labels(-9, &[("cap", 0.0)]);
assert_eq!(l.evaluate(&["1.4 * cap + 0.7".into()]).unwrap(), [0.7]);
l.specparam_mut("cap")
.unwrap()
.annotate(DeltaMode::Absolute, 2.5)
.unwrap();
let v = l.evaluate(&["1.4 * cap + 0.7".into()]).unwrap();
assert!((v[0] - 4.2).abs() < 1e-12, "{v:?}");
vj_runtime::specparam::clear();
}
#[test]
fn evaluation_declares_parameters_and_integral_specparams() {
let mut l = labels(-9, &[]);
l.params.push(("P".into(), ConstantValue::Real(2.0)));
l.specparams.push(Specparam {
name: "n".into(),
uid: SymbolId::synthetic("test.n"),
key: Some("test.n".into()),
source: Source::Integer(7),
refused: None,
annotated: None,
derived_from: Vec::new(),
});
assert_eq!(l.evaluate(&["P * (n / 2)".into()]).unwrap(), [6.0]);
}
#[test]
fn a_trailing_comment_cannot_swallow_the_closing_parenthesis() {
let l = labels(-9, &[("a", 3.0)]);
assert_eq!(l.evaluate(&["a + 1 // note".into()]).unwrap(), [4.0]);
}
#[test]
fn evaluation_failure_names_the_expression() {
let l = labels(-9, &[("a", 3.0)]);
let err = l.evaluate(&["a + undeclared".into()]).unwrap_err();
assert!(err.contains("a + undeclared"), "{err}");
}
#[test]
fn declarations_round_trip_every_i64() {
let l = labels(-9, &[]);
for n in [i64::MIN, -1, 0, 1, 3_000_000_000, i64::MAX] {
let mut sp = labels(-9, &[("x", 0.0)]).specparams.remove(0);
sp.source = Source::Integer(n);
let mut l2 = l.clone();
l2.specparams.push(sp);
let got = l2.evaluate(&["x".into()]).unwrap()[0];
assert_eq!(got, n as f64, "{n}");
}
}
}