awk Part III · Module 10 · Timing and STA Report Analysis
Slack Extraction
A slack line carries two statements of the same fact: a word — MET or VIOLATED — and a number whose sign ought to say the same thing. Almost every parser trusts the number, because a number is what you want to sort and compare. This chapter shows two real report shapes where the number and the word disagree, in opposite directions, and one where a sign-based count produces exactly the right total while being wrong about which paths failed. The report's own rounding is what destroys the sign, and the word is what survives it.
Advanced18 min readawkPart IIISTASlackNumeric Comparison
Part III · Module 10 · Chapter 10.3 · Slack Extraction
1. Why This Matters
The slack is the answer. Everything upstream — the path, the clock, the delay table — is context for one number and one word.
Chapter 10.2 made paths into records. This chapter takes the number, and it is marked Critical for a reason that is not obvious: the obvious extraction is wrong on report shapes that occur routinely, and it is wrong in a way that survives a sanity check on the totals. A timing signoff script that counts violations by the sign of the slack can report the right count of failures and the wrong set of failures.
2. The Slack Line
This chapter continues on timing.rpt, the three-path synthetic report listed in full in Chapter 10.1 section 2 — the whole module shares one fixture so that a result in a later chapter can be compared against an earlier one. Section 3 adds a second fixture for the shapes timing.rpt does not contain.
Three fields on a well-formed line:
awk '/^ *slack/ { printf "NF=%d |$1=%s| |$2=%s| |$3=%s| |$NF=%s|\n", NF, $1, $2, $3, $NF }' timing.rptNF=3 |$1=slack| |$2=(VIOLATED)| |$3=-0.018| |$NF=-0.018|
NF=3 |$1=slack| |$2=(MET)| |$3=0.067| |$NF=0.067|
NF=3 |$1=slack| |$2=(VIOLATED)| |$3=-0.023| |$NF=-0.023|$1 is the marker, $2 is the verdict in parentheses, $3 is the value. On these lines $3 and $NF are the same field, which is exactly the coincidence that section 4 breaks.
The extraction every later chapter in this module uses:
awk '/^ *slack/ && NF >= 3 { printf "%-12s %8.3f\n", $2, $3 }' timing.rpt(VIOLATED) -0.018
(MET) 0.067
(VIOLATED) -0.023Note the NF >= 3 guard in the pattern. Section 4 is the argument for it.
3. The Six Shapes a Slack Line Takes
A real report does not only emit the well-formed case. This fixture collects every shape:
slack (MET) 0.067
slack (VIOLATED) -0.018
slack (MET) 1e+30
slack (INFEASIBLE)
slack (MET) 0.000
slack (VIOLATED) -0.000awk '{ printf "NF=%d verdict=%-14s value=%s\n", NF, $2, (NF >= 3 ? $3 : "<none on the line>") }' odd.rptNF=3 verdict=(MET) value=0.067
NF=3 verdict=(VIOLATED) value=-0.018
NF=3 verdict=(MET) value=1e+30
NF=2 verdict=(INFEASIBLE) value=<none on the line>
NF=3 verdict=(MET) value=0.000
NF=3 verdict=(VIOLATED) value=-0.000What each one is:
0.067/-0.018— the ordinary cases.1e+30— an unconstrained path. The timing tool had no constraint to check against, so it reports a sentinel. The path is not passing; it is unmeasured.(INFEASIBLE)with no number — the tool could not compute a slack at all.NFis 2.0.000— exactly met, or met by less than the display precision.-0.000— a violation whose magnitude is smaller than the display precision. Section 5 is about this line.
Everything in this chapter follows from rows 3, 4 and 6 — the three that the obvious code gets wrong.
4. $NF on a Line With No Value
The guard first, because it is cheap and it explains itself:
awk '/^ *slack/ { printf "%-14s NF=%d -> %s\n", $2, NF, (NF >= 3 ? "has a value: " $3 : "NO VALUE -- $NF is the verdict, not a number") }' odd.rpt(MET) NF=3 -> has a value: 0.067
(VIOLATED) NF=3 -> has a value: -0.018
(MET) NF=3 -> has a value: 1e+30
(INFEASIBLE) NF=2 -> NO VALUE -- $NF is the verdict, not a number
(MET) NF=3 -> has a value: 0.000
(VIOLATED) NF=3 -> has a value: -0.000On the (INFEASIBLE) line, $NF is $2. The field that usually holds the value now holds the verdict word, and nothing about the code notices.
Then the comparison. $NF < 0 on all six:
awk '{ printf "NF=%d $NF=%-14s | $NF<0 -> %-5s | $NF+0<0 -> %-5s | ($NF+0)=%s\n", NF, $NF, ($NF < 0 ? "TRUE" : "false"), ($NF + 0 < 0 ? "TRUE" : "false"), $NF + 0 }' odd.rptNF=3 $NF=0.067 | $NF<0 -> false | $NF+0<0 -> false | ($NF+0)=0.067
NF=3 $NF=-0.018 | $NF<0 -> TRUE | $NF+0<0 -> TRUE | ($NF+0)=-0.018
NF=3 $NF=1e+30 | $NF<0 -> false | $NF+0<0 -> false | ($NF+0)=1.00000000000000001988462483866e+30
NF=2 $NF=(INFEASIBLE) | $NF<0 -> TRUE | $NF+0<0 -> false | ($NF+0)=0
NF=3 $NF=0.000 | $NF<0 -> false | $NF+0<0 -> false | ($NF+0)=0
NF=3 $NF=-0.000 | $NF<0 -> false | $NF+0<0 -> false | ($NF+0)=0$NF < 0 is TRUE for (INFEASIBLE). An unconstrained path is counted as a timing violation.
Why: (INFEASIBLE) is not a numeric string, so awk cannot apply the numeric-comparison rule from Chapter 5.2 section 7 and falls back to comparing it as a string against "0". ( is character 40 and 0 is character 48, so ( sorts first and the comparison is true.
awk 'BEGIN { print "(INFEASIBLE) < 0 ->", ("(INFEASIBLE)" < 0 ? "TRUE" : "false") }'(INFEASIBLE) < 0 -> TRUEAdding + 0 fixes this particular line — $NF + 0 < 0 is false — because it forces the numeric interpretation and a non-numeric string converts to 0. That is Chapter 8.5's rule, and it is necessary but not sufficient, because the next section breaks the numeric version too.
5. The Report Rounds Away the Sign
Row 6 of the fixture is slack (VIOLATED) -0.000. The word says the path fails. The number does not:
awk 'BEGIN { s = sprintf("%.3f", -0.0004); printf "re-read as text: [%s] and [%s]+0 < 0 -> %s\n", s, s, (s + 0 < 0 ? "TRUE" : "false") }'re-read as text: [-0.000] and [-0.000]+0 < 0 -> falseThis is not a contrived line. It is what a report prints when the slack is a genuine violation smaller than the display precision:
awk 'BEGIN { for (i = 1; i <= 4; i++) { v = -0.0004 / i; printf "true slack %-12.8f prints as %.3f sign of printed value: %s\n", v, v, (v + 0 < 0 ? "negative" : "NOT negative") } }'true slack -0.00040000 prints as -0.000 sign of printed value: negative
true slack -0.00020000 prints as -0.000 sign of printed value: negative
true slack -0.00013333 prints as -0.000 sign of printed value: negative
true slack -0.00010000 prints as -0.000 sign of printed value: negativeFour genuinely negative slacks. All four print as -0.000. Inside the tool the sign is intact — that is why every one of those rows reads negative. The loss happens at the %.3f, in the report, before awk ever sees the line. By the time the text reaches a parser the sign is a minus character in front of a zero, and reading it as a number discards it.
The verdict word did not go through that rounding. The tool wrote VIOLATED from the value it still had.
6. Three Methods, Three Counts
Count the violations in odd.rpt three ways:
printf '%-26s ' 'naive $NF < 0'; awk '/^ *slack/ { if ($NF < 0) v++ } END { printf "%d violating\n", v+0 }' odd.rpt
printf '%-26s ' 'numeric $3 + 0 < 0'; awk '/^ *slack/ { if ($3 + 0 < 0) v++ } END { printf "%d violating\n", v+0 }' odd.rpt
printf '%-26s ' 'verdict word'; awk '/^ *slack/ { if ($2 == "(VIOLATED)") v++ } END { printf "%d violating\n", v+0 }' odd.rptnaive $NF < 0 2 violating
numeric $3 + 0 < 0 1 violating
verdict word 2 violatingRead that carefully, because it is the chapter's sharpest result.
The numeric method — the one that looked like the fix in section 4 — reports 1. It misses the -0.000 violation entirely.
The naive method reports 2, which is the correct total. It is also wrong about which paths those are:
awk '/^ *slack/ { n++; a = ($NF < 0); b = ($3 + 0 < 0); c = ($2 == "(VIOLATED)"); printf "row %d %-14s naive=%-3s numeric=%-3s word=%-3s [%s]\n", n, $2, (a?"YES":"no"), (b?"YES":"no"), (c?"YES":"no"), (a == c && b == c ? "all agree" : "A METHOD IS WRONG HERE") }' odd.rptrow 1 (MET) naive=no numeric=no word=no [all agree]
row 2 (VIOLATED) naive=YES numeric=YES word=YES [all agree]
row 3 (MET) naive=no numeric=no word=no [all agree]
row 4 (INFEASIBLE) naive=YES numeric=no word=no [A METHOD IS WRONG HERE]
row 5 (MET) naive=no numeric=no word=no [all agree]
row 6 (VIOLATED) naive=no numeric=no word=YES [A METHOD IS WRONG HERE]awk '/^ *slack/ { n++; a = ($NF < 0); c = ($2 == "(VIOLATED)"); if (a != c) { d++; printf "row %d disagrees: naive says %s, the tool says %s\n", n, (a ? "VIOLATED" : "MET"), (c ? "VIOLATED" : "not VIOLATED") } } END { printf "%d row(s) disagree, yet both totals are 2\n", d+0 }' odd.rptrow 4 disagrees: naive says VIOLATED, the tool says not VIOLATED
row 6 disagrees: naive says MET, the tool says VIOLATED
2 row(s) disagree, yet both totals are 2Two errors in opposite directions, cancelling to the right total. The naive method invents a violation on row 4 and loses one on row 6. Any check that compares counts — against the tool's own summary, against last night's run, against a reviewer's expectation — passes.
And on the well-formed report all three methods agree, which is why this is never found during development:
printf '%-26s ' 'naive $NF < 0'; awk '/^ *slack/ { if ($NF < 0) v++ } END { printf "%d violating\n", v+0 }' timing.rpt
printf '%-26s ' 'numeric $3 + 0 < 0'; awk '/^ *slack/ { if ($3 + 0 < 0) v++ } END { printf "%d violating\n", v+0 }' timing.rpt
printf '%-26s ' 'verdict word'; awk '/^ *slack/ { if ($2 == "(VIOLATED)") v++ } END { printf "%d violating\n", v+0 }' timing.rptnaive $NF < 0 2 violating
numeric $3 + 0 < 0 2 violating
verdict word 2 violatingThree methods, one answer, on the input you test against. The divergence needs a report containing an unconstrained path or a sub-precision violation — which is to say, a real one.
7. Printing the Number Back Out
Two formatting hazards, both from the unconstrained sentinel:
awk '/^ *slack/ { printf "%%s->[%s] %%.3f->[%.3f] %%g->[%g]\n", $NF + 0, $NF + 0, $NF + 0 }' odd.rpt%s->[0.067] %.3f->[0.067] %g->[0.067]
%s->[-0.018] %.3f->[-0.018] %g->[-0.018]
%s->[1.00000000000000001988462483866e+30] %.3f->[1000000000000000019884624838656.000] %g->[1e+30]
%s->[0] %.3f->[0.000] %g->[0]
%s->[0] %.3f->[0.000] %g->[0]
%s->[0] %.3f->[0.000] %g->[0]%s of 1e+30 is 31 significant digits, and %.3f of it is 34 characters. Either one destroys a column in a report.
What makes that surprising is that awk's output formats are set to six significant digits:
awk 'BEGIN { x = 1e+30; print "print:", x; printf "concat: %s\n", x ""; print "CONVFMT=" CONVFMT, "OFMT=" OFMT }'print: 1.00000000000000001988462483866e+30
concat: 1.00000000000000001988462483866e+30
CONVFMT=%.6g OFMT=%.6gOFMT and CONVFMT are both %.6g and neither applied. The reason is the rule Chapter 5.2 section 9 introduced: those formats govern non-integral values only. 1e+30 has no fractional part, so awk prints it as an integer — and at that magnitude an integer needs 31 digits.
The second hazard is quieter. %s of a 0.000 slack prints 0, so a report that showed three decimals comes out with none. %.3f preserves what the input showed.
Both are solved by formatting explicitly and special-casing the sentinel, which section 8 does.
8. The Robust Extractor
# slack.awk — classify every slack line, word-first.
#
# Usage: awk -f slack.awk timing.rpt
# Exit: 0 clean 1 a word/sign disagreement 2 a line shape it cannot read
BEGIN { UNCONSTRAINED = 1e29 }
/^ *slack/ {
n++
word = $2
gsub(/[()]/, "", word)
if (NF < 3) { shape++; printf "%-4d %-14s %-10s [no value on the line]\n", n, word, "-"; next }
val = $3 + 0
if (word == "VIOLATED") cls = "VIOLATED"
else if (word == "MET") cls = (val > UNCONSTRAINED ? "UNCONSTRAINED" : "MET")
else { shape++; cls = "UNKNOWN" }
disagree = (word == "VIOLATED") != (val < 0)
if (disagree && cls != "UNCONSTRAINED") bad++
printf "%-4d %-14s %-10s %s\n", n, cls, (val > UNCONSTRAINED ? "1e+30" : sprintf("%.3f", val)),
(disagree && cls != "UNCONSTRAINED" ? "[word and sign disagree -- trust the word]" : "[ok]")
}
END {
printf "# %d slack line(s), %d disagreement(s), %d unreadable shape(s)\n", n + 0, bad + 0, shape + 0
if (shape) exit 2
if (bad) exit 1
}awk -f slack.awk timing.rpt
echo "status: $?"1 VIOLATED -0.018 [ok]
2 MET 0.067 [ok]
3 VIOLATED -0.023 [ok]
# 3 slack line(s), 0 disagreement(s), 0 unreadable shape(s)
status: 0Three clean paths, and nothing in the way on a well-formed report. The same script on the awkward fixture:
awk -f slack.awk odd.rpt
echo "status: $?"1 MET 0.067 [ok]
2 VIOLATED -0.018 [ok]
3 UNCONSTRAINED 1e+30 [ok]
4 INFEASIBLE - [no value on the line]
5 MET 0.000 [ok]
6 VIOLATED 0.000 [word and sign disagree -- trust the word]
# 6 slack line(s), 1 disagreement(s), 1 unreadable shape(s)
status: 2Every one of the three awkward rows is named rather than silently miscounted:
- Row 3 is
UNCONSTRAINED, not a passing path — the sentinel is recognised by magnitude, and its value is printed as1e+30rather than 31 digits. - Row 4 has no value, and the
NF < 3branch reports it instead of comparing a word against zero. - Row 6 is classified
VIOLATEDfrom the word, prints0.000because that is honestly what the rounded value is, and flags the disagreement.
The exit status distinguishes the two problems: 1 means the numbers and the words disagree somewhere, so the magnitudes are suspect; 2 means a line shape the script does not understand, so nothing it produced should be trusted.
9. Common Mistakes
$NF for the value. On a line with no number, $NF is the verdict word — section 4.
$NF < 0 without + 0. A non-numeric field compares as a string, and (INFEASIBLE) sorts below "0", so it reads as a violation.
Trusting the sign instead of the word. -0.000 is a violation the report rounded — section 5.
Checking that the totals agree. The naive method's two errors cancel — section 6.
Treating 1e+30 as a huge pass. It is an unconstrained path; it was never checked.
%s on a slack. 0.000 prints as 0 and 1e+30 prints with 31 digits — section 7.
Assuming OFMT limits the width. It governs non-integral values only, and the sentinel is integral.
Dropping the other bucket when counting verdicts. A report with INFEASIBLE lines silently loses them unless something counts what is neither MET nor VIOLATED — Chapter 6.2 section 8's point about a total that must reconcile.
10. Exercises
Exercise 1 — Extract the verdict and the value
awk '/^ *slack/ && NF >= 3 { printf "%-12s %8.3f\n", $2, $3 }' timing.rpt(VIOLATED) -0.018
(MET) 0.067
(VIOLATED) -0.023Exercise 2 — Count by the word, with an other bucket
awk '/^ *slack/ { w = $2; gsub(/[()]/, "", w); c[w]++ } END { printf "MET=%d VIOLATED=%d INFEASIBLE=%d\n", c["MET"]+0, c["VIOLATED"]+0, c["INFEASIBLE"]+0 }' timing.rpt
awk '/^ *slack/ { w = $2; gsub(/[()]/, "", w); c[w]++ } END { printf "MET=%d VIOLATED=%d INFEASIBLE=%d\n", c["MET"]+0, c["VIOLATED"]+0, c["INFEASIBLE"]+0 }' odd.rptMET=1 VIOLATED=2 INFEASIBLE=0
MET=3 VIOLATED=2 INFEASIBLE=1gsub(/[()]/, "", w) strips the parentheses so the key is a plain word — Chapter 4.4's substitution, used on a copy so $2 itself is untouched.
Exercise 3 — Debug a violation reported as a pass
A signoff script reports zero violations on a report containing one.
printf ' slack (VIOLATED) -0.000\n' > tiny.rpt
awk '/^ *slack/ { if ($3 + 0 < 0) v++ } END { printf "violations found: %d\n", v+0 }' tiny.rptviolations found: 0The sign-based test returns 0 on a line whose own verdict reads VIOLATED. The report printed the slack at three decimals, and the true value was smaller than that, so -0.000 is all the text has left.
awk '/^ *slack/ { if ($2 == "(VIOLATED)") v++ } END { printf "violations found: %d\n", v+0 }' tiny.rptviolations found: 1Exercise 4 — Debug an unconstrained path reported as a violation
printf ' slack (INFEASIBLE)\n' > inf.rpt
awk '/^ *slack/ { printf "$NF is [%s], and $NF < 0 -> %s\n", $NF, ($NF < 0 ? "TRUE" : "false") }' inf.rpt$NF is [(INFEASIBLE)], and $NF < 0 -> TRUE$NF is the verdict word because the line has only two fields, and a non-numeric string compares lexically against "0" — ( sorts before 0.
awk '/^ *slack/ { printf "NF=%d -> %s\n", NF, (NF < 3 ? "no value on this line, nothing to compare" : ($3 + 0 < 0 ? "violation" : "pass")) }' inf.rptNF=2 -> no value on this line, nothing to compareExercise 5 — Engineering application
Produce the slack table a signoff review actually reads: one row per path, the verdict from the word, the magnitude from the number, and a refusal if the two ever disagree.
awk '/^ *Startpoint:/ { s = $2 } /^ *Path Group:/ { g = $3 } /^ *Path Type:/ { ty = $3 } /^ *slack/ { if (NF < 3) { printf "%-24s %-9s %-4s %10s [NO VALUE]\n", s, g, ty, "-"; s = ""; next } w = $2; gsub(/[()]/, "", w); printf "%-24s %-9s %-4s %10.3f [%s]\n", s, g, ty, $3, w; s = "" }' timing.rptu_regfile/dout_reg[3] clk_core max -0.018 [VIOLATED]
u_ctrl/state_reg[1] clk_core max 0.067 [MET]
u_io/pad_reg[0] clk_io min -0.023 [VIOLATED]That is Chapter 10.2's state-carrying pattern with this chapter's extraction dropped into the slack rule — the identity from the header lines, the verdict from the word, the magnitude from the number, and the s = "" that keeps a missing header from mispairing.
The same command run through slack.awk first, to get the verdict counts and the exit status a gate needs:
awk -f slack.awk timing.rpt | tail -1
awk -f slack.awk odd.rpt | tail -1# 3 slack line(s), 0 disagreement(s), 0 unreadable shape(s)
# 6 slack line(s), 1 disagreement(s), 1 unreadable shape(s)One report is safe to summarise and the other is not, and the difference is stated rather than averaged away.
Exercise 6 — Challenge: show that a count check cannot find the bug
A reviewer proposes validating the naive parser by comparing its violation count against the verdict words. Show that the check passes on odd.rpt while the parser is wrong about two paths.
awk '/^ *slack/ { if ($NF < 0) naive++; if ($2 == "(VIOLATED)") word++ } END { printf "naive=%d word=%d -> %s\n", naive+0, word+0, (naive == word ? "CHECK PASSES" : "MISMATCH") }' odd.rptnaive=2 word=2 -> CHECK PASSESThe check passes. Now the same comparison made per row rather than in aggregate:
awk '/^ *slack/ { n++; if (($NF < 0) != ($2 == "(VIOLATED)")) { d++; printf "row %d: naive and word disagree\n", n } } END { printf "rows checked %d, rows disagreeing %d\n", n+0, d+0 }' odd.rptrow 4: naive and word disagree
row 6: naive and word disagree
rows checked 6, rows disagreeing 2Two of six rows are wrong and the aggregate check saw nothing. The errors are in opposite directions, so they cancel in the sum.
The general lesson, and the reason this chapter is marked Critical: a check on a total can only detect errors that do not cancel. Comparing two methods row by row costs one extra condition and detects every disagreement, including the ones that happen to balance.
This is the same argument as Chapter 9.2 section 7, where comparing a parser's record count against UVM's own summary caught a pattern error only because the error was one-directional; and Chapter 8.7 section 9, where a per-case assertion found what a pass-rate could not. Aggregate first, verify per row.
11. Summary
- A slack line is
slack+ verdict + value, and on a well-formed line$3and$NFare the same field. - Guard
NF >= 3. A line with no value makes$NFthe verdict word. $NF < 0is TRUE for(INFEASIBLE)— a lexical comparison, because(sorts before"0". An unconstrained path becomes a violation.+ 0forces the numeric reading, which fixes that line and not the next one.-0.000is a violation the report rounded. The sign was intact inside the tool and lost at the%.3f.- The verdict word is authoritative; the number is evidence. Classify by the word, measure with the number, report disagreements.
1e+30is an unconstrained path, not a large pass. It was never checked.- Three counting methods gave 2, 1 and 2 on the same six lines — and the naive 2 was right in total while wrong on two rows.
%sof a slack loses precision and%sof the sentinel prints 31 digits, becauseOFMTgoverns non-integral values only.- A check on a total cannot find errors that cancel. Compare per row.
The number is now trustworthy. Chapter 10.4 — Setup and Hold Violations uses it, and shows why classifying a violation by the sign of its slack puts setup and hold failures in the same bucket when they have different causes, different owners and different fixes.
Where this fits
Part of the awk curriculum.
