Skip to content
VLSI Mentor

awk Part III · Module 12 · AWK in EDA Automation Pipelines

Parsing Tcl-Generated Reports

In an EDA flow the data starts inside the tool, where a Tcl script can see it as objects, and ends outside the tool, where awk sees only text. The report is the boundary, and which format crosses it is a choice somebody makes — usually without realising they are choosing how hard every downstream script will be. This chapter emits the same three timing paths twice from a real Tcl interpreter, once as a formatted report and once as a delimited file, and compares the awk each one needs: four rules with carried state against one rule with no state at all.

Intermediate16 min readawkPart IIITclTool FlowDelimited Output

Part III · Module 12 · Chapter 12.4 · Parsing Tcl-Generated Reports

1. Why This Matters

Module 10 spent seven chapters on a report_timing block because a logical record spanned many lines with no boundary awk could see. Every one of those difficulties — the carried state, the clear-on-consume, the marker preconditions, the paragraph-mode failure — came from the format, not from the data.

And the format was chosen inside the tool, by a Tcl script, which had the paths as structured objects before it printed anything. That is the leverage this chapter is about: a report designed to be read by a human makes the downstream awk hard, and a report designed to be read by a program makes it trivial. The data is identical.

2. Where the Boundary Is

A Tcl script inside the EDA tool holds the timing paths as objects. It can emit either a formatted report, which needs four awk rules and carried state to parse, or a delimited file, which needs one rule and no state. Both reach the same awk-based metric extraction outside the tool.Tcl, in the toolHolds the paths as objectsFormatted reportReadable by a person; 4 awkrules plus stateDelimited fileReadable by a program; 1awk rule, no stateawk, outsideReduces either one to thesame metricsformatparse12
Figure 1 — The report is the boundary between the tool and the flow. Inside, a Tcl script has the timing paths as objects; outside, awk has only text. The two format choices carry the same data and leave very different work on the far side.

No EDA tool runs in this environment, so the scripts below hold their data as a literal list and say so in their own headers. tclsh is installed, so they are really executed — what is demonstrated is the format decision, which does not depend on where the paths came from.

3. The Same Data, Two Formats

Azvya Education Pvt. Ltd.VLSI Mentor
emit_pretty.tcl — synthetic teaching script. Stands in for a Tcl script inside an EDA tool; the data is a literal list.
# emit_pretty.tcl — emit the paths in the shape a tool's report_timing uses.
set paths {
    {u_regfile/dout_reg[3] u_alu/sum_reg[7]   clk_core max -0.018}
    {u_ctrl/state_reg[1]   u_alu/flag_reg[0]  clk_core max  0.067}
    {u_io/pad_reg[0]       u_io/out_reg[0]    clk_io   min -0.023}
}
puts "****************************************"
puts "Report : timing"
puts "****************************************"
puts ""
foreach p $paths {
    lassign $p st en grp typ slack
    puts "  Startpoint: $st"
    puts "  Endpoint: $en"
    puts "  Path Group: $grp"
    puts "  Path Type: $typ"
    if {$slack < 0} { set v VIOLATED } else { set v MET }
    puts [format "  slack (%s) %38.3f" $v $slack]
    puts ""
}
Azvya Education Pvt. Ltd.VLSI Mentor
emit_tsv.tcl — synthetic teaching script. The same literal data, one delimited record per path.
# emit_tsv.tcl — emit one tab-separated record per path, with a header row.
set paths {
    {u_regfile/dout_reg[3] u_alu/sum_reg[7]   clk_core max -0.018}
    {u_ctrl/state_reg[1]   u_alu/flag_reg[0]  clk_core max  0.067}
    {u_io/pad_reg[0]       u_io/out_reg[0]    clk_io   min -0.023}
}
puts "startpoint\tendpoint\tgroup\ttype\tslack\tverdict"
foreach p $paths {
    lassign $p st en grp typ slack
    set v [expr {$slack < 0 ? "VIOLATED" : "MET"}]
    puts "$st\t$en\t$grp\t$typ\t$slack\t$v"
}
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
tclsh emit_pretty.tcl > pretty.rpt; tclsh emit_tsv.tcl > paths.tsv
cat paths.tsv
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
startpoint	endpoint	group	type	slack	verdict
u_regfile/dout_reg[3]	u_alu/sum_reg[7]	clk_core	max	-0.018	VIOLATED
u_ctrl/state_reg[1]	u_alu/flag_reg[0]	clk_core	max	0.067	MET
u_io/pad_reg[0]	u_io/out_reg[0]	clk_io	min	-0.023	VIOLATED
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
head -9 pretty.rpt
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
****************************************
Report : timing
****************************************

  Startpoint: u_regfile/dout_reg[3]
  Endpoint: u_alu/sum_reg[7]
  Path Group: clk_core
  Path Type: max
  slack (VIOLATED)                                 -0.018

