Skip to content
VLSI Mentor

Tcl · Module 24

Reviewing a Report Parser

A parser is tested by the reports it will receive when something has gone wrong. Three stages of defence, five specimens, and the principle that makes the third stage work.

Advanced21 min readtcledapart-iireportstestingreview

Part II · Module 24 · Chapter 24.7 · Reviewing a Report Parser

Every parser in this module worked on the report it was written for. That is the easy case and it is not the one that matters.

The case that matters is the report that arrives when something else has already gone wrong — and a parser's behaviour then decides whether the flow stops or reports success.

1. The specimens

Four degenerate reports, each produced from the real one by a single mechanical operation, and each modelling a failure that actually happens:

SpecimenHow it was madeWhat it models
empty.rptzero bytesthe tool died before writing anything, or the disk filled
truncated.rptfirst 20 lines of the real reportthe run was killed partway through
renamed.rptslack (VIOLATED) → worst slack (VIOLATED)a tool upgrade changed a label
nosection.rptevery slack line deletedthe section is absent because the analysis did not run

Three things about that table are deliberate.

Each is one operation. Truncation, substitution, deletion. Building a specimen suite does not require inventing realistic-looking broken files — it requires damaging a real one in one identifiable way, so that when a test fails you know exactly which property the parser depended on.

None of them is corrupt in a way that would crash. All four are valid text files a parser will read happily to the end. That is the point: the interesting failures are the ones that produce a result.

The real report stays in the suite. A suite of only broken inputs cannot tell a parser that refuses everything from a parser that works — the lesson from Chapter 22.9 and from every checker that got switched off for crying wolf.

2. Stage 1: the naive gate

Here is the gate almost everyone writes, and it is defensible on its face — parse the report, count the violations, pass if there are none:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
proc gate_naive {path} {
    set paths [parse_timing $path]
    set v 0
    foreach p $paths { if {[dict get $p status] eq "VIOLATED"} { incr v } }
    if {$v == 0} { return "PASS" }
    return "FAIL ($v violated)"
}
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
=== the naive gate on five inputs ===
    timing.rpt     paths 3   verdict FAIL (2 violated)  the real report
    empty.rpt      paths 0   verdict PASS              an empty file
    truncated.rpt  paths 0   verdict PASS              a run killed partway through
    renamed.rpt    paths 1   verdict PASS              the field renamed by a tool upgrade
    nosection.rpt  paths 0   verdict PASS              the slack section simply absent

This is the failure shape every chapter in this module has hit from a different angle. 24.1 §7 returned a truncated number with ok=1. 24.3 §2 inverted a conclusion with three successful matches. 24.6 §3 wrote a corrupt row with a correct-looking library call. Here a gate passes a design on an empty file. In every case the operation succeeded.

3. Stage 2: make the parser assert what it expected

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
proc parse_timing_checked {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 paths [parse_timing $path]
    if {[llength $paths] == 0} {
        error "no timing paths found in $path — wrong format, or the field was renamed" \
              {} {RPT NOPATHS}
    }
    # every path must be complete: a slack with no endpoint is a parse failure
    set i 0
    foreach p $paths {
        incr i
        foreach k {start end group status slack} {
            if {[dict get $p $k] eq ""} {
                error "path $i parsed with an empty '$k'" {} [list RPT INCOMPLETE $k]
            }
        }
    }
    return $paths
}
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
=== the checked parser on the same five ===
    timing.rpt     accepted 3 path(s)
    empty.rpt      REFUSED  report is empty: reports/empty.rpt
    truncated.rpt  REFUSED  no timing paths found in reports/truncated.rpt — wrong format, or the field was renamed
    renamed.rpt    accepted 1 path(s)
    nosection.rpt  REFUSED  no timing paths found in reports/nosection.rpt — wrong format, or the field was renamed

Three of four caught. Note what the checks are doing:

  • file isfile and file size before parsing at all. Cheap, and they name the real problem instead of letting it become "0 paths".
  • Zero paths is an error, not a result. This is the Chapter 23.6 §6 argument — an ECO script that made no changes ran perfectly and did nothing — and Chapter 21.2's empty file list. A parser that found nothing has either been given the wrong file or has stopped matching.
  • Every field of every row must be non-empty. This catches the partial-block case directly rather than inferring it: a slack line that committed a row with no endpoint means a pattern stopped matching mid-block.
  • Each error carries a machine-readable errorCode — RPT EMPTY, RPT NOPATHS, RPT INCOMPLETE start — so a caller can distinguish "no report" from "wrong format" and act differently, per Chapter 14.2.

And then there is renamed.rpt.

4. The specimen that defeats stage 2

More checks of the same kind do not help, and that is the key observation. Every stage-2 check is written in terms of what the parser found, and the parser found one perfectly good path. You cannot detect a pattern that stopped matching by asking the pattern what it matched.

