Skip to content

Commit dea4a8b

Browse files
committed
Ensure expression and condition coverage is properly mapped to XML output
Signed-off-by: Matthew Ballance <matt.ballance@gmail.com>
1 parent 469459e commit dea4a8b

6 files changed

Lines changed: 198 additions & 63 deletions

File tree

src/ucis/cmd/cmd_convert.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,8 @@
77

88
def convert(args):
99
if args.input_format is None:
10-
args.input_format = "xml"
10+
rgy_tmp = FormatRgy.inst()
11+
args.input_format = rgy_tmp.detectDatabaseFormat(args.input) or "xml"
1112
if args.output_format is None:
1213
args.output_format = "xml"
1314

src/ucis/mem/mem_fsm_scope.py

Lines changed: 18 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -79,16 +79,30 @@ def incrementCount(self, amt: int = 1):
7979
class MemFSMScope(MemScope):
8080
"""In-memory FSM coverage scope.
8181
82-
Stores FSM states and transitions in memory. States and transitions
83-
are also reflected as FSMBIN cover items for compatibility with
84-
generic coverage iteration.
82+
Per UCIS LRM section 6.5.6: a UCIS_FSM scope shall have exactly one
83+
UCIS_FSM_STATES child scope and one UCIS_FSM_TRANS child scope.
84+
FSMBIN coveritems live in those sub-scopes, not directly on the FSM scope.
8585
"""
8686

8787
def __init__(self, parent, name, srcinfo, weight, source, flags=0):
8888
super().__init__(parent, name, srcinfo, weight, source,
8989
ScopeTypeT.FSM, flags)
9090
self._states = {} # name -> MemFSMState
9191
self._transitions = {} # (from_name, to_name) -> MemFSMTransition
92+
# Mandatory sub-scopes (LRM 6.5.6, Figure 36)
93+
self._states_scope = MemScope(self, "UCIS:STATE", srcinfo, weight,
94+
source, ScopeTypeT.FSM_STATES, flags)
95+
self.m_children.append(self._states_scope)
96+
self._trans_scope = MemScope(self, "UCIS:TRANSITION", srcinfo, weight,
97+
source, ScopeTypeT.FSM_TRANS, flags)
98+
self.m_children.append(self._trans_scope)
99+
100+
def createNextCover(self, name, data, sourceinfo):
101+
"""Route FSMBIN coveritems to the correct mandatory sub-scope."""
102+
if "->" in name:
103+
return self._trans_scope.createNextCover(name, data, sourceinfo)
104+
else:
105+
return self._states_scope.createNextCover(name, data, sourceinfo)
92106

93107
# --- State API ---
94108

@@ -178,8 +192,7 @@ def createNextTransition(self, from_state_name: str, to_state_name: str,
178192

179193
def getIntProperty(self, coverindex, property):
180194
if property == IntProperty.FSM_STATEVAL:
181-
# Return index of state at coverindex if it is a state bin
182-
items = list(self.m_cover_items)
195+
items = list(self._states_scope.m_cover_items)
183196
if 0 <= coverindex < len(items):
184197
name = items[coverindex].m_name
185198
state = self._states.get(name)

src/ucis/merge/db_merger.py

Lines changed: 23 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -368,8 +368,29 @@ def _merge_scopes_by_type(self, dst_parent, src_scopes, scope_type):
368368
0 # flags - use default
369369
)
370370

371-
# Merge coverage items
372-
self._merge_code_coverage_items(dst_sub_scope, src_scope_l)
371+
if scope_type == ScopeTypeT.FSM:
372+
# Per LRM: FSMBIN items live in FSM_STATES/FSM_TRANS sub-scopes.
373+
# Collect from src sub-scopes; dst.createNextCover() routes correctly.
374+
item_name_m: Dict[str, List] = {}
375+
item_name_l = []
376+
for src_fsm in src_scope_l:
377+
for sub_type in (ScopeTypeT.FSM_STATES, ScopeTypeT.FSM_TRANS):
378+
for sub_scope in src_fsm.scopes(sub_type):
379+
for ci in sub_scope.coverItems(CoverTypeT.FSMBIN):
380+
nm = ci.getName()
381+
cvg = ci.getCoverData()
382+
if nm not in item_name_m:
383+
item_name_m[nm] = [0, cvg.goal]
384+
item_name_l.append(nm)
385+
item_name_m[nm][0] += cvg.data
386+
for nm in item_name_l:
387+
count, goal = item_name_m[nm]
388+
cd = CoverData(CoverTypeT.FSMBIN, goal)
389+
cd.data = count
390+
dst_sub_scope.createNextCover(nm, cd, None)
391+
else:
392+
# Merge coverage items
393+
self._merge_code_coverage_items(dst_sub_scope, src_scope_l)
373394