Twenty-one lines against four. The same three paths.

4. One Rule Against Four

The delimited file:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
awk -F'\t' 'NR > 1 { printf "  %-22s %-9s %-4s %8s %s\n", $1, $3, $4, $5, $6 }' paths.tsv
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
  u_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

The formatted report, for the identical output:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
awk '/^  Startpoint:/ { s = $2 } /^  Path Group:/ { g = $3 } /^  Path Type:/ { t = $3 } /^  slack/ { v = $2; gsub(/[()]/, "", v); printf "  %-22s %-9s %-4s %8s %s\n", s, g, t, $NF, v; s = ""; g = ""; t = "" }' pretty.rpt
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
  u_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

Identical results. The difference is everything the second one carries:

delimitedformatted
rules14
carried statenonethree variables
clear-on-consume needednoyes — Chapter 10.2 §7
marker precondition needednoyes — Chapter 10.2 §8
a missing line costsone field, visibly emptythe previous path's value
paragraph modenot neededdefeated by the blank line — Chapter 10.7

5. Choosing the Delimiter Is Part of the Job

A delimited file is only unambiguous if the delimiter cannot appear in a value. Tcl's quoting decides whether it can, and the behaviour is worth measuring because it is the opposite of what the syntax suggests:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
tclsh <<'TCL'
set a {x\ty}
set b "x\ty"
puts "  braces  {x\\ty} -> length [string length $a], contains a real tab: [expr {[string first "\t" $a] >= 0}]"
puts "  quotes  \"x\\ty\" -> length [string length $b], contains a real tab: [expr {[string first "\t" $b] >= 0}]"
TCL
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
  braces  {x\ty} -> length 4, contains a real tab: 0
  quotes  "x\ty" -> length 3, contains a real tab: 1

Braces suppress backslash substitution, so {x\ty} is four characters — a literal backslash and a t. Only the double-quoted form produces a real tab. A first attempt at the collision below used braces and did not collide at all.

With quotes, a value can carry the delimiter:

Azvya Education Pvt. Ltd.VLSI Mentor
emit_bad.tcl — synthetic teaching script. One startpoint deliberately contains a tab.
set rows [list \
  [list "u_regfile/dout_reg\[3\]" "u_alu/sum_reg\[7\]" clk_core max -0.018] \
  [list "u_ctrl/state\treg\[1\]"  "u_alu/flag_reg\[0\]" clk_core max 0.067] ]
puts "startpoint\tendpoint\tgroup\ttype\tslack"
foreach r $rows {
    lassign $r st en grp typ slack
    puts "$st\t$en\t$grp\t$typ\t$slack"
}
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
tclsh emit_bad.tcl > bad.tsv
cat -t bad.tsv
awk -F'\t' '{ printf "  line %d: NF=%d\n", NR, NF }' bad.tsv
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
startpoint^Iendpoint^Igroup^Itype^Islack
u_regfile/dout_reg[3]^Iu_alu/sum_reg[7]^Iclk_core^Imax^I-0.018
u_ctrl/state^Ireg[1]^Iu_alu/flag_reg[0]^Iclk_core^Imax^I0.067
  line 1: NF=5
  line 2: NF=5
  line 3: NF=6

