Skip to content
VLSI Mentor

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:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
  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:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
=== 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

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
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
}
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
=== 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:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
{^\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, but 4 (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

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
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:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
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] { … }
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
=== 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/D

Look at the third row.

The fix is to sort within a check, never across:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
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]
}
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
=== 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.0310

Two 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

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
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
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
=== and the table's own total line, as a cross-check ===
    report claims : 5 violation(s)
    rows parsed   : 5
    agree         : yes

Third 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/D misses by 0.131 needs report_timing on 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.