Tcl for EDA · lesson 6

Control flow and error handling

if, switch and loops, then catch, error and try: how to make a script decide, repeat and survive a failing step.

8 min read · Free, no login

A flow script has to decide: continue if timing is met, stop if a step fails, and treat each file in a directory the same way. This lesson covers the decision and loop commands, and then the part that matters most in long runs, handling errors so a failure is recorded and does not silently ruin the rest of the flow.

Reading time: about 8 minutes.

6.1 if and elseif

The test is an expression, so it uses the rules of expr. The condition and the bodies are in braces.

proc status {slack} {
    if {$slack < 0} {
        return violated
    } elseif {$slack < 0.05} {
        return marginal
    } else {
        return met
    }
}
puts [status -0.1]
puts [status 0.02]
puts [status 0.4]

This prints:

violated
marginal
met

Always put the opening brace on the same line as if and else. A newline before a brace ends the command, and the next line becomes a separate command that fails.

6.2 switch

switch compares one value with several patterns.

proc kind {name} {
    switch -glob -- $name {
        ff_*    { return register }
        u_*     { return gate }
        default { return other }
    }
}
puts [kind ff_a]
puts [kind u_1]
puts [kind clk]
switch -exact -- setup { setup - hold { puts "a check" } default { puts "unknown" } }

This prints:

register
gate
other
a check

-glob matches with glob patterns, -exact (the default) compares whole strings, and -regexp uses regular expressions. The -- ends the options, so a value that starts with a minus sign is not read as an option. A body written as - means "use the body of the next pattern", which is how setup - hold shares a body.

6.3 Loops

set i 0
while {$i < 3} { puts "i=$i"; incr i }
for {set k 0} {$k < 3} {incr k} { puts "k=$k" }
foreach a {1 2 3} b {x y} { puts "$a $b" }

This prints:

i=0
i=1
i=2
k=0
k=1
k=2
1 x
2 y
3 

while repeats while a condition is true. for has an initial step, a test and a step after each pass. foreach walks one or more lists. When lists have different lengths, the shorter one is padded with empty values, as the last line shows.

break leaves the loop, and continue skips to the next pass.

foreach v {3 -1 5 -2 7} {
    if {$v < 0} { continue }
    if {$v > 6} { break }
    puts "v=$v"
}

This prints v=3 and v=5. The negative values were skipped, and the loop stopped at 7.

6.4 Errors

An error stops the current script and passes up to the caller, unless something catches it. The error command raises one on purpose.

proc need_positive {x} {
    if {$x <= 0} { error "value must be positive: $x" }
    return $x
}

6.5 catch

catch runs a script and returns a code instead of failing: 0 for normal completion, 1 for an error. The result or the error message goes into the variable you name, and an optional second variable receives a dictionary with details.

set rc [catch {expr {1/0}} msg]
puts "rc=$rc msg=$msg"
set rc [catch {set ok 5} msg]
puts "rc=$rc msg=$msg"
set rc [catch {error "step failed" "detail"} msg opts]
puts "rc=$rc msg=$msg"
puts [dict get $opts -code]

This prints:

rc=1 msg=divide by zero
rc=0 msg=5
rc=1 msg=step failed
1

Figure 6.1 shows how the pieces fit together. The usual shape in a flow script is:

catch {script} msg optsreturns 0msg = result of scriptopts: -code 0returns 1 (error)msg = error textopts: -errorinfo, -errorcodeTest the return value, then read msg and opts.Return codes 2, 3 and 4 mean return, break and continue.
Figure 6.1 – catch returns a code and a result; test the code before reading the result.
if {[catch {need_positive -2} msg opts]} {
    puts "failed: $msg"
}

The opts dictionary holds -code, -errorinfo (a stack trace) and -errorcode (a machine-readable list). The stack trace is the most useful when you need to find where a deep error began.

catch {need_positive -1} m o
puts [dict get $o -errorinfo]

This prints:

value must be positive: -1
    while executing
"error "value must be positive: $x" "
    (procedure "need_positive" line 2)
    invoked from within
"need_positive -1"

6.6 return -code and custom codes

A procedure can return an error without calling error, by using return -code error. The caller sees the same behaviour.

proc myerr {} { return -code error "custom failure" }
puts [catch myerr m]
puts $m

This prints:

1
custom failure

You can also pass a structured -errorcode to error, so a handler can decide what to do without parsing text.

catch {error "no lib" "" {EDA NOLIB u1}} m o
puts [dict get $o -errorcode]

This prints EDA NOLIB u1. The convention is a short list: a family, a kind and some detail. A handler that tests the first two elements is more reliable than one that searches the message.

6.7 try, on error and finally

Tcl 8.6 adds try, which reads more clearly than catch for multiple cases and for clean-up.

try {
    need_positive -5
} trap {} {err} {
    puts "caught: $err"
} finally {
    puts "cleanup done"
}
set r [try { need_positive 3 } on error {e} { puts bad; set e } ]
puts $r
try { error "boom" } on error {msg info} {
    puts "msg=$msg code=[dict get $info -code]"
}

This prints:

caught: value must be positive: -5
cleanup done
3
msg=boom code=1

The finally body always runs, whether or not an error happened. That is the place to close a file or restore a setting. Because try is a Tcl 8.6 command, check your tool's Tcl version before relying on it, and use catch where the tool ships an older version.

6.8 A loop that consumes a queue

set queue {a b c}
while {[llength $queue] > 0} {
    set queue [lassign $queue head]
    puts "processing $head"
}

This prints processing a, processing b and processing c. lassign removes the first element and returns the rest, which is a compact way to work through a to-do list.

6.9 A small Innovus example

Let us consider a step that saves the design. If the save fails, the script should record it and stop, rather than carry on. To be run at the Innovus prompt. Check the options against your release with saveDesign -help.

Legacy UI.

if {[catch {saveDesign test.enc} msg opts]} {
    puts "saveDesign failed: $msg"
    exit 1
}

The reference for saveDesign lists test.enc -timingGraph as a usage example, so the file name and the options are ones you can check in your release. The catch stops a failing save from being hidden in a long log, and the non-zero exit tells a calling shell script or scheduler that the run failed.

6.10 Why an interviewer asks about this

Interviewers like "how do you make a flow script stop when a step fails" and "what is the difference between catch and try". A strong answer says that catch returns a code and puts the message and details into variables, that try adds on error, trap and finally in 8.6, and that a flow should log the failure and exit with a non-zero status. A common follow-up is "how do you make sure a file is closed even on error", which is the finally clause.

Key takeaways

  • if, switch, while, for and foreach take their conditions as expr rules and their bodies in braces, with the opening brace on the same line.
  • catch turns an error into a return code, and the dictionary of details holds the stack trace and the error code.
  • try with finally is the clean way to guarantee clean-up in Tcl 8.6.
  • Log the failure and exit with a non-zero status, so the people and tools that call your script can see it.