awk Part IV · Module 13 · Real-World AWK Engineering Projects
Project: Coverage Summary Generator
The third script, and the one with the most ways to produce a confident wrong number. It reads per-block coverage across four metrics, aggregates each one over its own population, reproduces the weighted mean the tool reports for functional coverage, flags the blocks under target, and writes both a table for a reviewer and a CSV for a dashboard. On this report the weighted figure is 65.81% and the unweighted one 55.20% — and a coverage gate written the obvious way lets through one block where three should pass.
Advanced19 min readawkPart IVProjectCoverageWeights
Part IV · Module 13 · Chapter 13.3 · Project: Coverage Summary Generator
1. What the Script Has to Do
Four outputs, and three of them have a decision in them:
- A per-block table with every metric, so a reviewer can see where the gaps are.
- One aggregate per metric, each over its own population — never pooled, never averaged.
- The functional figure the tool would report, which is a weighted mean, with the unweighted one beside it because they differ.
- A CSV, written in the same pass, so a dashboard never parses the table.
Module 11 measured a failure for each of those. This chapter builds the script that avoids all of them at once.
2. The Report
awk 'BEGIN {
n = split("u_alu:3:520:560:410:620:88:96:72.50|u_regfile:1:210:210:248:256:18:18:98.00|u_ctrl:2:300:420:190:380:52:88:61.00|u_io:1:96:96:64:64:0:0:|u_dma:2:180:400:120:340:30:70:44.50|u_debug:0:0:120:0:160:0:24:0.00", B, "|")
print "========================================"
print "Coverage Summary : soc_top"
print "Tests merged : 412"
print "========================================"
print ""
for (i = 1; i <= n; i++) {
split(B[i], f, ":")
printf "Block: %s\n Weight: %d\n", f[1], f[2]
printf " Line %9s (%d of %d)\n", (f[4] ? sprintf("%.2f%%", 100*f[3]/f[4]) : "n/a"), f[3], f[4]
printf " Toggle %9s (%d of %d)\n", (f[6] ? sprintf("%.2f%%", 100*f[5]/f[6]) : "n/a"), f[5], f[6]
printf " Branch %9s (%d of %d)\n", (f[8] ? sprintf("%.2f%%", 100*f[7]/f[8]) : "n/a"), f[7], f[8]
printf " Functional %9s\n\n", (f[9] == "" ? "n/a" : sprintf("%.2f%%", f[9]))
lc += f[3]; lt += f[4]; tc += f[5]; tt += f[6]; bc += f[7]; bt += f[8]
}
print "========================================"
printf "TOTAL %-10s %9s (%d of %d)\n", "Line", sprintf("%.2f%%", 100*lc/lt), lc, lt
printf "TOTAL %-10s %9s (%d of %d)\n", "Toggle", sprintf("%.2f%%", 100*tc/tt), tc, tt
printf "TOTAL %-10s %9s (%d of %d)\n", "Branch", sprintf("%.2f%%", 100*bc/bt), bc, bt
print "========================================"
}' > cov.rpt
sed -n '6,12p' cov.rptBlock: u_alu
Weight: 3
Line 92.86% (520 of 560)
Toggle 66.13% (410 of 620)
Branch 91.67% (88 of 96)
Functional 72.50%Six blocks, four metrics each, a per-block weight, and three TOTAL rows the script will reconcile against. Two blocks are deliberately awkward: u_io has (0 of 0) branches and no functional score, and u_debug has weight 0 with nothing covered.
awk '/^Block: / { n++ } /^ Weight: 0$/ { z++ } /n\/a/ { na++ } END { printf " %d block(s), %d at weight 0, %d n/a field(s)\n", n, z+0, na+0 }' cov.rpt
awk '/^TOTAL/ { printf " %-8s %s (%s of %s)\n", $2, $3, substr($4,2), substr($6,1,length($6)-1) }' cov.rpt 6 block(s), 1 at weight 0, 2 n/a field(s)
Line 72.31% (1306 of 1806)
Toggle 56.70% (1032 of 1820)
Branch 63.51% (188 of 296)3. The Three Figures a Reviewer Could Be Given
awk '/^ Line / && $2 != "n/a" { p = $2; sub(/%$/,"",p); s += p+0; n++; c += substr($3,2)+0; t += $5+0 }
END { printf " mean of %d per-block percentages : %6.2f%%\n", n, s/n
printf " aggregate %d of %d counts : %6.2f%%\n", c, t, 100*c/t }' cov.rpt
awk '/^TOTAL Line/ { printf " report TOTAL : %s\n", $3 }' cov.rpt mean of 6 per-block percentages : 68.22%
aggregate 1306 of 1806 counts : 72.31%
report TOTAL : 72.31%68.22% against 72.31%, with the report agreeing on the latter. Chapter 11.4 measured the same error at 76.13% against 81.70%; the gap here is 4.09 points and in the same direction, because averaging gives the small blocks the same vote as the large ones.
So the script aggregates counts. That decision is not negotiable and it is also not sufficient — the next two sections are the parts that still go wrong.
4. A Gate on the Raw Field Passes the Wrong Blocks
awk '/^ Line / && $2 != "n/a" { if ($2 >= 80) a++; if ($2 + 0 >= 80) b++ } END { printf " >= 80 without +0: %d block(s) with +0: %d block(s)\n", a+0, b+0 }' cov.rpt >= 80 without +0: 1 block(s) with +0: 3 block(s)One against three. Chapter 11.4 section 4 measured why: the % makes the field a non-numeric string, so the comparison is lexical and 100.00% sorts below 90. Here it means two fully-or-nearly covered blocks are reported as failing the gate.
awk '/^Block: / { b = $2 } /^ Line / && $2 != "n/a" { printf " %-11s field=[%-8s] raw>=80 %-4s (+0)>=80 %s\n", b, $2, ($2 >= 80 ? "yes" : "no"), ($2 + 0 >= 80 ? "yes" : "no") }' cov.rpt u_alu field=[92.86% ] raw>=80 yes (+0)>=80 yes
u_regfile field=[100.00% ] raw>=80 no (+0)>=80 yes
u_ctrl field=[71.43% ] raw>=80 no (+0)>=80 no
u_io field=[100.00% ] raw>=80 no (+0)>=80 yes
u_dma field=[45.00% ] raw>=80 no (+0)>=80 no
u_debug field=[0.00% ] raw>=80 no (+0)>=80 noThe raw comparison passes the 92.86% block and fails both blocks at 100.00%. The ordering is purely lexical, against the string "80":
awk 'BEGIN { split("92.86% 100.00% 71.43% 45.00% 0.00%", v, " "); for (i=1;i<=5;i++) printf " [%-8s] first char %s, so >= \"80\" lexically: %s\n", v[i], substr(v[i],1,1), (v[i] >= 80 ? "yes" : "no") }' [92.86% ] first char 9, so >= "80" lexically: yes
[100.00% ] first char 1, so >= "80" lexically: no
[71.43% ] first char 7, so >= "80" lexically: no
[45.00% ] first char 4, so >= "80" lexically: no
[0.00% ] first char 0, so >= "80" lexically: noIt is comparing first characters. 9 sorts above 8 so 92.86% passes; 1 sorts below 8 so 100.00% fails. Nothing about the magnitude enters into it.
awk '/^ Line / && $2 != "n/a" { raw = ($2 >= 80); num = ($2 + 0 >= 80); if (raw != num) bad++; n++ } END { printf " %d of %d block(s) get a DIFFERENT verdict from the raw field\n", bad+0, n }' cov.rpt 2 of 6 block(s) get a DIFFERENT verdict from the raw field5. Weighted Against Unweighted
awk '/^Block: / { b = $2 } /^ Weight: / { w[b] = $2 + 0 } /^ Functional / && $2 != "n/a" { f = $2; sub(/%$/,"",f); sc[b] = f + 0; have[b] = 1; if (!(b in seen)) { seen[b] = 1; o[++n] = b } }
END { for (i = 1; i <= n; i++) { b = o[i]
if (w[b] > 0) { num += w[b] * sc[b]; den += w[b] }
unum += sc[b]; uden++ }
printf " weighted mean %6.2f%% over weight %d\n", num/den, den
printf " unweighted mean %6.2f%% over %d scored block(s)\n", unum/uden, uden
printf " difference %+6.2f points\n", num/den - unum/uden }' cov.rpt weighted mean 65.81% over weight 8
unweighted mean 55.20% over 5 scored block(s)
difference +10.61 points65.81% against 55.20%, a gap of ten and a half points, and the cause is one block:
awk '/^Block: / { b = $2 } /^ Weight: / { w = $2 + 0 } /^ Functional / { printf " %-11s weight %d functional %s%s\n", b, w, $2, (w == 0 ? " <- excluded from the weighted mean" : "") }' cov.rpt u_alu weight 3 functional 72.50%
u_regfile weight 1 functional 98.00%
u_ctrl weight 2 functional 61.00%
u_io weight 1 functional n/a
u_dma weight 2 functional 44.50%
u_debug weight 0 functional 0.00% <- excluded from the weighted meanu_debug is at 0.00% with weight 0. The weighted mean drops it entirely; the unweighted mean gives it a full vote. Chapter 11.7 measured this on a covergroup and the consequence is the same here one level up: a weight of zero removes a block from the score while its uncovered bins remain in the count aggregates.
Which is why the script prints both, names every zero-weighted block, and says in the output that their bins are still in the line, toggle and branch totals. That is Chapter 11.8's rule — an aggregation that changes the answer belongs in the report.
6. The Complete Script
# covsum.awk — coverage summary generator.
#
# Usage: awk -f covsum.awk cov.rpt
# awk -v gate=80 -v csv=cov.csv -f covsum.awk cov.rpt
# Exit: 0 every weighted block is at or above the gate
# 1 a weighted block is below the gate
# 2 the parse does not reconcile with the report's own TOTAL rows
#
# Design notes, each from a measured failure in Module 11:
# - Aggregates COUNTS, never percentages: averaging per-block percentages
# gave 68.22% against a true 72.31%. (11.4)
# - Converts into a variable before comparing: a field reading 100.00%
# fails a >= 80 gate because the percent sign makes it a string. (11.4)
# - Treats n/a as "no coverable items", not 0%: branch coverage had three
# different answers depending on which. (11.4)
# - Reproduces the tool's WEIGHTED mean for functional coverage, and reports
# the unweighted one beside it, because they differ by 10.6 points. (11.7)
# - Names every zero-weighted block: weight 0 removes a block from the score
# while its uncovered bins remain in the count aggregates. (11.7)
# - Reconciles against the report's own TOTAL rows -- the only check that
# detects a parse error. (11.8)
BEGIN { if (gate == "") gate = 80; split("Line Toggle Branch", metric, " ") }
/^Block: / { b = $2; if (!(b in bseen)) { bseen[b] = 1; border[++nb] = b } }
/^ Weight: / { wt[b] = $2 + 0 }
/^ (Line|Toggle|Branch) / {
m = $1
if ($2 == "n/a") { navail[b, m] = 1; next }
c = substr($3, 2) + 0; t = $5 + 0
cov[b, m] = c; tot[b, m] = t
aggc[m] += c; aggt[m] += t
}
/^ Functional / {
if ($2 == "n/a") { fna[b] = 1; next }
f = $2; sub(/%$/, "", f); fscore[b] = f + 0; fhave[b] = 1
}
/^TOTAL / { rc[$2] = substr($4, 2) + 0; rt[$2] = $6 + 0 }
END {
if (nb == 0) { print "ERROR: no blocks found" > "/dev/stderr"; exit 2 }
# reconcile first -- a table built from a bad parse is worse than none
for (i = 1; i <= 3; i++) { m = metric[i]
if (m in rc && (aggc[m] != rc[m] || aggt[m] != rt[m])) {
printf "ERROR: %s parsed %d of %d, report says %d of %d\n", m, aggc[m], aggt[m], rc[m], rt[m] > "/dev/stderr"
bad++
}
}
if (bad) exit 2
printf "%-11s %3s %18s %18s %18s %10s\n", "block", "wt", "line", "toggle", "branch", "functional"
for (j = 1; j <= nb; j++) {
b = border[j]
out = sprintf("%-11s %3d", b, wt[b])
for (i = 1; i <= 3; i++) { m = metric[i]
if ((b SUBSEP m) in navail || tot[b, m] == 0) out = out sprintf(" %8s %9s", "n/a", "(0 of 0)")
else out = out sprintf(" %7.2f%% %9s", 100 * cov[b, m] / tot[b, m], sprintf("(%d/%d)", cov[b, m], tot[b, m]))
}
out = out sprintf(" %9s", (b in fhave ? sprintf("%.2f%%", fscore[b]) : "n/a"))
if (b in fhave && wt[b] > 0 && fscore[b] < gate) { out = out " BELOW"; nbelow++ }
if (wt[b] == 0) { out = out " [weight 0]"; nzero++ }
print out
}
print ""
print "AGGREGATE (counts summed, never percentages averaged)"
for (i = 1; i <= 3; i++) { m = metric[i]
printf " %-8s %5d of %5d = %6.2f%%\n", m, aggc[m], aggt[m], 100 * aggc[m] / aggt[m] }
for (j = 1; j <= nb; j++) { b = border[j]
if (b in fhave && wt[b] > 0) { fnum += wt[b] * fscore[b]; fden += wt[b] }
if (b in fhave) { unum += fscore[b]; uden++ } }
printf " %-8s weighted mean %6.2f%% over weight %d (unweighted mean of %d block(s): %6.2f%%)\n",
"Funct", (fden ? fnum / fden : 0), fden, uden, (uden ? unum / uden : 0)
printf "#\n# %d block(s); %d below the %d%% functional gate; %d at weight 0\n", nb, nbelow + 0, gate, nzero + 0
if (nzero) { printf "# weight 0 blocks are absent from the weighted mean while their bins remain in the aggregates:"
for (j = 1; j <= nb; j++) if (wt[border[j]] == 0) printf " %s", border[j]
print "" }
if (csv != "") {
print "block,weight,line_cov,line_tot,toggle_cov,toggle_tot,branch_cov,branch_tot,functional" > csv
for (j = 1; j <= nb; j++) { b = border[j]
printf "%s,%d,%d,%d,%d,%d,%d,%d,%s\n", b, wt[b],
cov[b,"Line"] + 0, tot[b,"Line"] + 0, cov[b,"Toggle"] + 0, tot[b,"Toggle"] + 0,
cov[b,"Branch"] + 0, tot[b,"Branch"] + 0, (b in fhave ? sprintf("%.2f", fscore[b]) : "") > csv }
close(csv)
printf "# csv written to %s\n", csv
}
if (nbelow) exit 1
}Three structural points. The reconciliation runs before any output, so a bad parse produces a diagnostic and nothing else. The table and the CSV come from one pass over the same arrays, so a dashboard never re-parses the formatted table — Chapter 12.4's rule, and the mistake Chapter 13.1 made in its own exercises. And close(csv) matters: without it the file may still be buffered when the script exits, which Chapter 7.3 measured.
7. Running It
awk -v gate=80 -v csv=cov.csv -f covsum.awk cov.rpt
echo "status: $?"block wt line toggle branch functional
u_alu 3 92.86% (520/560) 66.13% (410/620) 91.67% (88/96) 72.50% BELOW
u_regfile 1 100.00% (210/210) 96.88% (248/256) 100.00% (18/18) 98.00%
u_ctrl 2 71.43% (300/420) 50.00% (190/380) 59.09% (52/88) 61.00% BELOW
u_io 1 100.00% (96/96) 100.00% (64/64) n/a (0 of 0) n/a
u_dma 2 45.00% (180/400) 35.29% (120/340) 42.86% (30/70) 44.50% BELOW
u_debug 0 0.00% (0/120) 0.00% (0/160) 0.00% (0/24) 0.00% [weight 0]
AGGREGATE (counts summed, never percentages averaged)
Line 1306 of 1806 = 72.31%
Toggle 1032 of 1820 = 56.70%
Branch 188 of 296 = 63.51%
Funct weighted mean 65.81% over weight 8 (unweighted mean of 5 block(s): 55.20%)
#
# 6 block(s); 3 below the 80% functional gate; 1 at weight 0
# weight 0 blocks are absent from the weighted mean while their bins remain in the aggregates: u_debug
# csv written to cov.csv
status: 1cat cov.csvblock,weight,line_cov,line_tot,toggle_cov,toggle_tot,branch_cov,branch_tot,functional
u_alu,3,520,560,410,620,88,96,72.50
u_regfile,1,210,210,248,256,18,18,98.00
u_ctrl,2,300,420,190,380,52,88,61.00
u_io,1,96,96,64,64,0,0,
u_dma,2,180,400,120,340,30,70,44.50
u_debug,0,0,120,0,160,0,24,0.00The CSV carries counts, not percentages, so a dashboard can compute any aggregation it wants and none of them is baked in — Chapter 11.8's rule. u_io's functional field is empty rather than 0, which is the only way a consumer can tell "no score" from "a score of zero".
8. The Reconciliation Refusal
awk '/^TOTAL Line/ { sub(/1806/, "9999") } { print }' cov.rpt > badtot.rpt
grep '^TOTAL Line' badtot.rpt
awk -v gate=80 -f covsum.awk badtot.rpt 2>&1 >/dev/null
echo "status: $?"TOTAL Line 72.31% (1306 of 9999)
ERROR: Line parsed 1306 of 1806, report says 1306 of 9999
status: 2The guard fires, which Chapter 11.2 Exercise 6 argued is the only way to know a guard works.
One note on how that mutation was made. An earlier attempt used sed with the full line text typed out and did not match, because the generated column widths differ from what was typed. awk '/^TOTAL Line/ { sub(/1806/, "9999") }' matches on the row and substitutes the number, which does not depend on spacing at all — the same reasoning as Chapter 11.6's match() over a field index.
9. Every Decision and Its Source
| decision | alternative that fails | measured in |
|---|---|---|
| aggregate counts per metric | averaging gave 68.22% against 72.31% | 11.4 |
| convert into a variable, then compare | the raw field misjudged 4 of 6 blocks | 11.4 |
n/a means no coverable items | counting it as 0% gave three different answers | 11.4 |
| reproduce the tool's weighted mean | a bin ratio does not reproduce it | 11.7 |
| print the unweighted mean beside it | they differ by 10.61 points here | 11.7 |
| name every zero-weighted block | its uncovered bins stay in the aggregates | 11.7 |
reconcile against the report's TOTAL | nothing else detects a parse error | 11.8 |
| refuse before emitting anything | a partial table invites use | 10.2 |
| CSV of counts, not percentages | percentages discard the weights | 11.8 |
| empty CSV field for a missing score | 0 and "no score" become indistinguishable | 11.4 |
close(csv) before exit | the file may still be buffered | 7.3 |
| one pass for both outputs | re-parsing your own table shifts fields | 12.4, 13.1 |
10. Common Mistakes
Averaging per-block percentages. 68.22% against a true 72.31% — section 3.
Comparing the raw percentage field. It misjudged four of six blocks — section 4.
Treating n/a as 0%. It means there was nothing to cover.
Reporting the unweighted mean as the coverage. 55.20% against the tool's 65.81%.
Dropping the weight column. A zero-weighted block vanishes from the score.
Pooling metrics. Statements, bits and branches are different populations.
Emitting the table before reconciling. A bad parse should produce no table.
Writing percentages to the CSV. The consumer then cannot re-aggregate.
0 for a missing functional score. Use an empty field.
Re-parsing your own formatted table to build the CSV — section 6.
11. Exercises
Exercise 1 — Blocks below the gate, correctly
awk -v gate=80 '/^Block: / { b = $2 } /^ Weight: / { w = $2 + 0 } /^ Functional / && $2 != "n/a" { p = $2 + 0; if (w > 0 && p < gate) printf " %-11s %6.2f%% (weight %d)\n", b, p, w }' cov.rpt u_alu 72.50% (weight 3)
u_ctrl 61.00% (weight 2)
u_dma 44.50% (weight 2)Three blocks, each with its weight so the reviewer knows which one moves the aggregate most. u_debug at 0.00% is absent because its weight is zero — it is reported separately rather than silently included or silently dropped.
Exercise 2 — The CSV, re-aggregated downstream
awk -v gate=80 -v csv=cov.csv -f covsum.awk cov.rpt > /dev/null
awk -F, 'NR > 1 { lc += $3; lt += $4; if ($9 != "" && $2 > 0) { num += $2 * $9; den += $2 } }
END { printf " from the CSV: line %d of %d = %.2f%%, functional weighted %.2f%%\n", lc, lt, 100*lc/lt, num/den }' cov.csv from the CSV: line 1306 of 1806 = 72.31%, functional weighted 65.81%Both figures reproduced from the CSV alone, which is the test of whether the CSV carried enough — and it did, because it carries counts and weights rather than percentages.
Exercise 3 — Debug a gate that fails a fully covered block
A coverage gate reports u_regfile below an 80% target. Its line coverage is 100.00%.
awk '/^ Line / { printf " NF=%d [%s][%s][%s][%s][%s]\n", NF, $1, $2, $3, $4, $5; exit }' cov.rpt NF=5 [Line][92.86%][(520][of][560)]The field layout is fine — NF=5 with the percentage at $2, the covered count at substr($3, 2) and the denominator at $5. And the conversion is well behaved on every value:
awk '/^Block: / { b = $2 } /^ Line / && $2 != "n/a" { printf " %-11s %-8s -> %g\n", b, $2, $2 + 0 }' cov.rpt u_alu 92.86% -> 92.86
u_regfile 100.00% -> 100
u_ctrl 71.43% -> 71.43
u_io 100.00% -> 100
u_dma 45.00% -> 45
u_debug 0.00% -> 0Six correct conversions — + 0 parses the numeric prefix and stops at the %, which Chapter 11.4 measured. So neither the data nor the conversion is at fault; the defect is entirely in the comparison, and the fix is to compare the converted value:
awk -v gate=80 '/^Block: / { b = $2 } /^ Line / && $2 != "n/a" { p = $2 + 0; if (p < gate) printf " %-11s %7.2f%% below %d%%\n", b, p, gate }' cov.rpt u_ctrl 71.43% below 80%
u_dma 45.00% below 80%
u_debug 0.00% below 80%Three blocks, and neither 100.00% block among them.
Exercise 4 — Debug a weighted figure that excludes a failing block
awk '/^Block: / { b = $2 } /^ Weight: / { w[b] = $2 + 0 } /^ Functional / && $2 != "n/a" { f = $2; sub(/%$/,"",f); sc[b] = f+0; if (!(b in s)) { s[b]=1; o[++n]=b } }
END { for (i=1;i<=n;i++) { b=o[i]; if (w[b] > 0) { a += w[b]*sc[b]; d += w[b] } else printf " excluded: %-11s %6.2f%% at weight 0\n", b, sc[b]; u += sc[b]; uc++ }
printf " weighted %.2f%% (den %d) unweighted %.2f%% (n %d)\n", a/d, d, u/uc, uc }' cov.rpt excluded: u_debug 0.00% at weight 0
weighted 65.81% (den 8) unweighted 55.20% (n 5)One block excluded, and it is the one at 0.00% — which is exactly the block whose exclusion moves the figure most.
Exercise 5 — Engineering application
The script is the application. What remains is the wiring, with the same status discipline as Chapter 13.2:
#!/bin/sh
# check_cov.sh — run the summary generator and route the outcome.
awk -v gate=80 -v csv=cov.csv -f covsum.awk cov.rpt > cov_report.txt 2> cov_errors.txt
status=$?
nbelow=$(awk '/below the/ { print $4 }' cov_report.txt)
case $status in
0) echo "COVERAGE OK: every weighted block at or above the gate" ;;
1) echo "COVERAGE BELOW GATE: $nbelow block(s); see cov_report.txt and cov.csv" ;;
2) echo "COVERAGE REPORT UNUSABLE:"; cat cov_errors.txt ;;
*) echo "GENERATOR FAILED (status $status)" ;;
esac
exit $statussh check_cov.sh
echo "status: $?"COVERAGE BELOW GATE: 3 block(s); see cov_report.txt and cov.csv
status: 1status is captured on the line immediately after the command, and the case reads the variable — so the command substitution that counts the blocks cannot disturb it.
Exercise 6 — Challenge: show that the CSV is sufficient and the table is not
Exercise 2 recomputed both aggregates from the CSV. Show that the same recomputation from the formatted table is not possible.
awk -v gate=80 -v csv=cov.csv -f covsum.awk cov.rpt > cov_report.txt
awk 'NR > 1 && /^u_/ { printf " %-11s field 2 = [%s]\n", $1, $2 }' cov_report.txt | head -3 u_alu field 2 = [3]
u_regfile field 2 = [1]
u_ctrl field 2 = [2]The weight is readable. The counts are not, because they were formatted into a parenthesised pair inside a wider column:
awk 'NR > 1 && /^u_alu/ { printf " NF=%d: ", NF; for (i = 1; i <= NF; i++) printf "[%s]", $i; print "" }' cov_report.txt NF=10: [u_alu][3][92.86%][(520/560)][66.13%][(410/620)][91.67%][(88/96)][72.50%][BELOW]The pair (520/560) is one field containing a slash, so recovering the two numbers needs a split on / after stripping the parentheses — and the field positions shift by one on any row carrying BELOW or [weight 0]:
awk 'NR > 1 && /^u_/ { printf " %-11s NF=%d\n", $1, NF }' cov_report.txt u_alu NF=10
u_regfile NF=9
u_ctrl NF=10
u_io NF=11
u_dma NF=10
u_debug NF=11Three different field counts across six rows — 9, 10, 10, 11, 10, 11 — because the trailing annotations are optional and u_io's n/a (0 of 0) has a different word count from a (520/560) pair. Any index-based parse of this table is wrong on some row.
So the CSV is not a convenience, it is the interface: fixed field count, one record per block, counts rather than percentages, and an empty field where a value is absent. The table is for a person and is deliberately not machine-readable — which is Chapter 12.4's conclusion applied to the script's own output, and the reason section 6 builds both from one pass over the same arrays rather than deriving one from the other.
12. Summary
- Four outputs: the per-block table, one aggregate per metric, the weighted functional figure with the unweighted one beside it, and a CSV.
- Aggregate counts, never percentages — averaging gave 68.22% against a true 72.31%.
- Convert into a variable before comparing. The raw field misjudged four of six blocks and passed the one it passed for the wrong reason.
n/ameans nothing to cover.u_iohas(0 of 0)branches and no functional score.- Weighted 65.81% against unweighted 55.20% — a 10.61-point gap caused by one block at 0.00% with weight 0.
- Name every zero-weighted block and say that its bins remain in the count aggregates.
- Reconcile against the report's own
TOTALrows before emitting anything. - The CSV carries counts and weights, so any aggregation can be recomputed downstream; an empty field distinguishes "no score" from zero.
- Build the table and the CSV in one pass. The table has 9, 10 or 11 fields per row and cannot be parsed by index.
- When a computed value contradicts the input you can see, print the inputs — Exercise 3's
45.00% -> 100was a draft error, not a behaviour.
Chapter 13.4 — Project: Synthesis Area Analyzer builds the fourth script: per-module area and cell counts, compared against a previous run, with the modules that grew listed — and the floating-point comparison that makes "grew" harder to define than it looks.
Where this fits
Part of the awk curriculum.
