Tcl · Module 29
Making a Flow Fail Loudly
A broken run and a broken sign-off cost the same compute. Only one of them produces a number somebody puts their name to, and gates are what decides which you get.
Advanced21 min readtcledapart-iidebuggingerror-handlingsignoff
Part II · Module 29 · Chapter 29.4 · Making a Flow Fail Loudly
Chapter 29.1 catalogued the failures and the assertions that catch them. This chapter is about the structure those assertions live in: gates between phases.
1. A broken run versus a broken sign-off
The registry's phrase for this chapter is worth taking literally.
2. Ungated: every phase runs
The mock below has five phases, each producing something the next one needs: read, elaborate, constrain, optimise, report.
proc run_ungated {files sdc} {
::g::reset
phase_read $files
phase_elaborate
phase_constrain $sdc
phase_optimise
set viol [phase_report]
return [expr {$viol == 0 ? "PASS" : "FAIL"}]
}Nothing in that is unusual, and nothing in it is checked.
=== ungated: every phase runs, verdict is the last one's ===
CASE VERDICT WHAT THE PHASES DID
everything-present FAIL read 1 of 1 file(s) | elaborated 4 cell(s) | constraints applied: 1 | optimised 4 cell(s) | reported 2 violation(s)
rtl-path-wrong PASS read 0 of 1 file(s) | elaborated 0 cell(s) | constraints applied: 1 | optimised 0 cell(s) | reported 0 violation(s)
sdc-path-wrong PASS read 1 of 1 file(s) | elaborated 4 cell(s) | constraints applied: 0 | optimised 4 cell(s) | reported 0 violation(s)
no-files-at-all PASS read 0 of 0 file(s) | elaborated 0 cell(s) | constraints applied: 1 | optimised 0 cell(s) | reported 0 violation(s)Three of four broken runs report PASS, and the phase log says precisely why each one did:
rtl-path-wrong—read 0 of 1, soelaborated 0 cell(s). There is no design, so there is nothing to violate.sdc-path-wrong—constraints applied: 0. There is a design and no requirement, so nothing is violated.no-files-at-all—read 0 of 0. The flow was given an empty file list and found all of it.
Every phase "succeeded". phase_read returned a count, and a count of zero is a count. phase_constrain returned 0, and nobody looked. The verdict is the last phase's output, and the last phase correctly reported that an empty unconstrained design has no violations.
Note read 0 of 1 and read 0 of 0 are different failures wearing the same clothes: a wrong path, and a caller that passed nothing. The of N denominator is what separates them — Chapter 22.7 §4's rule, earning its keep here.
3. Gated: stop at the phase that failed
proc gate {name value message} {
if {!$value} {
error "gate after '$name' failed: $message" {} [list GATE $name]
}
return $value
}
proc run_gated {files sdc} {
::g::reset
gate read [phase_read $files] "no input file was readable"
gate elaborate [phase_elaborate] "the design elaborated to zero cells"
gate constrain [phase_constrain $sdc] "no constraints were applied"
gate optimise [phase_optimise] "optimisation touched nothing"
set viol [phase_report]
gate report [dict exists $::g::state violations] "no report was produced"
return [expr {$viol == 0 ? "PASS" : "FAIL"}]
}=== gated: the flow stops at the phase that failed ===
CASE RESULT
everything-present FAIL (5 phases)
rtl-path-wrong STOPPED: gate after 'read' failed: no input file was readable
phases completed: 1
sdc-path-wrong STOPPED: gate after 'constrain' failed: no constraints were applied
phases completed: 3
no-files-at-all STOPPED: gate after 'read' failed: no input file was readable
phases completed: 1Four properties of that output are worth naming.
The correct case still runs all five phases and still reports FAIL — correctly, because the design has two real violations. A gate set that breaks the working case is one somebody removes (Chapter 23.1 §6).
Each stop names its phase. gate after 'constrain' is the location, in the first line, without reading a log.
The phases-completed count locates it independently. 1 for a read failure, 3 for a constraint failure — two numbers that say which phase failed even if the message is lost, truncated or buried. That is a second, independent signal about the same fact, which is Chapter 24.7 §5's principle applied to a failure report.
gate takes the value, not an expression. gate read [phase_read $files] runs the phase and hands its result to the gate, so the phase cannot be skipped by a short-circuit and the gate cannot be written without the phase. Two things that belong together are syntactically together.
4. The gate that must not be a truth test
5. Where gates go, and where they do not
Between phases, immediately after the phase they are about. Chapter 29.1 §6's locality argument: a gate at the end reports "the report is empty", three phases from the cause.
Not inside a phase. A phase that gates its own internals is a phase whose contract is unclear — the gate belongs at the seam, where the question is "did this produce what the next phase needs".
Not on every intermediate value. Gating each of a thousand cells is both slow and noise. Gates belong where a phase hands something to the next phase, and that is a handful of places.
And one gate per claim, not one per phase. The constraint phase has two claims — the call was made, and the state changed — and Chapter 29.1 §5 checks both. Collapsing them loses the distinction between "the file was unreadable" and "the file was read and contained nothing applicable".
6. Turning a gate into an exit status
A gate that raises is only half the mechanism. The flow has to exit with something the caller can act on, which is Chapter 27.1 §7's split:
if {[catch {run_gated $files $sdc} result]} {
puts stderr "flow stopped: $result"
puts stderr "phases completed: [llength $::g::log]"
foreach line $::g::log { puts stderr " $line" }
# a GATE error is a flow failure; anything else is a tool or script fault
if {[lindex $::errorCode 0] eq "GATE"} { exit 1 }
exit 2
}
puts $result
exit 0Three decisions there.
The phase log goes to stderr on failure. Not just the message — the whole sequence of what the phases did, because that is the §2 table and it is what makes the failure diagnosable without re-running. On success it is not printed, because nobody reads it then.
The errorCode distinguishes a gate from everything else. GATE means a phase did not produce what the next needed — a flow failure, exit 1. Anything else is a bug in the script or a fault in the tool — exit 2. Those go to different people, per Chapter 25.4 §7, and the distinction is free because Chapter 29.1 §5 put a code on every error.
exit 0 only on the success path, and note that FAIL — a design with real violations — also exits 0 here. That is deliberate and worth being explicit about: the flow worked. Whether the design meets timing is the verdict's business, and conflating "the flow broke" with "the design has violations" is how a regression driver comes to retry a genuine timing failure.
7. Exercises
Exercise 1. State the difference between a broken run and a broken sign-off in terms of cost.
Exercise 2. Three of four ungated cases reported PASS. Give the mechanism for each.
Exercise 3. Why did every phase "succeed" in those three cases?
Exercise 4. read 0 of 1 and read 0 of 0 are different failures. What distinguishes them and why does that matter?
Exercise 5. Why does the gated flow's first row matter?
Exercise 6. What does the phases-completed count give you that the message does not?
Exercise 7. Why does gate take a value rather than an expression?
Exercise 8. Why can the report gate not assert that violations exist?
Exercise 9. State the distinction between the two questions a gate and a verdict ask.
Exercise 10. Why one gate per claim rather than one per phase?
Exercise 11. Why does a design that fails timing exit 0 in §6, and when would you want otherwise?
Exercise 12. Why should the block-the-merge policy live in the caller rather than the flow?
Where this fits
Part of the Tcl curriculum.
