Skip to content
VLSI Mentor

Tcl · Module 30

A Timing Violation Extractor

Four malformed reports refused by name, and two that parse cleanly while being wrong. The second pair is what the chapter is about.

Advanced23 min readtcledapart-iiparsingcsvcase-study

Part II · Module 30 · Chapter 30.4 · A Timing Violation Extractor

This script has no tool in it. It reads a text report and writes a CSV, which is the most-written script in the industry and the one most likely to be quietly wrong.

The registry asks for the handling for every malformed section it will meet, so the chapter is organised around six specimens rather than around the parser. Four of them are refused and named. Two of them parse perfectly while being wrong, and those two are the chapter.

1. The parser

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
# ---- the parser ---------------------------------------------------------
proc extract {path} {
    if {![file isfile $path]}      { error "no such report: $path" {} {RPT NOFILE} }
    if {[file size $path] == 0}    { error "report is empty: $path" {} {RPT EMPTY} }

    set fh [open $path r]
    set rows {} ; set check "" ; set group "" ; set blocks 0 ; set n 0
    while {[gets $fh line] >= 0} {
        incr n
        # a section header: carry its identity INTO each row (24.4 section 1)
        if {[regexp {^\s*(max_delay/setup|min_delay/hold)\s+\('(\S+)'\s+group\)} $line -> c g]} {
            set check $c ; set group $g ; incr blocks ; continue
        }
        # a data row, recognised by SHAPE and anchored both ends (24.4 section 3)
        if {[regexp {^\s+(\S+)\s+(-?\d+\.\d+)\s+(-?\d+\.\d+)\s+(-?\d+\.\d+)\s*$} \
                 $line -> ep req act slk]} {
            if {$check eq ""} {
                close $fh
                error "$path:$n: a data row before any section header" {} {RPT ORPHANROW}
            }
            lappend rows [dict create endpoint $ep check $check group $group \
                              required $req actual $act slack $slk]
        }
    }
    close $fh
    if {$blocks == 0} { error "$path: no section header found" {} {RPT NOSECTIONS} }
    if {[llength $rows] == 0} { error "$path: $blocks section(s) and no data rows" {} {RPT NOROWS} }
    return [list $rows $blocks]
}

Everything in it is Module 24 applied once rather than explained again:

  • State carried forward. The section header sets check and group; every data row afterwards inherits them. A CSV row that does not know which check it came from is not usable, and the header is the only place that information exists.
  • Rows recognised by shape, anchored both ends. ^\s+(\S+) then three decimal numbers then \s*$. Chapter 24.4 §3 argued for the shape rather than a keyword, because a tool changes its column labels between versions and does not change its arithmetic.
  • An orphan row is an error, not a skip. A data row before any header has no check and no group; guessing is worse than refusing, so it raises with a line number.
  • Two post-conditions. No sections at all, and sections with no rows, are different failures with different causes and get different messages.

Each refusal carries an errorCode — RPT NOFILE, RPT EMPTY, RPT ORPHANROW, RPT NOSECTIONS, RPT NOROWS — so a caller can route on the kind of malformation without reading the message, which is Chapter 29.4's mechanism.

