Tcl · Module 28
Python Over the Reports
Comparing runs, aggregating and rendering are not things to grow inside a tool session. Hand the numbers over in a file and the second half of the job needs no licence at all.
Intermediate21 min readtcledapart-iipythondatareporting
Part II · Module 28 · Chapter 28.3 · Python Over the Reports
Module 24 got the numbers out of a report and into a Tcl data structure. The temptation at that point is to keep going — compare with last night's run, aggregate across corners, write an HTML page — all inside the tool session.
That is where to stop.
1. Why stop there
Four reasons, in increasing order of how much they matter.
Tcl is poor at it. No real data frames, no plotting, no HTML templating, no statistics. Every one of those is possible and all of them are more code than the equivalent Python.
A tool session holds a licence. An aggregation script that runs inside pt_shell occupies a sign-off licence while formatting a table, and on a shared pool that is a cost somebody else pays.
It cannot be tested without the tool. A dashboard grown inside the session can only be exercised by starting the tool, loading a design and running the analysis — so it is never unit-tested and every change is verified by eye.
The question is about more than one run. This is the decisive one. A session knows about this run. "Is WNS getting worse?" is a question about four runs, and no amount of code inside one session can answer it.
2. The hand-off is a file
# summary.tcl — Tcl's hand-off: one line per metric, two columns, nothing else.
proc write_summary {path rows} {
set fh [open $path w]
foreach {k v} $rows { puts $fh [format "%s\t%s" $k $v] }
close $fh
return [expr {[llength $rows] / 2}]
}=== one run's summary, as Tcl wrote it ===
run r001
wns_ns -0.0730
tns_ns -0.0920
area_um2 159268.01
cells 16233Five lines, two tab-separated columns. That is the entire interface between the two languages, and its smallness is the feature.
3. What Python does with four of them
=== 4 run(s) read from work/summaries/*.tsv ===
run wns_ns tns_ns area_um2 cells
r001 -0.0730 -0.0920 159268.01 16233
r002 -0.0410 -0.0410 160114.77 16388
r003 0.0120 0.0000 161880.12 16502
r004 -0.1550 -0.6210 158940.33 16101
=== run-to-run change in WNS, which no single report can show ===
r001 -> r002 -0.0730 -> -0.0410 +0.0320 better
r002 -> r003 -0.0410 -> +0.0120 +0.0530 better
r003 -> r004 +0.0120 -> -0.1550 -0.1670 worse
=== aggregated ===
best WNS : r003 +0.0120 ns
worst WNS : r004 -0.1550 ns
violating : 3 of 4
mean area : 160050.81 um2
=== rendered work/summary.html: 4 row(s), 3 highlighted ===Read the delta block, because it is the thing the brief is about. Runs 1 to 3 improve steadily; run 4 loses 0.1670 ns and goes from meeting timing to the worst result in the set. Nothing in r004's own report says that — the report says -0.1550, which is a number, not a regression. The regression exists only in the comparison.
And area went down in r004 while timing got worse, which is the trade Chapter 24.2 §6 warns about reading in isolation: somebody optimised for area and paid for it in timing. Two columns side by side make that visible; two separate reports do not.
4. The detail that justifies the chapter
My first compare.py printed this:
r001 -0.0730 -0.0920 159268.01 16233.00
16233.00 cells. Treating every numeric field as a float was the obvious thing, and cells is a count.
5. The consumer needs the same discipline as the producer
def read_summary(path):
row = {}
with open(path) as fh:
for n, line in enumerate(fh, 1):
line = line.rstrip("\n")
if not line:
continue
parts = line.split("\t")
if len(parts) != 2:
raise ValueError(f"{path}:{n}: expected two tab-separated fields")
...
missing = (NUMERIC | {"run"}) - set(row)
if missing:
raise ValueError(f"{path}: missing {sorted(missing)}")
return row=== the error paths ===
missing metrics : ValueError: work/bad/missing.tsv: missing ['area_um2', 'cells', 'tns_ns']
malformed line : ValueError: work/bad/malformed.tsv:2: expected two tab-separated fields
no summaries : no summaries matched 'work/nothing/*.tsv'Three things, each the consumer's version of a Module 24 rule.
Every message names a location. work/bad/malformed.tsv:2 — the file and the line. A parser error without a location is a parser error you debug by bisection (Chapter 24.2 §3).
A missing metric is an error, not a default. The set difference reports all of them at once — ['area_um2', 'cells', 'tns_ns'] — which is Chapter 27.4 §3's report-everything rule, in Python.
No summaries at all is an error. no summaries matched rather than an empty table and a confident "0 violating". That is Chapter 24.7 §2's empty-report argument one layer downstream: a glob matching nothing is indistinguishable from a clean result unless something says so.
And note enumerate(fh, 1) — one-based, so the reported line number matches what an editor shows. Off-by-one in an error message is a small cruelty.
6. Where Python should stop too
Being even-handed, since this chapter is an argument for moving work out of Tcl.
Python should not drive the tool. It can — there are bindings, and subprocess always works — and it means the flow logic is now in a language that cannot call the tool's commands directly. Every query becomes a round trip through a generated Tcl script, which is slower, harder to debug, and loses the tool's own error handling. The tool session's own language is the right language inside the tool session (Chapter 28.4).
Python should not own the dependency graph. That is Chapter 28.2's job, and a hand-written staleness check in Python is a worse Make.
And the summary should stay small. The pull is to add columns until the summary file is the report — at which point Python is parsing a large format again and the boundary has moved back. Five to fifteen metrics is a summary; a hundred is a report, and if you need the report, parse the report (Module 24) rather than teaching Tcl to re-emit it.
7. Exercises
Exercise 1. Give the four reasons not to grow the aggregation inside the tool session, and say which is decisive.
Exercise 2. Why is a file a better boundary than a function call? Give three reasons.
Exercise 3. Why tab rather than space as the separator?
Exercise 4. Where do the units live, and why there?
Exercise 5. r004 reports −0.1550. Why is that not a regression, and what is?
Exercise 6. Area went down in r004 while timing got worse. What does that suggest?
Exercise 7. Why did cells print as 16233.00, and whose job is it to fix?
Exercise 8. What larger class of problem does that stand for? Give two more examples.
Exercise 9. Why is "no summaries matched" an error rather than an empty table?
Exercise 10. Why should Python not drive the tool?
Exercise 11. What happens if the summary file keeps growing columns?
Where this fits
Part of the Tcl curriculum.