NF=6 on the third line, and the consequence:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
awk -F'\t' 'NR > 1 { printf "  row %d: startpoint=[%s] slack=[%s]\n", NR-1, $1, $5 }' bad.tsv
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
  row 1: startpoint=[u_regfile/dout_reg[3]] slack=[-0.018]
  row 2: startpoint=[u_ctrl/state] slack=[max]

The startpoint is truncated to u_ctrl/state and the slack field holds max — the path type, shifted one position by the extra tab. A word where a number belongs.

That is the shape Chapter 11.2 section 3 measured, and the check it suggested catches this one without knowing anything about the report:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
awk -F'\t' 'NR > 1 { bad = ($5 + 0 != $5) || NF != 5; printf "  row %d: NF=%d slack=[%s] -> %s\n", NR-1, NF, $5, (bad ? "REJECT" : "ok") }' bad.tsv
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
  row 1: NF=5 slack=[-0.018] -> ok
  row 2: NF=6 slack=[max] -> REJECT

Two assertions, both one expression: the field count must be the expected one, and a numeric field must be numeric. A delimited format makes both available; a formatted report makes neither.

The durable fixes belong on the Tcl side:

  • Pick a delimiter that cannot occur. A tab is safe for register names and unsafe for free text; an ASCII unit separator (\x1f) is safe for both.
  • Reject or escape at emission. A Tcl script that knows a value contains the delimiter can say so, where awk can only guess.
  • Emit the field count in the header and let the reader check it, which is what the NF != 5 test above does with a count hardcoded.
  • Write the separator as an octal escape on the awk side. -F takes \037 and not \x1f — Exercise 6 measures it.

6. Common Mistakes

Accepting the formatted report as the only interface. If you own the Tcl, emit a delimited file too — section 4.

Treating Module 10's machinery as inherent to timing analysis. It is the cost of one format choice.

Assuming {...} in Tcl processes escapes. Braces suppress substitution; {x\ty} is four characters — section 5.

Choosing a delimiter that can appear in a value. One tab shifted the slack field onto the path type.

Not checking NF. A delimited format's field count is a free precondition.

Not asserting the type of a numeric field. slack=max is detectable; a wrong number is not.

Dropping the header row. NR > 1 is one condition and the header names the fields for the next reader.

Escaping on the awk side. The Tcl script knows the value; awk only sees the damage.

7. Exercises

Exercise 1 — Reduce the delimited file

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
awk -F'\t' 'NR > 1 { n++; if ($6 == "VIOLATED") v++ } END { printf "%d path(s), %d violating\n", n+0, v+0 }' paths.tsv
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
3 path(s), 2 violating

One rule, two counters, no state. NR > 1 skips the header and $6 is the verdict column the emitter wrote — a field lookup rather than a parse.

Per path, with every field addressed by position:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
awk -F'\t' 'NR > 1 { printf "  %-22s %-9s %-4s %8s %s\n", $1, $3, $4, $5, $6 }' paths.tsv
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
  u_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

Exercise 2 — The same reduction on the formatted report

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
awk '/^  slack/ { n++; v = $2; gsub(/[()]/, "", v); if (v == "VIOLATED") b++ } END { printf "%d path(s), %d violating\n", n+0, b+0 }' pretty.rpt
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
3 path(s), 2 violating

Same answer, and the rule needs gsub to strip the parentheses the formatting added.

Exercise 3 — Debug a slack field holding a word

A metrics job reports a slack of max.

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
awk -F'\t' 'NR > 1 { printf "  row %d: NF=%d slack=[%s]\n", NR-1, NF, $5 }' bad.tsv
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
  row 1: NF=5 slack=[-0.018]
  row 2: NF=6 slack=[max]

Six fields where five were expected: a value contains the delimiter, so every field after it is shifted.

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
cat -t bad.tsv | sed -n '3p'
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
u_ctrl/state^Ireg[1]^Iu_alu/flag_reg[0]^Iclk_core^Imax^I0.067

The startpoint u_ctrl/state reg[1] carries a tab, which cat -t shows as ^I.

