Tcl · Module 29
Stale Inputs: Netlists, Libraries, Constraints
A flawless analysis of last week's netlist asserts cleanly at every step. Catching that needs the identity of the inputs, because timestamps do not have the resolution to answer it.
Advanced22 min readtcledapart-iidebuggingprovenancesignoff
Part II · Module 29 · Chapter 29.2 · Stale Inputs
Chapter 29.1 ended on a failure assertions cannot catch: a correct result computed from the wrong inputs. Every step succeeds, every assertion passes, every number is internally consistent — and the netlist was last week's.
1. Why this one is the worst of them
The faults in Chapter 29.1 produce results that are wrong. This one produces results that are right about the wrong thing, and the difference matters:
- A wrong result is sometimes implausible, so somebody squints at it.
- A result from stale inputs is entirely plausible — it is a correct analysis, just not of the design you have.
- It is self-consistent. The timing report agrees with the netlist it was computed from, the area report agrees with both, and every cross-check in Module 24 passes.
- And it is reproducible. Running again gives the same answer, which is the usual evidence that a result is trustworthy.
Three real ways it happens, none of them exotic: synthesis failed and the flow continued on the previous netlist (Chapter 29.4); a library path pointed at last quarter's release; an SDC was edited after the run and before the review.
2. The check everyone writes
if {[file mtime $netlist] < [file mtime $rtl]} {
error "netlist is older than the RTL"
}That is the obvious check and it is this chapter's own brief. It does not work, and the reason is a resolution problem.
=== case 1: netlist written LAST, both in the same second ===
mtime rtl 1791486019 netlist 1791486019 same second: 1
netlist < rtl : 0 (correct: not stale)
netlist <= rtl : 1 <- FALSE POSITIVE: it is NOT stale
=== case 2: rtl edited LAST, both in the same second ===
mtime rtl 1791486019 netlist 1791486019 same second: 1
netlist < rtl : 0 <- FALSE NEGATIVE: it IS stale
netlist <= rtl : 1 (catches it)
=== so within one second, neither comparison is right ===
< misses a genuinely stale netlist
<= rejects a genuinely fresh one3. The information exists; Tcl 8.5 cannot reach it
=== what file stat offers ===
atime 1791486019
blksize 4096
blocks 8
ctime 1791486019
dev 16777230
gid 0
ino 83610641
mode 33188
mtime 1791486019
nlink 1
size 7
type file
uid 501
=== is there any sub-second field? ===
sub-second fields in file stat : 0Thirteen fields and not one sub-second. Meanwhile the filesystem is holding the real value — the same file, seen two ways:
=== the same file, seen by Tcl and by Python ===
tcl file mtime : 1791486019
python st_mtime : 1791486019.6776323So this is a Tcl 8.5 limitation, not a filesystem one. The fractional part exists; file mtime truncates it and file stat does not expose it.
Three consequences worth knowing.
You can reach it by other means and probably should not. exec stat -f %Fm on this platform, or a small C extension, gets the fraction — at the cost of a platform-specific dependency in the one place you least want one. stat's flags differ between BSD and GNU, so the wrapper is immediately non-portable.
ctime is not creation time. It is the inode change time, and it updates on a permission change or a rename. Reaching for it as a tiebreaker introduces a field that moves for reasons unrelated to content.
And the fundamental objection stands regardless of resolution, which is §4.
4. Timestamps answer the wrong question
Even with nanosecond resolution, a timestamp comparison is a proxy. The question is "is this output consistent with these inputs", and the timestamp answers "which was touched more recently" — which is a different question that usually has the same answer.
Where they diverge:
touchwith no edit makes a file newer with identical content. Agit checkoutthat restores a file to exactly what it was updates its mtime, and the whole flow reruns for nothing — the same hazard Chapter 28.2 §5 names for Make.- An edit and an undo leave a newer mtime and the original content. The flow reruns and produces the identical result.
- A copied or restored file can arrive with an older mtime than the output it should invalidate —
cp -p, a tar extraction, or a restore from backup. Then the stale output looks current. - Two machines with skewed clocks writing to shared storage make the comparison meaningless in either direction.
5. The fix is identity
Record what each input was, beside the output, and compare that.
proc ::prov::digest {path} {
if {![file isfile $path]} {
error "no such input: $path" {} [list PROV NOINPUT $path]
}
switch -- $::prov::HASHER {
tcllib { return [string range [::md5::md5 -hex -file $path] 0 11] }
shasum { return [string range [lindex [exec shasum $path] 0] 0 11] }
none { error "no hash available" {} {PROV NOHASH} }
}
}=== a stage records what its inputs WERE ===
tcl 8.5.9
hasher tcllib
stage synth
design core
input 854A7178B03C rtl/core.sv
input 1B1685A8BC30 constraints/c.sdcThen the check becomes a comparison of identities rather than of times:
=== nothing changed ===
checked 2 changed 0 missing 0
=== now edit the RTL in the SAME SECOND as the manifest ===
manifest mtime 1791486019 rtl mtime 1791486019 same second: 1
a timestamp check (<) would say stale: 0
the identity check says changed: 1
rtl/core.sv was 854A7178B03C now A9D2B2EC8AD3The timestamp check says "not stale". The identity check names the file and shows both digests. Same second, same files, and one of the two methods can answer the question.
6. What to record the identity of
Not only the netlist. The three the registry names, and two more:
| Input | Why it goes stale | What to record |
|---|---|---|
| Netlist | synthesis re-run, or failed and the old one remained | digest |
| Constraints | edited after the run, or the wrong variant sourced | digest per file |
| Libraries | a path to a release that moved | digest is impractical; record the resolved path and the release string |
| Parasitics | an extraction re-run for a different corner | digest, or the extraction's own run ID |
| The flow scripts | somebody edited the script after the run | digest — Chapter 28.2 §1 lists it as a prerequisite for the same reason |
The library row is the honest exception. Hashing a multi-gigabyte .db on every stage is not viable, so the practical substitute is the resolved absolute path plus whatever version string the library itself carries — and file normalize matters there, because a path through a current symlink records something that will mean a different library next quarter.
dict set prov library_path [file normalize $lib]
dict set prov library_release [library_version_string $lib]That is weaker than a digest and it catches the failure that actually happens: the symlink moved.
7. Where the check goes
At the start of a stage, on everything it is about to read. Not at the end — a stage that discovers its inputs were stale after six hours of work has wasted the six hours, which is Chapter 23.2 §1's cost-curve argument.
And at the start of a review, on everything the result claims to come from. That is the case the chapter title is really about: somebody is looking at a timing report from last month and asking whether it still describes the design. verify_inputs on its manifest answers that in a second, and the answer is frequently no.
# before trusting a result
set s [::prov::verify_inputs $manifest]
if {[llength [dict get $s missing]] || [llength [dict get $s changed]]} {
error "this result no longer describes the current inputs" {} {PROV STALE}
}The failure mode without that check is not a wrong number; it is a meeting. A review spends an hour discussing a violation that was fixed three weeks ago, because nothing in the report says which netlist it describes.
8. Exercises
Exercise 1. Why is a stale-input result worse than a wrong one?
Exercise 2. Give three ordinary ways a stale input reaches an analysis.
Exercise 3. What resolution does file mtime have, and what follows?
Exercise 4. Explain both measured cases: what < does and what <= does, and why neither is right.
Exercise 5. Which direction of error is the dangerous one, and why?
Exercise 6. Does the filesystem hold sub-second mtimes? Does file stat expose them?
Exercise 7. Why is ctime a poor tiebreaker?
Exercise 8. Even with nanosecond resolution, why is a timestamp the wrong instrument? Give two divergences.
Exercise 9. Name four properties that make an identity check better than a timestamp.
Exercise 10. Why is hashing a library impractical, and what is recorded instead?
Exercise 11. Why does file normalize matter for a library path?
Exercise 12. Where do the two checks go, and what is the failure mode of omitting the second?
Where this fits
Part of the Tcl curriculum.