374395
def _merge_code_coverage_items(self, dst_scope, src_scopes):
375396
"""Merge code coverage items from multiple source scopes.

src/ucis/sqlite/sqlite_fsm_scope.py

Lines changed: 67 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -26,11 +26,14 @@
2626
class FSMState:
2727
"""Represents an FSM state"""
2828

29-
def __init__(self, fsm_scope, state_id: int, state_name: str, state_index: int):
29+
def __init__(self, fsm_scope, state_id: int, state_name: str, state_index: int,
30+
cover_scope_id: int = None):
3031
self.fsm_scope = fsm_scope
3132
self.state_id = state_id
3233
self.state_name = state_name
3334
self.state_index = state_index
35+
# scope_id where the FSMBIN coveritem lives (FSM_STATES sub-scope per LRM)
36+
self._cover_scope_id = cover_scope_id if cover_scope_id is not None else fsm_scope.scope_id
3437

3538
def getName(self) -> str:
3639
"""Get state name"""
@@ -42,11 +45,10 @@ def getIndex(self) -> int:
4245

4346
def getVisitCount(self) -> int:
4447
"""Get number of times this state was visited"""
45-
# Find coveritem for this state
4648
cursor = self.fsm_scope.ucis_db.conn.execute(
4749
"""SELECT cover_data FROM coveritems
4850
WHERE scope_id = ? AND cover_name = ? AND cover_type & 0x800 != 0""",
49-
(self.fsm_scope.scope_id, self.state_name)
51+
(self._cover_scope_id, self.state_name)
5052
)
5153
row = cursor.fetchone()
5254
return row[0] if row else 0
@@ -61,7 +63,7 @@ def incrementCount(self, amt: int = 1):
6163
self.fsm_scope.ucis_db.conn.execute(
6264
"""UPDATE coveritems SET cover_data = ?
6365
WHERE scope_id = ? AND cover_name = ? AND cover_type & 0x800 != 0""",
64-
(current + amt, self.fsm_scope.scope_id, self.state_name)
66+
(current + amt, self._cover_scope_id, self.state_name)
6567
)
6668

6769
def incrementVisitCount(self, amt: int = 1):
@@ -109,12 +111,59 @@ def incrementCount(self, amt: int = 1):
109111

110112

111113
class SqliteFSMScope(SqliteScope):
112-
"""FSM coverage scope with state and transition tracking"""
114+
"""FSM coverage scope with state and transition tracking.
115+
116+
Per UCIS LRM section 6.5.6: a UCIS_FSM scope shall have exactly one
117+
UCIS_FSM_STATES child scope and one UCIS_FSM_TRANS child scope.
118+
FSMBIN coveritems are routed to those sub-scopes, not the FSM scope itself.
119+
"""
113120

114121
def __init__(self, ucis_db, scope_id: int):
115122
super().__init__(ucis_db, scope_id)
116123
self._states_cache = {}
117124
self._transitions_cache = {}
125+
self._states_scope_cache = None
126+
self._trans_scope_cache = None
127+
128+
def _get_or_create_sub_scope(self, scope_type, scope_name):
129+
"""Find existing or create the mandatory FSM sub-scope in the DB."""
130+
cursor = self.ucis_db.conn.execute(
131+
"SELECT scope_id FROM scopes WHERE parent_id = ? AND scope_type = ?",
132+
(self.scope_id, scope_type)
133+
)
134+
row = cursor.fetchone()
135+
if row:
136+
return SqliteScope(self.ucis_db, row[0])
137+
cursor = self.ucis_db.conn.execute(
138+
"""INSERT INTO scopes (parent_id, scope_type, scope_name, scope_flags,
139+
weight, goal, source_file_id, source_line, source_token)
140+
VALUES (?, ?, ?, 0, 1, 100, NULL, -1, -1)""",
141+
(self.scope_id, scope_type, scope_name)
142+
)
143+
return SqliteScope(self.ucis_db, cursor.lastrowid)
144+
145+
def _get_states_scope(self):
146+
"""Return (and lazy-create) the FSM_STATES child scope."""
147+
from ucis.scope_type_t import ScopeTypeT
148+
if self._states_scope_cache is None:
149+
self._states_scope_cache = self._get_or_create_sub_scope(
150+
ScopeTypeT.FSM_STATES, "UCIS:STATE")
151+
return self._states_scope_cache
152+
153+
def _get_trans_scope(self):
154+
"""Return (and lazy-create) the FSM_TRANS child scope."""
155+
from ucis.scope_type_t import ScopeTypeT
156+
if self._trans_scope_cache is None:
157+
self._trans_scope_cache = self._get_or_create_sub_scope(
158+
ScopeTypeT.FSM_TRANS, "UCIS:TRANSITION")
159+
return self._trans_scope_cache
160+
161+
def createNextCover(self, name, data, sourceinfo):
162+
"""Route FSMBIN coveritems to the correct mandatory sub-scope."""
163+
if "->" in name:
164+
return self._get_trans_scope().createNextCover(name, data, sourceinfo)
165+
else:
166+
return self._get_states_scope().createNextCover(name, data, sourceinfo)
118167

119168
def createState(self, state_name: str, state_index: int = None) -> FSMState:
120169
"""Create a new FSM state"""
@@ -135,13 +184,14 @@ def createState(self, state_name: str, state_index: int = None) -> FSMState:
135184
)
136185
state_id = cursor.lastrowid
137186

138-
# Create a coveritem for state visits
187+
# Create a coveritem for state visits (routes to FSM_STATES sub-scope)
139188
cover_data = CoverData(0x800, 0) # FSMBIN type
140189
cover_data.data = 0
141190
state_cover = self.createNextCover(state_name, cover_data, None)
142191

143-
# Create state object
144-
state = FSMState(self, state_id, state_name, state_index)
192+
# Create state object; coveritem lives in FSM_STATES sub-scope
193+
state = FSMState(self, state_id, state_name, state_index,
194+
cover_scope_id=self._get_states_scope().scope_id)
145195
self._states_cache[state_name] = state
146196

147197
return state
@@ -161,7 +211,8 @@ def getState(self, state_name: str) -> FSMState:
161211
if not row:
162212
return None
163213

164-
state = FSMState(self, row[0], state_name, row[1])
214+
state = FSMState(self, row[0], state_name, row[1],
215+
cover_scope_id=self._get_states_scope().scope_id)
165216
self._states_cache[state_name] = state
166217
return state
167218

@@ -177,7 +228,9 @@ def getStates(self):
177228
for row in cursor:
178229
state_name = row[1]
179230
if state_name not in self._states_cache:
180-
self._states_cache[state_name] = FSMState(self, row[0], state_name, row[2])
231+
self._states_cache[state_name] = FSMState(
232+
self, row[0], state_name, row[2],
233+
cover_scope_id=self._get_states_scope().scope_id)
181234
yield self._states_cache[state_name]
182235

183236
def getNumStates(self) -> int:
@@ -275,12 +328,13 @@ def getStateCoveragePercent(self) -> float:
275328
if total == 0:
276329
return 0.0
277330

278-
# Count states with visits > 0
331+
# Coveritems live in FSM_STATES sub-scope per LRM
332+
states_scope_id = self._get_states_scope().scope_id
279333
cursor = self.ucis_db.conn.execute(
280334
"""SELECT COUNT(*) FROM fsm_states fs
281-
JOIN coveritems c ON c.scope_id = fs.scope_id AND c.cover_name = fs.state_name
335+
JOIN coveritems c ON c.scope_id = ? AND c.cover_name = fs.state_name
282336
WHERE fs.scope_id = ? AND c.cover_data > 0""",
283-
(self.scope_id,)
337+
(states_scope_id, self.scope_id)
284338
)
285339
row = cursor.fetchone()
286340
visited = row[0] if row else 0

src/ucis/xml/xml_writer.py

Lines changed: 73 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -70,7 +70,7 @@ def write(self, file, db : UCIS, ctx=None):
7070
self.setAttr(self.root, "writtenBy", wb if wb else getpass.getuser())
7171
wt = db.getWrittenTime()
7272
if wt:
73-
self.setAttrDateTime(self.root, "writtenTime", str(wt))
73+
self.setAttrDateTime(self.root, "writtenTime", wt)
7474
else:
7575
self.setAttrDateTime(self.root, "writtenTime",
7676
date.today().strftime("%Y%m%d%H%M%S"))
@@ -208,15 +208,7 @@ def write_instance_coverages(self, s, parent_instance_id=None):
208208
self.write_branch_coverage(inst, s)
209209
self.write_fsm_coverage(inst, s)
210210
self.write_assertion_coverage(inst, s)
211-
# Warn once per instance if condition/expression scopes are present
212-
warned = False
213-
for _ in s.scopes(ScopeTypeT.COND):
214-
if not warned and self.ctx is not None:
215-
self.ctx.warn(
216-
"xml: condition/expression coverage is not supported "
217-
"— scopes skipped")
218-
warned = True
219-
break
211+
self.write_condition_coverage(inst, s)
220212
self.write_user_attrs(inst, s)
221213

222214
# Recursively write child instances
@@ -241,7 +233,10 @@ def write_toggle_coverage(self, inst_elem, scope):
241233
tb_elem.set("key", "0")
242234
for bin_item in bins:
243235
name = bin_item.getName()
244-
if "to" in name.lower():
236+
if "->" in name:
237+
parts = name.split("->", 1)
238+
from_val, to_val = parts[0].strip(), parts[1].strip()
239+
elif "to" in name.lower():
245240
parts = name.lower().split("to", 1)
246241
from_val, to_val = parts[0], parts[1]
247242
else:
@@ -300,27 +295,31 @@ def write_fsm_coverage(self, inst_elem, scope):
300295
fsm_elem.set("name", fsm_scope.getScopeName())
301296
fsm_elem.set("type", "reg")
302297
fsm_elem.set("width", "1")
303-
bins = list(fsm_scope.coverItems(CoverTypeT.FSMBIN))
298+
# FSMBIN items live in FSM_STATES and FSM_TRANS child scopes (LRM 6.5.6)
299+
state_bins = []
300+
for ss in fsm_scope.scopes(ScopeTypeT.FSM_STATES):
301+
state_bins.extend(ss.coverItems(CoverTypeT.FSMBIN))
302+
trans_bins = []
303+
for ts in fsm_scope.scopes(ScopeTypeT.FSM_TRANS):
304+
trans_bins.extend(ts.coverItems(CoverTypeT.FSMBIN))
304305
# States must come before transitions in XML (schema order)
305-
for bin_item in bins:
306-
if "->" not in bin_item.getName():
307-
state_elem = self.mkElem(fsm_elem, "state")
308-
state_elem.set("stateName", bin_item.getName())
309-
state_elem.set("stateValue", str(bins.index(bin_item)))
310-
sb_elem = self.mkElem(state_elem, "stateBin")
311-
contents = self.mkElem(sb_elem, "contents")
312-
contents.set("coverageCount", str(bin_item.getCoverData().data))
313-
for bin_item in bins:
314-
if "->" in bin_item.getName():
315-
parts = bin_item.getName().split("->", 1)
316-
trans_elem = self.mkElem(fsm_elem, "stateTransition")
317-
from_elem = self.mkElem(trans_elem, "state")
318-
from_elem.text = parts[0]
319-
to_elem = self.mkElem(trans_elem, "state")
320-
to_elem.text = parts[1]
321-
tb_elem = self.mkElem(trans_elem, "transitionBin")
322-
contents = self.mkElem(tb_elem, "contents")
323-
contents.set("coverageCount", str(bin_item.getCoverData().data))
306+
for i, bin_item in enumerate(state_bins):
307+
state_elem = self.mkElem(fsm_elem, "state")
308+
state_elem.set("stateName", bin_item.getName())
309+
state_elem.set("stateValue", str(i))
310+
sb_elem = self.mkElem(state_elem, "stateBin")
311+
contents = self.mkElem(sb_elem, "contents")
312+
contents.set("coverageCount", str(bin_item.getCoverData().data))
313+
for bin_item in trans_bins:
314+
parts = bin_item.getName().split("->", 1)
315+
trans_elem = self.mkElem(fsm_elem, "stateTransition")
316+
from_elem = self.mkElem(trans_elem, "state")
317+
from_elem.text = parts[0].strip() if len(parts) > 1 else bin_item.getName()
318+
to_elem = self.mkElem(trans_elem, "state")
319+
to_elem.text = parts[1].strip() if len(parts) > 1 else ""
320+
tb_elem = self.mkElem(trans_elem, "transitionBin")
321+
contents = self.mkElem(tb_elem, "contents")
322+
contents.set("coverageCount", str(bin_item.getCoverData().data))
324323
self.write_user_attrs(fc_elem, scope)
325324

326325
def write_assertion_coverage(self, inst_elem, scope):
@@ -360,6 +359,44 @@ def write_assertion_coverage(self, inst_elem, scope):
360359
str(sum(b.getCoverData().data for b in bins)))
361360
self.write_user_attrs(ac_elem, scope)
362361

362+
def write_condition_coverage(self, inst_elem, scope):
363+
cond_scopes = list(scope.scopes(ScopeTypeT.COND))
364+
if not cond_scopes:
365+
return
366+
cc_elem = self.mkElem(inst_elem, "conditionCoverage")
367+
cc_elem.set("metricMode", "UCIS:STD")
368+
for key, cond_scope in enumerate(cond_scopes):
369+
src = cond_scope.getSourceInfo()
370+
if src and src.file:
371+
file_id = self.file_id_m.get(src.file.getFileName(), 1)
372+
line = src.line if src.line >= 1 else 1
373+
else:
374+
file_id = 1
375+
line = 1
376+
uid = f"#cond#{file_id}#{line}#{key}#"
377+
expr_elem = self.mkElem(cc_elem, "expr")
378+
expr_elem.set("name", uid)
379+
expr_elem.set("key", str(key))
380+
expr_elem.set("exprString", cond_scope.getScopeName())
381+
expr_elem.set("index", "0")
382+
expr_elem.set("width", "1")
383+
self.addId(expr_elem, src)
384+
bins = list(cond_scope.coverItems(CoverTypeT.CONDBIN))
385+
# Infer input count from max length of binary-vector bin names
386+
binary_names = [b.getName() for b in bins
387+
if all(c in '01-' for c in b.getName())]
388+
n_inputs = max((len(n) for n in binary_names), default=1) if binary_names else 1
389+
# Emit placeholder subExpr names (signal names not available in DM)
390+
for i in range(n_inputs):
391+
sub_elem = self.mkElem(expr_elem, "subExpr")
392+
sub_elem.text = f"_input_{i}"
393+
for bin_item in bins:
394+
bin_elem = self.mkElem(expr_elem, "bin")
395+
bin_elem.set("alias", bin_item.getName())
396+
contents_elem = self.mkElem(bin_elem, "contents")
397+
contents_elem.set("coverageCount", str(bin_item.getCoverData().data))
398+
self.write_user_attrs(cc_elem, scope)
399+
363400
def write_covergroups(self, inst, scope):
364401
for cg in scope.scopes(ScopeTypeT.COVERGROUP):
365402
cgElem = self.mkElem(inst, "covergroupCoverage")
@@ -425,14 +462,15 @@ def write_coverpoint_bins(self, cpElem, coveritems : Iterator[CoverIndex]):
425462
cpBinElem = self.mkElem(cpElem, "coverpointBin")
426463
self.setAttr(cpBinElem, "name", cp_bin.getName())
427464

428-
if cp_bin.data.type == CoverTypeT.CVGBIN:
465+
bin_type = cp_bin.getCoverData().type
466+
if bin_type == CoverTypeT.CVGBIN:
429467
self.setAttr(cpBinElem, "type", "bins")
430-
elif cp_bin.data.type == CoverTypeT.IGNOREBIN:
468+
elif bin_type == CoverTypeT.IGNOREBIN:
431469
self.setAttr(cpBinElem, "type", "ignore")
432-
elif cp_bin.data.type == CoverTypeT.ILLEGALBIN:
470+
elif bin_type == CoverTypeT.ILLEGALBIN:
433471
self.setAttr(cpBinElem, "type", "illegal")
434472
else:
435-
raise Exception("Unknown bin type %s" % str(cp_bin.type))
473+
raise Exception("Unknown bin type %s" % str(bin_type))
436474
self.setAttr(cpBinElem, "key", "0")
437475

438476
# Now, add the coverage data

0 commit comments

Comments
 (0)