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",
}
}
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct DbItem {
pub kind: ItemKind,
pub name: String,
pub at_least: u64,
pub weight: i64,
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)
}
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"))
}