2. CSV, done properly

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
proc csv_field {s} {
    if {[regexp {[",\n\r]} $s]} { return "\"[string map [list \" \"\"] $s]\"" }
    return $s
}
proc csv_row {fields} {
    set out {}
    foreach f $fields { lappend out [csv_field $f] }
    return [join $out ","]
}

Four lines, and they are the difference between a CSV file and a file with commas in it. A field containing a comma, a quote or a newline is quoted, and an embedded quote is doubled. Chapter 24.6 measured why hand-joining with commas breaks: endpoint names in real designs contain characters you did not anticipate, and the failure appears in the spreadsheet rather than in your script.

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
proc to_csv {rows out} {
    file mkdir [file dirname $out]
    set fh [open $out w]
    puts $fh [csv_row {endpoint check group required actual slack}]
    foreach r [lsort -real -index 11 $rows] {
        puts $fh [csv_row [list [dict get $r endpoint] [dict get $r check] \
                               [dict get $r group] [dict get $r required] \
                               [dict get $r actual] [dict get $r slack]]]
    }
    close $fh
    return [llength $rows]
}

lsort -real -index 11 sorts by slack — index 11 because a dict is a flat list and slack is the sixth key, so its value is the twelfth element. Worst first, so the first data row of the CSV is the one somebody has to fix.

3. The six specimens

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
=== every specimen, through the same extractor ===
  viol_dropped.rpt     2 row(s), 1 section(s), 1 total-line(s); rows=claim NO  sections=totals yes
  viol_empty.rpt     REFUSED: report is empty: /private/tmp/m30/reports/viol_empty.rpt
  viol_good.rpt        3 row(s), 2 section(s), 2 total-line(s); rows=claim yes sections=totals yes
  viol_norows.rpt    REFUSED: /private/tmp/m30/reports/viol_norows.rpt: 1 section(s) and no data rows
  viol_nosections.rpt REFUSED: /private/tmp/m30/reports/viol_nosections.rpt: no section header found
  viol_orphanrow.rpt REFUSED: /private/tmp/m30/reports/viol_orphanrow.rpt:2: a data row before any section header
  viol_truncated.rpt   2 row(s), 2 section(s), 1 total-line(s); rows=claim yes sections=totals NO

Four refusals, each naming the file and the specific problem — and viol_orphanrow.rpt:2 naming the line. That is the easy half. The two that did not refuse are:

  • viol_dropped.rpt — 2 rows parsed, and the report's own Total says 3.
  • viol_truncated.rpt — 2 rows parsed, 2 section headers, and only 1 Total line.

Both produced a CSV. Both CSVs are wrong. Neither is detectable by anything inside the parser, because nothing inside the parser is wrong — the lines it parsed, it parsed correctly.

And the good one:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
=== the CSV from the good one ===
endpoint,check,group,required,actual,slack
u_sel_reg/D,max_delay/setup,core_clk,4.8200,4.9510,-0.1310
u_addr_reg/D,max_delay/setup,core_clk,4.8200,4.8930,-0.0730
u_sync_reg/D,min_delay/hold,core_clk,0.1200,0.0890,-0.0310

Three rows, worst first, each carrying its own check and group. The hold row sits in the same file as the setup rows and is distinguishable from them, which is the whole reason the header is carried into the row — and it is the same point Chapter 30.2 §4 made about aggregation, arriving here as a column.

4. The two that need a second opinion

The report hands over a cross-check for free: each section ends with its own count.

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
# ---- the cross-check the report hands over for free (24.4 section 6) ---
proc claimed_total {path} {
    set fh [open $path r] ; set total 0 ; set seen 0
    while {[gets $fh line] >= 0} {
        if {[regexp {^\s*Total\s+(\d+)\s+\(VIOLATED\)} $line -> k]} { incr total $k ; incr seen }
    }
    close $fh
    return [list $total $seen]
}

Check 1: do the rows I parsed equal the count the report claims? That catches viol_dropped.rpt immediately. Its third row is an unconstrained endpoint:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
  u_unconstrained/D                    N/A       5.1020         N/A

The shape regex wants three decimal numbers and finds one. It declines the row — correctly, because N/A is not a slack — and the row disappears with no message at all. The parser is right; the output is missing a third of its content. Check 1 notices because the Total line still says 3.

Check 2: is there one Total line per section header? That catches viol_truncated.rpt, which check 1 cannot.

5. Neither check is sufficient

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
=== neither check is sufficient alone ===
  SPECIMEN             check 1   check 2   what is actually wrong
  viol_good.rpt        pass      pass      nothing
  viol_dropped.rpt     FAIL      pass      one row silently unparsed
  viol_truncated.rpt   pass      FAIL      a whole section lost

  check 1: rows parsed == the sections' own Total count
  check 2: one Total line per section header
  a truncation removes BOTH sides of check 1, so only check 2 sees it
  a dropped row leaves the Total intact, so only check 1 sees it

Read the two FAIL cells. They are in different columns, and that is the entire result.

Here is how viol_truncated.rpt ends:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
  u_addr_reg/D                    4.8200       4.8930     -0.0730
  u_sel_reg/D                     4.8200       4.9510     -0.1310
  -------------------------------------------------------------
  Total                                                    2 (VIOLATED)

  min_delay/hold ('core_clk' group)

Why check 1 passes on a truncated report: the file was cut off in the middle of the hold section, after its header and before any of its rows or its Total line. So the rows the parser found (2) and the total the report claims (2) both refer only to the setup section. The truncation removed the evidence of itself from both sides of the comparison. A count against a claim is only as good as the claim surviving.

Why check 2 passes on a dropped row: the section is complete — header, rows, Total — so one header matches one total. Nothing structural is missing; one line inside the structure did not match a regex.

Each check is blind to exactly the corruption the other catches, and the blindness is not an accident of these two specimens: it follows from what each check compares. One compares content against a claim, the other compares structure against structure. A corruption that damages content and claim together defeats the first; a corruption that damages neither structure defeats the second.

6. What two checks still cannot catch

  • A number that is wrong but well-formed. If the tool printed 4.8200 4.8930 -0.7300 — slack an order of magnitude out — both checks pass and the CSV is confidently wrong. Catching that needs arithmetic: required - actual should equal slack, and the parser never checks it. Chapter 24.7 names this as the class assertions do not reach.
  • A report of the wrong run. Both checks are internal. A perfectly consistent report of last night's netlist passes everything, which is Chapter 29.2's problem and needs identity, not counting.
  • A section the tool never emitted. If the tool was asked for setup and hold and silently produced only setup, the file is internally consistent — one header, one total, matching rows. Detecting it requires knowing what was expected, which is the plan-driven approach Chapter 30.5 builds next.
  • Rows the regex matches that are not data. A summary line that happens to have three decimal numbers in the right places would be extracted as an endpoint. The anchors make it unlikely; they do not make it impossible.

That last pair is the honest ceiling of this chapter: a parser can check itself against the file, and cannot check the file against the world. Everything in section 5 is the first kind. The next chapter is about the second.

Exercises

  1. Add the arithmetic check from section 6 — required - actual == slack within a tolerance. Which specimen does it catch, and why does the tolerance matter?
  2. Remove the \s*$ anchor from the data-row regex. Construct a line that is now matched and should not be.
  3. viol_truncated.rpt loses its hold section. Make a specimen that loses the setup section instead. Do both checks behave the same way?
  4. The orphan-row error reports a line number. Why is that worth more here than in most error messages?
  5. Make claimed_total tolerate a Total line with no (VIOLATED) suffix. What does that cost?
  6. A third check is proposed: the CSV must have as many data lines as rows parsed. Is it worth adding?

Answers

  1. None of the six — which is the point. Every specimen's arithmetic is self-consistent, so the check passes everywhere and you have to build a seventh specimen to see it work; that is the section-6 class arriving as a chore. The tolerance matters because the three columns are rounded to four decimals independently, so an exact comparison fails on correct data: 4.8200 - 4.8930 = -0.0730 works here, but a report rounding to two decimals would not.
  2. u_addr_reg/D 4.8200 4.8930 -0.0730 (worst in group) — three numbers in the right shape followed by a trailing annotation. With the anchor it is declined; without it, the annotation is ignored and the row is accepted. Which may even be what you want, and that is the problem: you cannot tell an annotation you want to ignore from a fourth column you needed to read.
  3. No. Losing the setup section means the file starts with the hold header, so the parser sees 1 section, 1 total and the hold rows — internally consistent, both checks pass, and half the violations are gone with no signal at all. Truncation at the end is visible because the header survives without its total; truncation at the front removes the header too. The only defence is knowing which sections were expected.
  4. Because the message cannot say what is wrong with the row — the row is fine; what is missing is the header that should have preceded it. There is nothing to quote and no value to name, so the location is the diagnosis.
  5. It costs the distinction between a violating section and a clean one. Total 0 with no suffix is a section that passed; counting it as a claim of zero rows is correct, but then a clean section and a section whose total line was mangled become the same thing. The suffix is the only evidence the line is the one you think it is.
  6. No — it compares the writer against itself. to_csv iterates the rows it was given, so the count can only differ if puts failed, and if puts failed you have a bigger problem than a count. A check worth adding compares two independently produced things; this one compares a loop to its own input.

Where this fits

Part of the Tcl curriculum.