Exercise 4 — Debug a format you cannot change

A vendor tool emits only the formatted report, and a reduction built on it reports a path twice.

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
grep -v '^  Startpoint: u_ctrl' pretty.rpt > noctrl.rpt
awk '/^  Startpoint:/ { s = $2 } /^  slack/ { printf "  %-22s %8s\n", s, $NF }' noctrl.rpt
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
  u_regfile/dout_reg[3]    -0.018
  u_regfile/dout_reg[3]     0.067
  u_io/pad_reg[0]          -0.023

Chapter 10.2's mispairing, reproduced on a Tcl-generated report: the second path has no Startpoint: line, so s still holds the first path's value.

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
awk '/^  Startpoint:/ { s = $2 } /^  slack/ { printf "  %-22s %8s\n", (s == "" ? "<MISSING>" : s), $NF; s = "" }' noctrl.rpt
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
  u_regfile/dout_reg[3]    -0.018
  <MISSING>                 0.067
  u_io/pad_reg[0]          -0.023

Exercise 5 — Engineering application

A Tcl emitter that produces both formats from one data structure, and a reader that validates the delimited one before using it.

Azvya Education Pvt. Ltd.VLSI Mentor
emit_both.tcl — synthetic teaching script.
# emit_both.tcl — emit the same paths as a human report AND a delimited file.
#
# Usage:  tclsh emit_both.tcl <pretty-out> <tsv-out>
#
# The delimiter is an ASCII unit separator (\x1f), which cannot occur in a
# register name or in free text -- see section 5. The header row names the
# fields and the record count is emitted as a trailer so the reader can check
# it (the same marker-count precondition as Chapter 10.2 section 8).

set paths {
    {u_regfile/dout_reg[3] u_alu/sum_reg[7]   clk_core max -0.018}
    {u_ctrl/state_reg[1]   u_alu/flag_reg[0]  clk_core max  0.067}
    {u_io/pad_reg[0]       u_io/out_reg[0]    clk_io   min -0.023}
}
set US "\x1f"

set fp [open [lindex $argv 0] w]
puts $fp "****************************************"
puts $fp "Report : timing"
puts $fp "****************************************"
puts $fp ""
foreach p $paths {
    lassign $p st en grp typ slack
    set v [expr {$slack < 0 ? "VIOLATED" : "MET"}]
    puts $fp "  Startpoint: $st"
    puts $fp "  Endpoint: $en"
    puts $fp "  Path Group: $grp"
    puts $fp "  Path Type: $typ"
    puts $fp [format "  slack (%s) %38.3f" $v $slack]
    puts $fp ""
}
close $fp

set fp [open [lindex $argv 1] w]
puts $fp [join {startpoint endpoint group type slack verdict} $US]
set n 0
foreach p $paths {
    lassign $p st en grp typ slack
    set v [expr {$slack < 0 ? "VIOLATED" : "MET"}]
    foreach f [list $st $en $grp $typ $slack $v] {
        if {[string first $US $f] >= 0} {
            puts stderr "ERROR: field contains the delimiter: $f"
            exit 2
        }
    }
    puts $fp [join [list $st $en $grp $typ $slack $v] $US]
    incr n
}
puts $fp "# records $n"
close $fp
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
tclsh emit_both.tcl both.rpt both.usv
awk -F'\037' 'NR == 1 { want = NF; next } /^# records/ { split($0, t, " "); declared = t[3]; next } { n++; if (NF != want) bad++; if ($5 + 0 != $5) nonnum++ } END { printf "%d record(s), header declares %d field(s)\n", n, want; printf "declared %s, read %d -> %s\n", declared, n, (declared + 0 == n ? "AGREE" : "DISAGREE"); printf "%d field-count error(s), %d non-numeric slack(s)\n", bad+0, nonnum+0 }' both.usv
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
3 record(s), header declares 6 field(s)
declared 3, read 3 -> AGREE
0 field-count error(s), 0 non-numeric slack(s)

Four checks, all from the format rather than from knowledge of timing analysis: the header declares the field count, the trailer declares the record count, every record's NF is compared against the header, and the slack field is asserted numeric. None of them is available in the formatted report.

