Tcl · Module 27
Idempotent Helper Procedures
A flow script is re-run far more often than it is written, so running it twice must be harmless. Finding out whether it is cannot be done by reading the code.
Intermediate20 min readtcledapart-iiinfrastructureidempotencyhelpers
Part II · Module 27 · Chapter 27.7 · Idempotent Helper Procedures
A flow script gets written once and run hundreds of times — after a crash, from a partial state, at a prompt while debugging, and by a driver that retries. So the question "what does this do if it runs twice" is not hypothetical; it is the normal case.
1. The same setup, run twice
proc setup_naive {} {
create_clock_naive core_clk 5.0
create_clock_naive io_clk 10.0
add_lib_path_naive libs/std
add_lib_path_naive libs/io
}=== the naive setup, run once then twice ===
after 1 run : clocks core_clk=5.0 io_clk=10.0 libdirs 2 -> libs/std libs/io
after 2 runs: clocks core_clk=5.0 io_clk=10.0 libdirs 4 -> libs/std libs/io libs/std libs/io2. Making it deliberate
# Returns what it DID, so a caller and a log can tell "created" from
# "already there". Returning nothing would make the two indistinguishable.
proc create_clock_if_missing {name period} {
if {[dict exists $::flow::CLOCKS $name]} {
set have [dict get $::flow::CLOCKS $name]
if {$have != $period} {
error "clock $name already defined with period $have, not $period" \
{} [list FLOW CLOCKCONFLICT $name $have $period]
}
return "unchanged $name"
}
dict set ::flow::CLOCKS $name $period
return "created $name"
}
proc add_lib_path_once {dir} {
if {[lsearch -exact $::flow::LIB_DIRS $dir] >= 0} { return "unchanged $dir" }
lappend ::flow::LIB_DIRS $dir
return "added $dir"
}=== the idempotent setup, from a clean state ===
run 1:
created core_clk
created io_clk
added libs/std
added libs/io
after 1 run : clocks core_clk=5.0 io_clk=10.0 libdirs 2
run 2:
unchanged core_clk
unchanged io_clk
unchanged libs/std
unchanged libs/io
after 2 runs: clocks core_clk=5.0 io_clk=10.0 libdirs 22 library paths after two runs, and the second run says unchanged four times.
The return values are the part people leave out. A helper that returns nothing makes "I just created it" and "it was already there" indistinguishable — and that distinction is exactly what you need when a re-run was supposed to be a no-op and something changed anyway. Printing them turns a silent re-run into a one-line statement of what it actually did.
3. A conflict is not a no-op
=== and a conflict is not a no-op ===
1 / clock core_clk already defined with period 5.0, not 4.04. file mkdir is already idempotent — but only on a directory
=== what plain file mkdir does in the same two cases ===
on an existing dir : 0 / no error
on an existing file : 1 / can't create directory "work/notadir": file already existsfile mkdir on an existing directory succeeds silently, so the usual if {![file exists $d]} {file mkdir $d} guard is unnecessary. On an existing file it fails.
So the check that matters is the type, not the existence:
proc ensure_dir {path} {
if {[file isdirectory $path]} { return "unchanged $path" }
if {[file exists $path]} {
error "$path exists and is not a directory" {} [list FLOW NOTADIR $path]
}
file mkdir $path
return "created $path"
}=== ensure_dir: directory, twice, then a file in the way ===
created work/idem
unchanged work/idem
1 / work/notadir exists and is not a directory[file exists] is the wrong test and [file isdirectory] is the right one — the same distinction as Chapter 14.7's open on a directory, where file exists passed and the failure arrived at read. A name that exists is not a name you can use for what you wanted.
And the error message says what is wrong — exists and is not a directory — rather than passing through can't create directory, which describes the attempt rather than the cause.
5. What idempotency is worth, and what it is not
Being honest, because "make everything idempotent" is not free.
It makes a re-run a diagnostic. If running the setup twice prints unchanged throughout, the session is in the state the script describes. If it prints created, something had been lost — which is information you did not have before.
It does not make a flow resumable. A helper being safe to run twice says nothing about whether the flow can be restarted from the middle. Re-running create_clock_if_missing is harmless; re-running a synthesis step on a netlist that was already optimised is a different design. Step-level resumability needs recorded state — which stage completed, with what inputs — and is a much larger thing than helper-level idempotency.
It cannot protect state the helper does not own. create_clock_if_missing checks ::flow::CLOCKS because that is the mock's own record. In a real tool the clock lives in the tool's database, and the check has to ask the tool — get_clocks, or whatever that tool calls it. Checking your own bookkeeping instead of the tool's is how a helper comes to believe a clock exists that was lost when the design was reloaded.
Not everything should be. A step that deliberately regenerates a report should regenerate it; making it skip when the file exists produces stale reports that look current, which is worse than the cost of rewriting one. The question is whether the operation's purpose is to reach a state (idempotent) or to do work (not).
6. Exercises
Exercise 1. Why is "what does this do if it runs twice" not a hypothetical question?
Exercise 2. The naive setup left clocks unchanged and library paths duplicated. Why the difference?
Exercise 3. Why are the accidentally-idempotent operations the dangerous ones?
Exercise 4. Why do the idempotent helpers return created / unchanged rather than nothing?
Exercise 5. What are the three outcomes of create_clock_if_missing, and what goes wrong if the third is treated as the second?
Exercise 6. Why $have != $period rather than ne?
Exercise 7. Is file mkdir idempotent? Answer precisely.
Exercise 8. Why is file isdirectory the right test and file exists the wrong one?
Exercise 9. Why does helper-level idempotency not make a flow resumable?
Exercise 10. Why can a helper not protect state it does not own?
Exercise 11. State the narrower rule about which helpers should be idempotent, and the failure mode of getting it wrong.
Where this fits
Part of the Tcl curriculum.