5. Stage 3: measure the file a second, independent way

The way out is to count something about the file that does not go through the pattern under test:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
proc count_lines_matching {path re} {
    set fh [open $path r] ; set n 0
    while {[gets $fh line] >= 0} { if {[regexp $re $line]} { incr n } }
    close $fh
    return $n
}

proc parse_timing_crosschecked {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 paths [parse_timing $path]
    # Every path block carries exactly one Startpoint and exactly one slack line.
    # Counting the blocks independently of the parse catches a pattern that
    # stopped matching.
    set blocks [count_lines_matching $path {^\s*Startpoint:}]
    if {$blocks == 0} { error "no path blocks in $path" {} {RPT NOBLOCKS} }
    if {[llength $paths] != $blocks} {
        error "parsed [llength $paths] path(s) from $blocks block(s) — a field has changed" \
              {} [list RPT COUNTMISMATCH [llength $paths] $blocks]
    }
    return $paths
}
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
=== blocks counted independently of the parse ===
    FILE           BLOCKS   PARSED   VERDICT
    timing.rpt     3        3        accepted
    empty.rpt      0        0        REFUSED
    truncated.rpt  1        0        REFUSED
    renamed.rpt    3        1        REFUSED
    nosection.rpt  3        0        REFUSED
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
=== the renamed file, in full ===
    catch 1
    parsed 1 path(s) from 3 block(s) — a field has changed
    errorcode: RPT COUNTMISMATCH

All four degenerate inputs refused, the real one accepted. And the error message names the actual problem — a field has changed — rather than a symptom.

The truncated.rpt row is worth a second look: 1 block, 0 parsed. The truncation cut the file in the middle of the first block, after its Startpoint: and before its slack line. The count catches that too, for the same reason.

6. What a cross-check still cannot catch

Honest limits, because stage 3 now looks like a solution.

A change that affects both measurements equally. If a release renamed Startpoint: to Start Point: and changed the slack line, both counts would go to zero together and RPT NOBLOCKS would fire — which is the right outcome by luck rather than by design. A format change large enough to break everything is caught; one that breaks both counts by the same amount is not.

A value that parsed wrongly but completely. 24.1 §7's -0.0 is a fully populated path whose slack is wrong by a factor of 73. Three blocks, three paths, counts agree, every field non-empty — and a number that is wrong. Count cross-checks verify structure, not values; the defence against a wrong value is the arithmetic cross-check of 24.2 §4 and 24.3 §4, which is why both kinds are worth having.

A report that is internally consistent and describes the wrong design. No amount of parsing catches a correct report on a stale netlist. That is Chapter 22.9's subject and it lives upstream of everything here.

The specimens you did not think of. The suite has four entries because four failures were imagined. A fifth will happen. The mitigation is that stage 3's cross-check is not specimen-specific — it catches any disagreement between the two counts, including from a cause nobody anticipated, which is why a structural invariant is worth more than a longer list of tests.

7. The review

What to check before trusting a report parser:

  • Does it refuse a missing file and a zero-byte file by name, rather than returning zero rows?
  • Is zero rows an error? A parser that found nothing has the wrong file or a stale pattern.
  • Is every field of every row asserted non-empty, so a half-parsed block cannot become a row?
  • Is there a second, independent count — block headers against parsed rows, or the report's own total against yours?
  • Is there an arithmetic cross-check where the report states a total beside its parts?
  • Are units captured with values, so a comparison cannot be silently wrong (Chapter 24.3)?
  • Are provenance fields captured — design, library, tool version, corner — so two runs can be refused as incomparable (Chapter 24.2 §6)?
  • Does every error carry a machine-readable code, so a caller can tell "no report" from "wrong format"?
  • Does the suite contain the good report too, so a parser that refuses everything fails the suite?
  • Is the exit status non-zero when any of this fails, so the flow stops rather than continuing on a result nobody checked?

That last one closes the module where Chapter 15.4 began: a script whose checks all failed and whose exit status is 0 has told the flow that everything is fine.

8. Exercises

Exercise 1. Why is each specimen made by a single mechanical operation on the real report?

Exercise 2. Why must the real report stay in the specimen suite?

Exercise 3. The naive gate reports PASS on a zero-byte file. Name the inference that is wrong.

Exercise 4. Why is "zero rows parsed" an error rather than a result?

Exercise 5. renamed.rpt passes every stage-2 check. Explain how, including why it parsed 1 path rather than 0.

Exercise 6. Why can't more checks of the stage-2 kind catch the renamed field?

Exercise 7. State the closing principle in one sentence.

Exercise 8. Why would counting Startpoint: lines with the parser's own pattern prove nothing?

Exercise 9. truncated.rpt shows 1 block and 0 parsed paths. What does that tell you about where the file was cut?

Exercise 10. Name one thing a count cross-check cannot catch, and what does catch it.

Where this fits

Part of the Tcl curriculum.