Tcl · Module 24
Building a Violation Table
The report groups endpoints under checks. Triage needs rows you can sort, filter and count. Inverting that is the job — and one obvious sort turns out to be meaningless.
Advanced20 min readtcledapart-iireportsviolationstriage
Part II · Module 24 · Chapter 24.4 · Building a Violation Table
Chapter 22.7 produced the all-violators report, the one command that says whether a design is closed. This chapter turns it into the form people actually work from.
The gap between those two is not formatting. It is data modelling.
1. How the report is organised, and how you need it
The report is organised by check. A section header names a check type and a clock group, and the endpoints that fail it are listed underneath — here is the first section, entire:
max_delay/setup ('core_clk' group)
Endpoint Required Actual Slack
---------------------------------------------------------------------
u_mem/addr_reg_7/D 4.8200 4.8930 -0.0730
u_mem/dout_reg_3/D 4.8200 4.8390 -0.0190
u_ctrl/state_reg_0/D 4.8200 4.8410 -0.0210
u_dec/sel_reg_2/D 4.8200 4.9510 -0.1310
---------------------------------------------------------------------
Total 4 (VIOLATED)A second section follows with the same structure and a different check:
=== and the second section's header, for contrast ===
min_delay/hold ('core_clk' group)That is the right shape for reading. It is the wrong shape for every question that follows, because the thing you want to sort, filter, count and compare is the endpoint, and the endpoint's check type is three lines above it in a header it does not repeat.
2. The parser
proc parse_violators {path} {
set fh [open $path r]
set rows {}
set group "" ; set check ""
while {[gets $fh line] >= 0} {
# a section header names the check and the clock group
if {[regexp {^\s*(max_delay/setup|min_delay/hold)\s+\('(\S+)'\s+group\)} $line -> c g]} {
set check $c ; set group $g ; continue
}
# a data row: four columns, the last three numeric
if {[regexp {^\s+(\S+)\s+(\d+\.\d+)\s+(\d+\.\d+)\s+(-?\d+\.\d+)\s*$} \
$line -> ep req act slk]} {
lappend rows [dict create endpoint $ep check $check group $group \
required $req actual $act slack $slk]
}
}
close $fh
return $rows
}=== one row per endpoint ===
ENDPOINT CHECK GROUP SLACK
u_mem/addr_reg_7/D max_delay/setup core_clk -0.0730
u_mem/dout_reg_3/D max_delay/setup core_clk -0.0190
u_ctrl/state_reg_0/D max_delay/setup core_clk -0.0210
u_dec/sel_reg_2/D max_delay/setup core_clk -0.1310
u_io/u_sync/sync_reg_1/D min_delay/hold core_clk -0.0310
5 row(s)The structure is Chapter 24.1 §4's stateful loop with one difference worth noticing: the header sets state that persists across many rows, rather than being cleared after each. A timing report's Endpoint: belongs to exactly one slack line; a section header belongs to every row until the next header. Which of the two you are looking at decides whether the accumulator is cleared on commit — and getting it backwards produces either empty fields or fields that leak into the next section.
3. Recognise a data row by its shape
The subtle pattern is the second one:
{^\s+(\S+)\s+(\d+\.\d+)\s+(\d+\.\d+)\s+(-?\d+\.\d+)\s*$}It is anchored at both ends, and that is what makes it work. Consider everything else in the file it has to reject:
---------------------------------------------------------------------— no numeric columns.Endpoint Required Actual Slack— four columns, none numeric.Total 4 (VIOLATED)— starts plausibly, but4 (VIOLATED)cannot satisfy three decimal columns followed by end-of-line.****************************************— not indented.Report : constraint— not indented.
None of those needed an explicit exclusion. The pattern describes what a data row is — leading whitespace, a non-blank field, then exactly three decimal numbers, then the end of the line — and everything that is not one fails to match.
4. The questions that are now one line each
set in_mem {}
foreach r $rows { if {[string match "u_mem/*" [dict get $r endpoint]]} { lappend in_mem $r } }
set hold {}
foreach r $rows { if {[dict get $r check] eq "min_delay/hold"} { lappend hold $r } }string match rather than regexp for the block prefix, per Chapter 13.10: the pattern is a glob, the two cost the same, and the glob says what it means. Watch the anchoring though — string match is anchored at both ends by definition, so u_mem/* is right and u_mem alone would match nothing.
5. The sort that means nothing
The obvious thing to do with a violation table is sort it worst-first:
proc by_slack {a b} {
set x [dict get $a slack] ; set y [dict get $b slack]
if {$x < $y} { return -1 } elseif {$x > $y} { return 1 }
return 0
}
foreach r [lsort -command by_slack $rows] { … }=== sorted worst first, which is the triage order ===
-0.1310 u_dec/sel_reg_2/D
-0.0730 u_mem/addr_reg_7/D
-0.0310 u_io/u_sync/sync_reg_1/D
-0.0210 u_ctrl/state_reg_0/D
-0.0190 u_mem/dout_reg_3/DLook at the third row.
The fix is to sort within a check, never across:
set by_check [dict create]
foreach r $rows { dict lappend by_check [dict get $r check] $r }
dict for {c rs} $by_check {
set worst 0.0
foreach r $rs { if {[dict get $r slack] < $worst} { set worst [dict get $r slack] } }
puts [format " %-16s %d endpoint(s) worst %s" $c [llength $rs] $worst]
}=== grouped by check, because setup and hold are different problems ===
max_delay/setup 4 endpoint(s) worst -0.1310
min_delay/hold 1 endpoint(s) worst -0.0310Two numbers per check, each with its denominator. dict lappend builds the grouping in one pass and treats a missing key as an empty list, so no "does this check exist yet" test is needed — the same idiom as Chapter 23.2 §3's dict incr tally.
This is Module 23's recurring argument arriving in a new form. There the mistake was collapsing a distribution to its extreme; here it is merging two distributions that do not share an axis. Both produce a single confident number that cannot be acted on.
6. The report checks your parse again
set fh [open reports/violators.rpt r]
set claimed 0
while {[gets $fh line] >= 0} {
if {[regexp {^\s*Total\s+(\d+)\s+\(VIOLATED\)} $line -> n]} { incr claimed $n }
}
close $fh=== and the table's own total line, as a cross-check ===
report claims : 5 violation(s)
rows parsed : 5
agree : yesThird report in a row that states a total beside its parts, and the third free correctness test. The pattern is now unmistakable, and Chapter 24.7 turns it into the module's closing principle.
What makes this one particularly good is that it is checking the thing most likely to break. A shape-matched data-row pattern (§3) fails by dropping rows — a column widens, a value appears as 3 rather than 3.0000, an endpoint name contains a space — and a dropped row is invisible unless something counts. agree : yes is that something.
Note incr claimed $n rather than incr claimed: there is one Total line per section, each carrying its own count, so the sum across sections is what must match. A parser that took only the first Total would have checked 4 against 5 and reported a failure on a correct parse.
7. What a violation table still does not tell you
Honest limits, because this table is persuasive and incomplete.
- It has no path detail. Why
u_dec/sel_reg_2/Dmisses by 0.131 needsreport_timingon that endpoint. The table says where to look, not what is wrong. - It is one corner. Chapter 22.8 showed that per-corner results cannot be summarised by their worst, and a violation table from one corner says nothing about the others. The corner belongs in the table as a column, filled in by the loop that ran the corners.
- Endpoints are not independent. Four setup failures on paths through one shared cell are one fix, not four. The table cannot see the sharing; it counts endpoints because endpoints are what the report lists.
- It cannot tell a new violation from a known one. That needs last night's table, which is Chapter 24.6's reason for existing.
8. Exercises
Exercise 1. The report is organised by check. Why is that the wrong shape for triage?
Exercise 2. Why is the section header's state not cleared after each row, when the timing parser's endpoint was?
Exercise 3. The data-row pattern needs no explicit rules for banners, rules, headers or the Total line. Why not?
Exercise 4. What does a skip-list parser do when the report format gains a new line type? What does a shape-matched parser do?
Exercise 5. Why is u_io/u_sync/sync_reg_1/D at −0.031 arguably worse than u_mem/addr_reg_7/D at −0.073?
Exercise 6. Why can't a setup and a hold violation be ranked on one axis?
Exercise 7. Why incr claimed $n rather than incr claimed?
Exercise 8. The data-row pattern fails by dropping rows rather than accepting junk. Why is that the better failure mode?
Exercise 9. Four setup violations on paths through one shared buffer. What does the table get wrong?
Where this fits
Part of the Tcl curriculum.
