vj-sim 0.3.0

Netlist, scheduler and process model of the vj SystemVerilog simulator.
Documentation
//! The coverage database of `$set_coverage_db_name` / `$load_coverage_db`
//! (IEEE 1800-2023 19.9).
//!
//! The LRM fixes what the database holds ("the cumulative coverage information
//! for all coverage group types") and nothing about its format, so this is a
//! documented line-oriented text format, not UCIS:
//!
//! ```text
//! vj-coverage-db 1
//! covergroup <name>
//! coverpoint <name> at_least=<n> weight=<n>
//! bin <name> <hits>
//! cross <name> at_least=<n> weight=<n>
//! bin <name> <hits>
//! ```
//!
//! A `bin` line belongs to the `coverpoint` / `cross` above it, which belongs to
//! the `covergroup` above that. Names are percent-escaped (every byte outside
//! printable ASCII, `%` and space becomes `%XX`; an empty name is a lone `%`).
//! Blank lines and lines starting with `#` are ignored. Hit counts of the same
//! bin name add up when two databases are merged, and the larger `at_least` and
//! `weight` win, which is exactly how 19.11.3 merges instances.

use super::value::CovError;

pub const HEADER: &str = "vj-coverage-db 1";

#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum ItemKind {
    Coverpoint,
    Cross,
}

impl ItemKind {
    fn keyword(self) -> &'static str {
        match self {
            ItemKind::Coverpoint => "coverpoint",
            ItemKind::Cross => "cross",
        }
    }
}

/// The cumulative state of one coverpoint or cross of a covergroup type.
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct DbItem {
    pub kind: ItemKind,
    pub name: String,
    /// The `at_least` a bin needs to count as covered (the largest of the
    /// merged instances).
    pub at_least: u64,
    /// The `option.weight` of the item (the largest of the merged instances).
    pub weight: i64,
    /// The bins by name, with their hit counts.
    pub bins: Vec<(String, u64)>,
}

impl DbItem {
    fn merge(&mut self, other: &DbItem) {
        self.at_least = self.at_least.max(other.at_least);
        self.weight = self.weight.max(other.weight);
        for (name, hits) in &other.bins {
            match self.bins.iter_mut().find(|(n, _)| n == name) {
                Some((_, h)) => *h = h.saturating_add(*hits),
                None => self.bins.push((name.clone(), *hits)),
            }
        }
    }
}

#[derive(Clone, Debug, PartialEq, Eq)]
pub struct DbType {
    pub name: String,
    pub items: Vec<DbItem>,
}

impl DbType {
    pub fn item(&self, kind: ItemKind, name: &str) -> Option<&DbItem> {
        self.items.iter().find(|i| i.kind == kind && i.name == name)
    }

    pub fn merge(&mut self, other: &DbType) {
        for item in &other.items {
            match self
                .items
                .iter_mut()
                .find(|i| i.kind == item.kind && i.name == item.name)
            {
                Some(mine) => mine.merge(item),
                None => self.items.push(item.clone()),
            }
        }
    }
}

#[derive(Clone, Debug, Default, PartialEq, Eq)]
pub struct Db {
    pub types: Vec<DbType>,
}

impl Db {
    pub fn ty(&self, name: &str) -> Option<&DbType> {
        self.types.iter().find(|t| t.name == name)
    }

    /// Adds `other` into `self`: hit counts of equally named bins add up.
    pub fn merge(&mut self, other: &Db) {
        for t in &other.types {
            match self.types.iter_mut().find(|m| m.name == t.name) {
                Some(mine) => mine.merge(t),
                None => self.types.push(t.clone()),
            }
        }
    }

    pub fn render(&self) -> String {
        let mut out = String::new();
        out.push_str(HEADER);
        out.push('\n');
        for t in &self.types {
            out.push_str(&format!("covergroup {}\n", escape(&t.name)));
            for i in &t.items {
                out.push_str(&format!(
                    "{} {} at_least={} weight={}\n",
                    i.kind.keyword(),
                    escape(&i.name),
                    i.at_least,
                    i.weight
                ));
                for (name, hits) in &i.bins {
                    out.push_str(&format!("bin {} {hits}\n", escape(name)));
                }
            }
        }
        out
    }

