Skip to main content

vj_sim/covergroup/
db.rs

1//! The coverage database of `$set_coverage_db_name` / `$load_coverage_db`
2//! (IEEE 1800-2023 19.9).
3//!
4//! The LRM fixes what the database holds ("the cumulative coverage information
5//! for all coverage group types") and nothing about its format, so this is a
6//! documented line-oriented text format, not UCIS:
7//!
8//! ```text
9//! vj-coverage-db 1
10//! covergroup <name>
11//! coverpoint <name> at_least=<n> weight=<n>
12//! bin <name> <hits>
13//! cross <name> at_least=<n> weight=<n>
14//! bin <name> <hits>
15//! ```
16//!
17//! A `bin` line belongs to the `coverpoint` / `cross` above it, which belongs to
18//! the `covergroup` above that. Names are percent-escaped (every byte outside
19//! printable ASCII, `%` and space becomes `%XX`; an empty name is a lone `%`).
20//! Blank lines and lines starting with `#` are ignored. Hit counts of the same
21//! bin name add up when two databases are merged, and the larger `at_least` and
22//! `weight` win, which is exactly how 19.11.3 merges instances.
23
24use super::value::CovError;
25
26pub const HEADER: &str = "vj-coverage-db 1";
27
28#[derive(Clone, Copy, Debug, PartialEq, Eq)]
29pub enum ItemKind {
30    Coverpoint,
31    Cross,
32}
33
34impl ItemKind {
35    fn keyword(self) -> &'static str {
36        match self {
37            ItemKind::Coverpoint => "coverpoint",
38            ItemKind::Cross => "cross",
39        }
40    }
41}
42
43/// The cumulative state of one coverpoint or cross of a covergroup type.
44#[derive(Clone, Debug, PartialEq, Eq)]
45pub struct DbItem {
46    pub kind: ItemKind,
47    pub name: String,
48    /// The `at_least` a bin needs to count as covered (the largest of the
49    /// merged instances).
50    pub at_least: u64,
51    /// The `option.weight` of the item (the largest of the merged instances).
52    pub weight: i64,
53    /// The bins by name, with their hit counts.
54    pub bins: Vec<(String, u64)>,
55}
56
57impl DbItem {
58    fn merge(&mut self, other: &DbItem) {
59        self.at_least = self.at_least.max(other.at_least);
60        self.weight = self.weight.max(other.weight);
61        for (name, hits) in &other.bins {
62            match self.bins.iter_mut().find(|(n, _)| n == name) {
63                Some((_, h)) => *h = h.saturating_add(*hits),
64                None => self.bins.push((name.clone(), *hits)),
65            }
66        }
67    }
68}
69
70#[derive(Clone, Debug, PartialEq, Eq)]
71pub struct DbType {
72    pub name: String,
73    pub items: Vec<DbItem>,
74}
75
76impl DbType {
77    pub fn item(&self, kind: ItemKind, name: &str) -> Option<&DbItem> {
78        self.items.iter().find(|i| i.kind == kind && i.name == name)
79    }
80
81    pub fn merge(&mut self, other: &DbType) {
82        for item in &other.items {
83            match self
84                .items
85                .iter_mut()
86                .find(|i| i.kind == item.kind && i.name == item.name)
87            {
88                Some(mine) => mine.merge(item),
89                None => self.items.push(item.clone()),
90            }
91        }
92    }
93}
94
95#[derive(Clone, Debug, Default, PartialEq, Eq)]
96pub struct Db {
97    pub types: Vec<DbType>,
98}
99
100impl Db {
101    pub fn ty(&self, name: &str) -> Option<&DbType> {
102        self.types.iter().find(|t| t.name == name)
103    }
104
105    /// Adds `other` into `self`: hit counts of equally named bins add up.
106    pub fn merge(&mut self, other: &Db) {
107        for t in &other.types {
108            match self.types.iter_mut().find(|m| m.name == t.name) {
109                Some(mine) => mine.merge(t),
110                None => self.types.push(t.clone()),
111            }
112        }
113    }
114
115    pub fn render(&self) -> String {
116        let mut out = String::new();
117        out.push_str(HEADER);
118        out.push('\n');
119        for t in &self.types {
120            out.push_str(&format!("covergroup {}\n", escape(&t.name)));
121            for i in &t.items {
122                out.push_str(&format!(
123                    "{} {} at_least={} weight={}\n",
124                    i.kind.keyword(),
125                    escape(&i.name),
126                    i.at_least,
127                    i.weight
128                ));
129                for (name, hits) in &i.bins {
130                    out.push_str(&format!("bin {} {hits}\n", escape(name)));
131                }
132            }
133        }
134        out
135    }
136
137    pub fn parse(text: &str) -> Result<Db, CovError> {
138        let mut db = Db::default();
139        let mut seen_header = false;
140        for (idx, raw) in text.lines().enumerate() {
141            let line = idx + 1;
142            let bad = |detail: String| CovError::BadDb { line, detail };
143            let trimmed = raw.trim();
144            if trimmed.is_empty() || trimmed.starts_with('#') {
145                continue;
146            }
147            if !seen_header {
148                if trimmed != HEADER {
149                    return Err(bad(format!(
150                        "expected the header `{HEADER}`, found `{trimmed}`"
151                    )));
152                }
153                seen_header = true;
154                continue;
155            }
156            let toks: Vec<&str> = trimmed.split_whitespace().collect();
157            match toks.as_slice() {
158                ["covergroup", name] => {
159                    let name = unescape(name).map_err(&bad)?;
160                    if db.ty(&name).is_some() {
161                        return Err(bad(format!("covergroup `{name}` appears twice")));
162                    }
163                    db.types.push(DbType {
164                        name,
165                        items: Vec::new(),
166                    });
167                }
168                [kw @ ("coverpoint" | "cross"), name, at_least, weight] => {
169                    let kind = if *kw == "cross" {
170                        ItemKind::Cross
171                    } else {
172                        ItemKind::Coverpoint
173                    };
174                    let name = unescape(name).map_err(&bad)?;
175                    let at_least = field(at_least, "at_least").map_err(&bad)?;
176                    let weight = field(weight, "weight").map_err(&bad)?;
177                    let Some(ty) = db.types.last_mut() else {
178                        return Err(bad(format!("`{kw} {name}` before any covergroup")));
179                    };
180                    if ty.item(kind, &name).is_some() {
181                        return Err(bad(format!(
182                            "{kw} `{name}` appears twice in covergroup `{}`",
183                            ty.name
184                        )));
185                    }
186                    ty.items.push(DbItem {
187                        kind,
188                        name,
189                        at_least,
190                        weight,
191                        bins: Vec::new(),
192                    });
193                }
194                ["bin", name, hits] => {
195                    let name = unescape(name).map_err(&bad)?;
196                    let hits: u64 = hits
197                        .parse()
198                        .map_err(|_| bad(format!("bad hit count `{hits}`")))?;
199                    let Some(item) = db.types.last_mut().and_then(|t| t.items.last_mut()) else {
200                        return Err(bad(format!("bin `{name}` before any coverpoint or cross")));
201                    };
202                    if item.bins.iter().any(|(n, _)| *n == name) {
203                        return Err(bad(format!(
204                            "bin `{name}` appears twice in `{}`",
205                            item.name
206                        )));
207                    }
208                    item.bins.push((name, hits));
209                }
210                _ => return Err(bad(format!("unrecognised line `{trimmed}`"))),
211            }
212        }
213        if !seen_header {
214            return Err(CovError::BadDb {
215                line: 1,
216                detail: format!("empty file, expected the header `{HEADER}`"),
217            });
218        }
219        Ok(db)
220    }
221}
222
223fn field<T: std::str::FromStr>(tok: &str, key: &str) -> Result<T, String> {
224    tok.strip_prefix(key)
225        .and_then(|r| r.strip_prefix('='))
226        .and_then(|v| v.parse().ok())
227        .ok_or_else(|| format!("expected `{key}=<number>`, found `{tok}`"))
228}
229
230fn escape(s: &str) -> String {
231    if s.is_empty() {
232        return "%".to_string();
233    }
234    let mut out = String::with_capacity(s.len());
235    for &b in s.as_bytes() {
236        if b > b' ' && b < 0x7f && b != b'%' {
237            out.push(b as char);
238        } else {
239            out.push_str(&format!("%{b:02X}"));
240        }
241    }
242    out
243}
244
245fn unescape(tok: &str) -> Result<String, String> {
246    if tok == "%" {
247        return Ok(String::new());
248    }
249    let bytes = tok.as_bytes();
250    let mut out = Vec::with_capacity(bytes.len());
251    let mut i = 0;
252    while i < bytes.len() {
253        if bytes[i] == b'%' {
254            let hex = tok
255                .get(i + 1..i + 3)
256                .and_then(|h| u8::from_str_radix(h, 16).ok())
257                .ok_or_else(|| format!("bad escape in `{tok}`"))?;
258            out.push(hex);
259            i += 3;
260        } else {
261            out.push(bytes[i]);
262            i += 1;
263        }
264    }
265    String::from_utf8(out).map_err(|_| format!("`{tok}` is not valid UTF-8 once unescaped"))
266}