The emitter refuses rather than corrupting, which is the part that cannot be done downstream:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
awk -F'\037' 'NR == 1 { printf "header: %d field(s): ", NF; for (i = 1; i <= NF; i++) printf "[%s]", $i; print "" }' both.usv
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
header: 6 field(s): [startpoint][endpoint][group][type][slack][verdict]

Exercise 6 — Challenge: show that the formatted report cannot carry the same guarantees

both.usv supports four preconditions. Show that none of them can be reconstructed from both.rpt.

First a portability detail that has to be settled before any of it works. The emitter wrote a real 0x1F byte:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
head -1 both.usv | od -c | head -2
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
0000000    s   t   a   r   t   p   o   i   n   t 037   e   n   d   p   o
0000020    i   n   t 037   g   r   o   u   p 037   t   y   p   e 037   s

037 is the octal rendering of 0x1F, so the file is right. But awk's -F does not accept a hex escape:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
printf 'a\037b\037c\n' > us.txt
printf "  -F hex NF=%s   -F octal NF=%s\n" "$(awk -F'\x1f' '{ print NF }' us.txt)" "$(awk -F'\037' '{ print NF }' us.txt)"
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
  -F hex NF=1   -F octal NF=3

One field against three. POSIX awk recognises octal escapes in a string and not \x, so a separator written the way C or Tcl would write it silently fails to split anything — every record becomes a single field, $5 is empty, and section 5's type assertion then fires on every row for entirely the wrong reason. Use \037, or pass the byte itself with -F"$(printf '\037')".

With that settled, both numbers are readable from the delimited file and neither from the report:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
awk 'END { printf "  formatted report: %d lines, no field count, no record count\n", NR }' both.rpt
awk -F'\037' 'NR == 1 { f = NF } /^# records/ { split($0, t, " "); r = t[3] } END { printf "  delimited file: %s field(s) declared, %s record(s) declared\n", f, r }' both.usv
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
  formatted report: 22 lines, no field count, no record count
  delimited file: 6 field(s) declared, 3 record(s) declared

The trailer is split on whitespace rather than on the delimiter, because # records 3 is a comment rather than a record and contains no separator at all — a small cost of mixing metadata into a delimited stream, and the reason a record count often goes in a sidecar file instead.

The formatted report states neither number, and nothing in it can be derived:

  • Field count is meaningless — its records are not fields.
  • Record count requires already knowing which marker delimits a record, which is the parsing problem.
  • Type assertions require knowing which position holds the slack, which varies with the formatting.

So the delimited file is not merely more convenient. It carries its own validity conditions, and the formatted report cannot. That is the strongest argument for emitting both, and it is a decision available only on the Tcl side of the boundary.

8. Summary

  • The report is the boundary, and its format is chosen inside the tool by a script that has the data as objects.
  • One awk rule against four, for identical output — plus carried state, clear-on-consume and a marker precondition on the formatted side.
  • Every Module 10 hazard was a format consequence, and a delimited file has no milder version of them — it has none.
  • Emit both. The formatted report for a human, the delimited file for every script.
  • Tcl braces suppress backslash substitution: {x\ty} is four characters, "x\ty" is three with a real tab.
  • A delimiter inside a value shifts every later field — NF=6 and slack=[max], a word where a number belongs.
  • Check NF and assert the type. Two one-expression preconditions a delimited format gives you free.
  • Choose a delimiter that cannot occur — a unit separator rather than a tab — and reject at emission, where the value is known.
  • awk's -F takes an octal escape, not a hex one. \037 splits; \x1f leaves one field.
  • The delimited file carries its own validity conditions; the formatted report cannot carry them at all.

The Tcl side of this boundary — writing the script that emits the report in the first place — is the subject of the Tcl track.

Chapter 12.5 — awk With Python takes the next boundary out: when a one-line extraction is the whole job awk is the right tool to call from Python, and when the aggregation starts growing it is the wrong one to keep.

Where this fits

Part of the awk curriculum.