    pub fn parse(text: &str) -> Result<Db, CovError> {
        let mut db = Db::default();
        let mut seen_header = false;
        for (idx, raw) in text.lines().enumerate() {
            let line = idx + 1;
            let bad = |detail: String| CovError::BadDb { line, detail };
            let trimmed = raw.trim();
            if trimmed.is_empty() || trimmed.starts_with('#') {
                continue;
            }
            if !seen_header {
                if trimmed != HEADER {
                    return Err(bad(format!(
                        "expected the header `{HEADER}`, found `{trimmed}`"
                    )));
                }
                seen_header = true;
                continue;
            }
            let toks: Vec<&str> = trimmed.split_whitespace().collect();
            match toks.as_slice() {
                ["covergroup", name] => {
                    let name = unescape(name).map_err(&bad)?;
                    if db.ty(&name).is_some() {
                        return Err(bad(format!("covergroup `{name}` appears twice")));
                    }
                    db.types.push(DbType {
                        name,
                        items: Vec::new(),
                    });
                }
                [kw @ ("coverpoint" | "cross"), name, at_least, weight] => {
                    let kind = if *kw == "cross" {
                        ItemKind::Cross
                    } else {
                        ItemKind::Coverpoint
                    };
                    let name = unescape(name).map_err(&bad)?;
                    let at_least = field(at_least, "at_least").map_err(&bad)?;
                    let weight = field(weight, "weight").map_err(&bad)?;
                    let Some(ty) = db.types.last_mut() else {
                        return Err(bad(format!("`{kw} {name}` before any covergroup")));
                    };
                    if ty.item(kind, &name).is_some() {
                        return Err(bad(format!(
                            "{kw} `{name}` appears twice in covergroup `{}`",
                            ty.name
                        )));
                    }
                    ty.items.push(DbItem {
                        kind,
                        name,
                        at_least,
                        weight,
                        bins: Vec::new(),
                    });
                }
                ["bin", name, hits] => {
                    let name = unescape(name).map_err(&bad)?;
                    let hits: u64 = hits
                        .parse()
                        .map_err(|_| bad(format!("bad hit count `{hits}`")))?;
                    let Some(item) = db.types.last_mut().and_then(|t| t.items.last_mut()) else {
                        return Err(bad(format!("bin `{name}` before any coverpoint or cross")));
                    };
                    if item.bins.iter().any(|(n, _)| *n == name) {
                        return Err(bad(format!(
                            "bin `{name}` appears twice in `{}`",
                            item.name
                        )));
                    }
                    item.bins.push((name, hits));
                }
                _ => return Err(bad(format!("unrecognised line `{trimmed}`"))),
            }
        }
        if !seen_header {
            return Err(CovError::BadDb {
                line: 1,
                detail: format!("empty file, expected the header `{HEADER}`"),
            });
        }
        Ok(db)
    }
}

fn field<T: std::str::FromStr>(tok: &str, key: &str) -> Result<T, String> {
    tok.strip_prefix(key)
        .and_then(|r| r.strip_prefix('='))
        .and_then(|v| v.parse().ok())
        .ok_or_else(|| format!("expected `{key}=<number>`, found `{tok}`"))
}

fn escape(s: &str) -> String {
    if s.is_empty() {
        return "%".to_string();
    }
    let mut out = String::with_capacity(s.len());
    for &b in s.as_bytes() {
        if b > b' ' && b < 0x7f && b != b'%' {
            out.push(b as char);
        } else {
            out.push_str(&format!("%{b:02X}"));
        }
    }
    out
}

fn unescape(tok: &str) -> Result<String, String> {
    if tok == "%" {
        return Ok(String::new());
    }
    let bytes = tok.as_bytes();
    let mut out = Vec::with_capacity(bytes.len());
    let mut i = 0;
    while i < bytes.len() {
        if bytes[i] == b'%' {
            let hex = tok
                .get(i + 1..i + 3)
                .and_then(|h| u8::from_str_radix(h, 16).ok())
                .ok_or_else(|| format!("bad escape in `{tok}`"))?;
            out.push(hex);
            i += 3;
        } else {
            out.push(bytes[i]);
            i += 1;
        }
    }
    String::from_utf8(out).map_err(|_| format!("`{tok}` is not valid UTF-8 once unescaped"))
}