use vj_sim::netlist::specify::PulseStyle;
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
pub enum SdfMtm {
Minimum,
#[default]
Typical,
Maximum,
}
#[derive(Clone, Copy, Debug, Default, PartialEq)]
pub struct TimingOptions {
pub per_bit_vectors: bool,
pub negative_timing_checks: bool,
pub reject_limit_pct: Option<f64>,
pub error_limit_pct: Option<f64>,
pub pulse_style: Option<PulseStyle>,
pub showcancelled: Option<bool>,
pub sdf_mtm: Option<SdfMtm>,
}
impl TimingOptions {
pub fn reject_pct(&self) -> f64 {
self.reject_limit_pct.unwrap_or(100.0)
}
pub fn error_pct(&self) -> f64 {
self.error_limit_pct.unwrap_or(100.0)
}
pub fn default_pulse_style(&self) -> PulseStyle {
self.pulse_style.unwrap_or_default()
}
pub fn default_showcancelled(&self) -> bool {
self.showcancelled.unwrap_or(false)
}
pub fn tool_control(&self) -> SdfMtm {
self.sdf_mtm.unwrap_or_default()
}
pub fn parse_arg(&mut self, arg: &str) -> Result<bool, String> {
let (name, value) = match arg.split_once('=') {
Some((n, v)) => (n, Some(v)),
None => (arg, None),
};
match name {
"--timing-check-per-bit" => {
no_value(name, value)?;
self.per_bit_vectors = true;
}
"--enable-negative-timing-checks" => {
no_value(name, value)?;
self.negative_timing_checks = true;
}
"--timing-reject-limit-pct" => {
set_once(name, &mut self.reject_limit_pct, percent(name, value)?)?;
}
"--timing-error-limit-pct" => {
set_once(name, &mut self.error_limit_pct, percent(name, value)?)?;
}
"--pulse-style" => {
let style = match required(name, value)? {
"on-event" => PulseStyle::OnEvent,
"on-detect" => PulseStyle::OnDetect,
other => {
return Err(format!(
"{name}={other}: expected on-event or on-detect (LRM 30.7.4.1)"
));
}
};
set_once(name, &mut self.pulse_style, style)?;
}
"--showcancelled" => {
no_value(name, value)?;
set_once(name, &mut self.showcancelled, true)?;
}
"--noshowcancelled" => {
no_value(name, value)?;
set_once(name, &mut self.showcancelled, false)?;
}
"--sdf-mtm" => {
let mtm = match required(name, value)?.to_ascii_lowercase().as_str() {
"minimum" => SdfMtm::Minimum,
"typical" => SdfMtm::Typical,
"maximum" => SdfMtm::Maximum,
other => {
return Err(format!(
"{name}={other}: expected minimum, typical or maximum (LRM Table 32-5)"
));
}
};
set_once(name, &mut self.sdf_mtm, mtm)?;
}
_ => return Ok(false),
}
Ok(true)
}
}
impl TimingOptions {
pub fn to_image(&self) -> vj_sim::image::TimingOptionsImage {
vj_sim::image::TimingOptionsImage {
per_bit_vectors: self.per_bit_vectors,
negative_timing_checks: self.negative_timing_checks,
reject_limit_pct: self.reject_limit_pct.map(f64::to_bits),
error_limit_pct: self.error_limit_pct.map(f64::to_bits),
pulse_style: self.pulse_style.map(pulse_style_ordinal),
showcancelled: self.showcancelled,
sdf_mtm: self.sdf_mtm.map(sdf_mtm_ordinal),
}
}
pub fn conflicts_with(&self, built: &vj_sim::image::TimingOptionsImage) -> Vec<String> {
let mut out = Vec::new();
if self.per_bit_vectors && !built.per_bit_vectors {
out.push("--timing-check-per-bit: the library was built without it".to_string());
}
if self.negative_timing_checks && !built.negative_timing_checks {
out.push(
"--enable-negative-timing-checks: the library was built without it".to_string(),
);
}
if let Some(pct) = self.reject_limit_pct {
let got = built.reject_limit_pct.map(f64::from_bits).unwrap_or(100.0);
if pct != got {
out.push(format!(
"--timing-reject-limit-pct={pct}: the library was built with {got}"
));
}
}
if let Some(pct) = self.error_limit_pct {
let got = built.error_limit_pct.map(f64::from_bits).unwrap_or(100.0);
if pct != got {
out.push(format!(
"--timing-error-limit-pct={pct}: the library was built with {got}"
));
}
}
if let Some(style) = self.pulse_style {
let want = pulse_style_ordinal(style);
let got = built.pulse_style.unwrap_or(0);
if want != got {
out.push(format!(
"--pulse-style={}: the library was built with --pulse-style={}",
pulse_style_name(want),
pulse_style_name(got)
));
}
}
if let Some(sc) = self.showcancelled {
let got = built.showcancelled.unwrap_or(false);
if sc != got {
out.push(format!(
"--{}showcancelled: the library was built with --{}showcancelled",
if sc { "" } else { "no" },
if got { "" } else { "no" },
));
}
}
if let Some(mtm) = self.sdf_mtm {
let want = sdf_mtm_ordinal(mtm);
let got = built.sdf_mtm.unwrap_or(1);
if want != got {
out.push(format!(
"--sdf-mtm={}: the library was built with --sdf-mtm={}",
sdf_mtm_name(want),
sdf_mtm_name(got)
));
}
}
out
}
}
fn pulse_style_ordinal(s: PulseStyle) -> u8 {
match s {
PulseStyle::OnEvent => 0,
PulseStyle::OnDetect => 1,
}
}
fn pulse_style_name(v: u8) -> &'static str {
if v == 0 { "on-event" } else { "on-detect" }
}
fn sdf_mtm_ordinal(m: SdfMtm) -> u8 {
match m {
SdfMtm::Minimum => 0,
SdfMtm::Typical => 1,
SdfMtm::Maximum => 2,
}
}
fn sdf_mtm_name(v: u8) -> &'static str {
match v {
0 => "minimum",
2 => "maximum",
_ => "typical",
}
}
fn no_value(name: &str, value: Option<&str>) -> Result<(), String> {
match value {
None => Ok(()),
Some(_) => Err(format!("{name} takes no value")),
}
}
fn required<'a>(name: &str, value: Option<&'a str>) -> Result<&'a str, String> {
value.ok_or_else(|| format!("{name} needs a value: {name}=VALUE"))
}
fn percent(name: &str, value: Option<&str>) -> Result<f64, String> {
let text = required(name, value)?;
let pct: f64 = text
.parse()
.map_err(|_| format!("{name}={text}: not a number"))?;
if !pct.is_finite() || !(0.0..=100.0).contains(&pct) {
return Err(format!(
"{name}={text}: a percentage from 0 to 100 is required"
));
}
Ok(pct)
}
fn set_once<T: PartialEq>(name: &str, slot: &mut Option<T>, new: T) -> Result<(), String> {
match slot {
Some(old) if *old != new => Err(format!(
"{name} (or its opposite) was given twice with different settings"
)),
_ => {
*slot = Some(new);
Ok(())
}
}
}
#[cfg(test)]
mod tests {
use super::*;
fn parsed(args: &[&str]) -> Result<TimingOptions, String> {
let mut o = TimingOptions::default();
for a in args {
assert!(o.parse_arg(a)?, "{a} should be a timing option");
}
Ok(o)
}
#[test]
fn default_is_the_lrm_behaviour() {
let o = TimingOptions::default();
assert_eq!((o.reject_pct(), o.error_pct()), (100.0, 100.0));
assert_eq!(o.default_pulse_style(), PulseStyle::OnEvent);
assert!(!o.default_showcancelled());
assert_eq!(o.tool_control(), SdfMtm::Typical);
assert!(!o.per_bit_vectors && !o.negative_timing_checks);
}
#[test]
fn every_flag_parses_into_its_own_field() {
let o = parsed(&["--timing-check-per-bit"]).unwrap();
assert!(o.per_bit_vectors);
let o = parsed(&["--enable-negative-timing-checks"]).unwrap();
assert!(o.negative_timing_checks);
let o = parsed(&["--timing-reject-limit-pct=40"]).unwrap();
assert_eq!((o.reject_pct(), o.error_pct()), (40.0, 100.0));
let o = parsed(&["--timing-error-limit-pct=12.5"]).unwrap();
assert_eq!((o.reject_pct(), o.error_pct()), (100.0, 12.5));
let o = parsed(&["--pulse-style=on-detect"]).unwrap();
assert_eq!(o.default_pulse_style(), PulseStyle::OnDetect);
let o = parsed(&["--pulse-style=on-event"]).unwrap();
assert_eq!(o.pulse_style, Some(PulseStyle::OnEvent));
let o = parsed(&["--showcancelled"]).unwrap();
assert_eq!(o.showcancelled, Some(true));
let o = parsed(&["--noshowcancelled"]).unwrap();
assert_eq!(o.showcancelled, Some(false));
for (text, want) in [
("minimum", SdfMtm::Minimum),
("TYPICAL", SdfMtm::Typical),
("Maximum", SdfMtm::Maximum),
] {
let o = parsed(&[&format!("--sdf-mtm={text}")]).unwrap();
assert_eq!(o.tool_control(), want, "{text}");
}
}
#[test]
fn percentages_accept_the_closed_range_and_nothing_else() {
assert_eq!(
parsed(&["--timing-reject-limit-pct=0"])
.unwrap()
.reject_pct(),
0.0
);
assert_eq!(
parsed(&["--timing-error-limit-pct=100"])
.unwrap()
.error_pct(),
100.0
);
for bad in [
"--timing-reject-limit-pct=-1",
"--timing-reject-limit-pct=100.5",
"--timing-error-limit-pct=nan",
"--timing-error-limit-pct=inf",
"--timing-error-limit-pct=abc",
"--timing-error-limit-pct=",
] {
assert!(parsed(&[bad]).is_err(), "{bad} must be refused");
}
}
#[test]
fn a_missing_or_unwanted_value_is_an_error() {
for bad in [
"--timing-reject-limit-pct",
"--timing-error-limit-pct",
"--pulse-style",
"--sdf-mtm",
"--showcancelled=1",
"--noshowcancelled=1",
"--timing-check-per-bit=1",
"--enable-negative-timing-checks=1",
"--pulse-style=ondetect",
"--sdf-mtm=tool_control",
"--sdf-mtm=fast",
] {
assert!(parsed(&[bad]).is_err(), "{bad} must be refused");
}
}
#[test]
fn contradictory_repeats_are_errors_and_agreeing_repeats_are_fine() {
assert!(parsed(&["--showcancelled", "--noshowcancelled"]).is_err());
assert!(parsed(&["--pulse-style=on-event", "--pulse-style=on-detect"]).is_err());
assert!(
parsed(&[
"--timing-reject-limit-pct=10",
"--timing-reject-limit-pct=20"
])
.is_err()
);
assert!(parsed(&["--sdf-mtm=minimum", "--sdf-mtm=maximum"]).is_err());
let o = parsed(&["--showcancelled", "--showcancelled"]).unwrap();
assert_eq!(o.showcancelled, Some(true));
}
#[test]
fn to_image_round_trips_every_field() {
let o = parsed(&[
"--timing-check-per-bit",
"--enable-negative-timing-checks",
"--timing-reject-limit-pct=40",
"--timing-error-limit-pct=60",
"--pulse-style=on-detect",
"--showcancelled",
"--sdf-mtm=minimum",
])
.unwrap();
let img = o.to_image();
assert!(img.per_bit_vectors && img.negative_timing_checks);
assert_eq!(img.reject_limit_pct, Some(40.0f64.to_bits()));
assert_eq!(img.error_limit_pct, Some(60.0f64.to_bits()));
assert_eq!(img.pulse_style, Some(1));
assert_eq!(img.showcancelled, Some(true));
assert_eq!(img.sdf_mtm, Some(0));
}
#[test]
fn default_options_never_conflict_with_a_non_default_image() {
let built = TimingOptions::default().to_image();
assert!(TimingOptions::default().conflicts_with(&built).is_empty());
let non_default = parsed(&["--pulse-style=on-detect", "--sdf-mtm=minimum"])
.unwrap()
.to_image();
assert!(
TimingOptions::default()
.conflicts_with(&non_default)
.is_empty(),
"an unmentioned flag never conflicts, whatever the build used"
);
}
#[test]
fn an_agreeing_flag_never_conflicts() {
let built = parsed(&["--pulse-style=on-detect"]).unwrap().to_image();
let asked = parsed(&["--pulse-style=on-detect"]).unwrap();
assert!(asked.conflicts_with(&built).is_empty());
}
#[test]
fn a_disagreeing_flag_names_both_values() {
let built = parsed(&["--pulse-style=on-event"]).unwrap().to_image();
let asked = parsed(&["--pulse-style=on-detect"]).unwrap();
let conflicts = asked.conflicts_with(&built);
assert_eq!(conflicts.len(), 1);
assert!(conflicts[0].contains("--pulse-style"));
assert!(conflicts[0].contains("on-event"));
assert!(conflicts[0].contains("on-detect"));
let built = parsed(&["--sdf-mtm=minimum"]).unwrap().to_image();
let asked = parsed(&["--sdf-mtm=maximum"]).unwrap();
let conflicts = asked.conflicts_with(&built);
assert_eq!(conflicts.len(), 1);
assert!(conflicts[0].contains("minimum") && conflicts[0].contains("maximum"));
let built = TimingOptions::default().to_image();
let asked = parsed(&["--enable-negative-timing-checks"]).unwrap();
let conflicts = asked.conflicts_with(&built);
assert_eq!(conflicts.len(), 1);
assert!(conflicts[0].contains("--enable-negative-timing-checks"));
}
#[test]
fn an_argument_that_is_not_a_timing_option_is_left_to_the_caller() {
let mut o = TimingOptions::default();
assert_eq!(o.parse_arg("+plusarg"), Ok(false));
assert_eq!(o.parse_arg("--sv-lib"), Ok(false));
assert_eq!(o.parse_arg("design.c"), Ok(false));
assert_eq!(o, TimingOptions::default());
}
}