1use 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#[derive(Clone, Debug, PartialEq, Eq)]
45pub struct DbItem {
46 pub kind: ItemKind,
47 pub name: String,
48 pub at_least: u64,
51 pub weight: i64,
53 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 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}