All sections

The cards come in more than one shape: a command to explain, a snippet with a fault to find, and a description whose command you name. A missed card returns later in the round and starts the schedule over. A card answered correctly moves up the schedule and next appears after 1, 3, 7, then 21 days. Keys 1 to 4 choose an option, h shows a hint, and Enter moves on. A hint keeps the card where it is on the schedule. An ordering card is answered by clicking its items in order, and one wrong click ends it. Time values are in nanoseconds. Progress is stored in this browser.

SourceTCL (Tool Command Language) is the scripting language every place-and-route and timing tool embeds, so the same skills carry between them. These cards follow the official Tcl manual pages, and each answer links to the page for that command. The examples are in the style the OpenROAD-flow-scripts scripts use, since that is Tcl written for a real flow. Reference: the Tcl manual (Tcl on GitHub).
All 71 cards in this deck, with answers
  • set period 2.0 set half [expr {$period / 2}] puts "half is $half"
    Brackets run a command and substitute its result; quotes substitute variables.

    Hint: Three kinds of bracket appear here. Ask what each one does before the command runs.

    Square brackets are the command ones. Substitution happens once, before the command runs: [...] is replaced by the result of the command inside it, and $name by the variable's value. Braces, {...}, suppress both, which is why they appear around expressions and procedure bodies. Tcl reference

  • set n 4 set a [expr {$n * 2}] set b [expr $n * 2]
    Both work here, but the braced form is safer and faster.

    Hint: Both give 8. Ask what happens to the expression text before expr sees it.

    Without braces the substitution happens first, so expr receives the values as text and must parse them again. That is slower, and a value that contains an operator or a space can change the expression's meaning. Braced expressions are the rule in tool scripts. Tcl reference

  • set x 0.5 set y [expr {$x + 1}]
    Values are strings; `expr` is what interprets them as numbers.

    Hint: Ask what type x has before expr looks at it.

    Everything in Tcl is a string at the language level, and commands interpret it. This is why arithmetic needs expr, why comparing numbers as strings misleads, and why a stray space in a value can turn into two arguments. Tcl reference

  • incr count append names " $cell"
    Adds one to a numeric variable, and adds text to the end of a string.

    Hint: Two shorthands. Ask what each saves you writing.

    incr takes an optional step, so incr count 5 adds five and a negative value subtracts. append grows a string in place; the list equivalent is lappend, which keeps list structure instead of concatenating text. Tcl reference

  • set cells {} foreach c [get_cells *] { lappend cells [get_name $c] }
    Appends each name as one element, keeping the list structure intact.

    Hint: Ask what lappend does that set with a concatenation would not.

    A list is not just text with spaces: lappend quotes an element that contains spaces or brackets so it stays a single element. Building a list by string concatenation breaks as soon as a name contains a bracket, which hierarchical names often do. Tcl reference

  • set corners {slow typical fast} set first [lindex $corners 0] set last [lindex $corners end]
    Indexes from 0, and `end` names the last element.

    Hint: Note where counting starts, and the word used for the other end.

    end and arithmetic on it, such as end-1, work in every list command that takes an index. llength gives the count, and lrange takes a span, so lrange $corners 1 end drops the first element. Tcl reference

  • lsort -real -decreasing $slacks
    Sorts numerically and largest first, rather than as text ascending.

    Hint: Ask what the default comparison would do to numbers.

    Sorting defaults to text order, where 10 comes before 9. -real and -integer compare numerically, -unique removes duplicates, and -command sorts with your own procedure, which is how a list of objects is sorted by a property. Tcl reference

  • lassign [split $line ":"] rule value set parts [split "metal1 metal2 metal3"]
    Assigns list elements to named variables, and splits text into a list.

    Hint: One takes a list apart into variables; the other makes a list from text.

    split cuts text at every character of the separator set, defaulting to whitespace; join is its inverse. lassign unpacks a list positionally, which is the readable way to take apart a line already parsed into fields. Tcl reference

  • if { $slack < 0 } { puts "failing" } else { puts "met" }
    The braces keep the condition unsubstituted so `if` evaluates it as an expression.

    Hint: Ask why the condition is in braces rather than quotes.

    if evaluates its condition the way expr does, so the same rule applies: braces stop the text being substituted first. Note also that } else { must sit on the line the brace closes, since a newline would end the command. Tcl reference

  • foreach {name value} $pairs { puts "$name = $value" }
    Takes two elements per pass, binding them to the two names.

    Hint: Two variable names in braces. Ask how many elements each pass consumes.

    A flat list of alternating keys and values is a common shape in tool output, and this is how it is walked. foreach can also take several lists at once, stepping through them in parallel. Tcl reference

  • switch $stage { place { global_placement } route { global_route } default { error "unknown stage: $stage" } }
    Runs the branch that matches, and default catches anything else.

    Hint: Ask what the last branch is for, and what happens without it.

    Only the matching branch runs. Without a default an unmatched value does nothing at all, silently, which is how a typo in a stage name becomes a script that appears to work. Patterns are glob-style unless -exact or -regexp is given. Tcl reference

  • proc report_stage {stage {verbose 0}} { puts "stage $stage" if { $verbose } { report_design_area } }
    Defines a command whose second argument is optional, defaulting to 0.

    Hint: Read the second parameter: it is written differently from the first.

    A parameter written as a two-element list gives a default. The special name args collects any remaining arguments into a list, which is how a wrapper passes options through to the command it wraps. Tcl reference

  • proc count_cell {} { global cell_count incr cell_count }
    Makes the global variable visible inside the procedure.

    Hint: Ask what a procedure can see by default.

    A procedure's variables are local unless declared otherwise, so a script that sets a variable at the top level and expects a procedure to see it is a common bug. upvar links to a caller's variable by name, which is how a procedure modifies a variable passed to it. Tcl reference

  • proc add_to {listVar item} { upvar 1 $listVar l lappend l $item }
    Links the caller's variable to a local name, so changes reach the caller.

    Hint: The procedure is given a variable's name rather than its value.

    Tcl passes arguments by value, so modifying a list means passing its name and linking to it. The 1 is how many levels up to look, almost always the immediate caller. uplevel is the same idea for running a script in the caller's scope. Tcl reference

  • if { [string match "*_reg*" $name] } { ... }
    Glob-style matching, where `*` matches any text and `?` one character.

    Hint: Ask which pattern language this is: the shell's or a regular expression's.

    Glob patterns are the same ones the object-access commands take, which is why get_cells *_reg* feels familiar. For anything with alternation or capture, regexp is the tool, at the cost of a denser pattern. Tcl reference

  • if { [regexp {^(\w+)/(\w+)$} $pin -> inst port] } { ... }
    Captures the two groups into inst and port, discarding the whole match.

    Hint: Three variables follow the pattern. Ask what the first one receives.

    The first variable after the string takes the whole match, and -> is the conventional throwaway name for it. The pattern is in braces so Tcl does not substitute the backslashes before the regular-expression engine sees them. Tcl reference

  • set out [string map {"/" "_" "\[" "" "\]" ""} $name]
    Replaces each key with its value, so the name becomes filename-safe.

    Hint: Ask what the pairs in the list mean.

    This is the quick way to make a hierarchical pin name usable as a file name, without a regular expression. The replacements are applied in one pass, so an earlier pair cannot match text produced by a later one. Tcl reference

  • puts [format "%-30s %8.3f" $endpoint $slack]
    Formats a padded name and a slack with three decimals, so columns line up.

    Hint: Two field specifications. Ask what the numbers before them control.

    The minus sign left-justifies, the first number is the field width, and the second is the precision. Reports that line up are read far faster than ones that do not, which matters when a script's output is scanned by eye across hundreds of rows. Tcl reference

  • dict set metrics wns -0.06 dict set metrics tns -2.18 puts [dict get $metrics wns]
    A dictionary, which is a value held in an ordinary variable.

    Hint: Ask what the value of the variable metrics actually is.

    A dictionary is a list of alternating keys and values. Because it is a value, it can be passed to a procedure, returned, and stored in a list, which an array cannot. dict for iterates it, and dict exists tests a key without the error that dict get would raise. Tcl reference

  • set area(nand2) 0.798 foreach cell [array names area] { puts "$cell $area($cell)" }
    An array, which stays in the interpreter and cannot be passed by value.

    Hint: Compare it with a dictionary: ask what you cannot do with this one.

    Arrays predate dictionaries and are still everywhere in tool scripts. Passing one to a procedure needs upvar on its name. array names lists the keys, in no defined order, and array get flattens it into a list that a dictionary can be built from. Tcl reference

  • set def_file $::env(RESULTS_DIR)/6_final.def
    Reads an environment variable exported by the flow's makefile.

    Hint: Ask where a make-driven flow puts the values its scripts need.

    env is an array holding the process environment, here the variables the makefile exported; and :: names the global namespace explicitly so it works inside a procedure too. This is exactly how OpenROAD-flow-scripts passes its config.mk variables into every Tcl stage script. Tcl reference

  • set fh [open $report w] puts $fh "slack $slack" close $fh
    Opens the file for writing, writes a line to it, and closes the handle.

    Hint: Note the extra argument to puts, and what open returns.

    puts writes to standard output unless given a channel first. The mode is r to read, w to truncate and write, and a to append. Forgetting close can leave the last buffered lines unwritten if the script exits abruptly. Tcl reference

  • if { [file exists $odb] } { set stem [file rootname [file tail $odb]] }
    Tests the path exists, then strips the directory and extension.

    Hint: One command, several jobs, chosen by the word after it.

    file exists is the test and tail with rootname does the stripping. file is an ensemble: tail drops the directory, dirname keeps it, rootname drops the extension, extension keeps it, and join builds a path. file exists before reading is the usual guard in a flow script. Tcl reference

  • set n [exec grep -c "VIOLATED" $report]
    Runs the program and returns what it printed, as the command's result.

    Hint: Ask where the output of the program goes.

    A non-zero exit status raises an error, so exec is usually wrapped in catch when the program's failure is expected, as grep finding nothing is. Redirections and pipes can be written in the arguments. Tcl reference

  • source $::env(SCRIPTS_DIR)/load.tcl
    Runs the file's commands in the current scope, as if typed there.

    Hint: Ask what scope the sourced file's commands and variables end up in.

    Everything the file sets is visible afterwards, and a second source runs it again. This is how the flow shares a common setup across stage scripts; it is also why a sourced file that sets a variable can surprise the script that sourced it. Tcl reference

  • if { [catch { read_spef $spef } err] } { puts "no parasitics: $err" }
    Runs the script and returns non-zero if it failed, with err set.

    Hint: Ask what catch returns, and what the second variable receives.

    The message goes into the named variable. catch returns 0 on success, so the if above reads as "if this failed". Wrapping a whole stage in catch hides real failures, so it is used narrowly, around the step whose failure is expected and handled. Tcl reference

  • if { ![file exists $lib] } { error "missing library: $lib" }
    Raises an error that stops the script unless a caller catches it.

    Hint: Ask what happens to the script after this line runs.

    Failing early with a clear message is far better than continuing on missing input and failing later in a stage that cannot explain why. Flow scripts use this to check their inputs before spending an hour on a run that cannot succeed. Tcl reference

  • if { [info exists ::env(CTS_ARGS)] } { set args $::env(CTS_ARGS) }
    Tests whether the variable is set, since reading an unset one is an error.

    Hint: Ask what reading an unset variable does.

    An optional setting is exactly this shape: use it if it exists, otherwise keep the default. info also answers questions about procedures, commands, and the call stack, which is how a script reports where it is. Tcl reference

  • set cells [get_cells *_reg*] puts [llength $cells]
    The tool returns objects, which not every list command handles.

    Hint: Ask whether what the tool returns is really a Tcl list.

    What comes back is a collection of objects. They carry properties that a name cannot, which is why get_property and get_full_name exist. Some tools return a collection type that needs its own commands, so mixing list commands with tool objects is a common source of confusion when a script moves between tools. Tcl reference

  • set args "" if { $::env(GPL_TIMING_DRIVEN) } { append args " -timing_driven" } global_placement {*}$args
    Expands the string into separate arguments instead of one long one.

    Hint: Read the last line: something is done to the variable before the command sees it.

    Without {*}, a command receives " -timing_driven" as one argument and rejects it. Building a command's options conditionally and expanding them is the standard way a flow turns variables into tool arguments. Tcl reference

  • set period_ns 2.0 create_clock -name clk -period $period_ns [get_ports clk]
    The number has no unit in Tcl; the tool reads it in library units.

    Hint: Ask what the tool does with the number, and where its unit comes from.

    Library units are usually nanoseconds, but not always. A script can hold a number that means something different to the tool than to its author, and nothing will complain. report_units shows what the tool is using, and set_units records the intent in a constraint file. Tcl reference

  • foreach path [find_timing_paths -path_delay max -group_path_count 10] { puts "[get_property $path slack]" }
    Iterates the path objects the tool returns, reading a property from each.

    Hint: Ask why the script asks for objects rather than reading a report.

    Parsing a printed report is brittle: its format changes between versions. Asking for objects and reading properties is stable, and it is why reporting commands usually have a find_ counterpart meant for scripts. Tcl reference

  • if { ![design_is_routed] } { error "routing did not finish" }
    Stops the flow before a later stage runs on a design that is not ready.

    Hint: Ask what this protects the rest of the flow from.

    A stage that runs on an unfinished design either fails confusingly much later or produces results that look plausible and are not. Checks between stages turn that into one clear failure at the point where something actually went wrong. Tcl reference

  • set name clk puts "the clock is $name" puts {the clock is $name}
    Quotes substitute variables and commands; braces pass the text through.

    Hint: Read the second line and the third as the interpreter would.

    The second line prints the clock is clk and the third prints the dollar sign and the name. This is the single most useful rule in the language: braces mean literally this, quotes mean fill it in. It is why procedure bodies and expressions are braced. Tcl reference

  • set name clk expr { $name == "clk" } expr { $name eq "clk" }
    Both compare here, but only the second is always a string comparison.

    Hint: One of the two operators works on numbers first.

    The numeric operator converts when it can, so comparing 1.0 with 1 is true and comparing 0x10 with 16 is true. That is rarely what a script comparing names wants, and it is a classic source of a condition that is true for the wrong reason. Use eq and ne for text. Tcl reference

  • set stage place puts "${stage}_done"
    The braces mark where the variable name ends.

    Hint: Ask where the interpreter would decide the variable name ends.

    Without them the interpreter looks for a variable called stage_done and fails. A name runs to the first character that cannot be part of one, and an underscore can be, which is why this bites when building file names. Tcl reference

  • set ::top_module gcd puts $::env(HOME)
    The variable at the top level, reachable from inside any procedure.

    Hint: Two colons in front of a name. Ask which variable is meant.

    A procedure sees only its own variables unless it says otherwise, and the leading colons name the global namespace directly, which avoids having to declare it. The environment array is the common case, since a flow is usually configured through it. Tcl reference

  • set corners {slow typical fast} lsearch $corners typical lsearch -exact -not -inline -all $corners typical
    The position of a match, or minus one.

    Hint: The plain form answers where, not whether.

    The plain call returns an index, so a test reads if {[lsearch $l $x] >= 0}. Options change both the matching and the result: -exact, -glob, and -regexp choose how to match, -all returns every index, and -inline returns the elements instead. Tcl reference

  • set corners {slow typical fast} lreplace $corners 1 1 nominal puts $corners
    The original is unchanged, since the command returns a new list.

    Hint: Ask what the last line prints.

    Every list command except lappend and lset returns a value and leaves its argument alone, so the result has to be assigned. Forgetting this is why a filtered list comes out identical to the one it was built from. Tcl reference

  • join {metal1 metal2 metal3} ", " concat {a b} {c d}
    The first makes one string with a separator; the second merges lists.

    Hint: One produces a string, the other a list.

    Joining is how a report line or a comma-separated argument is built, and split is its inverse. Merging produces a flat list, which is the difference from lappend, since appending a list adds it as one element. Tcl reference

  • set pin {dpath/a_reg[9]/D} report_slack $pin eval "report_slack $pin"
    The second substitutes the name a second time, so the brackets run as a command.

    Hint: One of the two lines lets the brackets be seen by the interpreter again.

    Substitution happens once per evaluation, so passing a variable directly is safe however strange the name is. Building a command as a string and evaluating it exposes every special character again. Where a command must be built, list quotes each part correctly. Tcl reference

  • for {set i 0} {$i < $n} {incr i} { ... } while {[gets $fh line] >= 0} { ... }
    Both are ordinary commands whose arguments are braced scripts.

    Hint: Count the pieces of the first line and ask what each one is.

    The loop takes four arguments: a start script, a test expression, a step script, and a body. Nothing here is special syntax, which is why the opening brace must sit on the same line as the command: a newline would end it. Tcl reference

  • foreach c $cells { if { [string match "FILLER*" $c] } { continue } if { $found } { break } }
    The first goes to the next item; the second ends the loop entirely.

    Hint: One skips the rest of this pass, the other leaves the loop.

    Both are implemented as return codes rather than as jumps, which is why they work inside a braced body but not inside a procedure called from one. A procedure that wants to stop its caller's loop has to return a value the caller tests. Tcl reference

  • switch -glob -- $name { *_reg* - *_ff* { set kind register } default { set kind logic } }
    Glob matching, with the first pattern falling through to the next body.

    Hint: One branch has a body of a single hyphen.

    A body of one hyphen means use the next one, which is how several patterns share code. The double hyphen ends the options, so a value beginning with one is not mistaken for a switch. Without -glob the match is exact, and -regexp is also available. Tcl reference

  • proc report {stage args} { puts "$stage: $args" } report place -verbose -limit 10
    Whatever is left over arrives as a list in the final parameter.

    Hint: The last parameter has a name the language treats specially.

    This is how a wrapper passes options through to the command it calls, usually with {*}$args so the list is expanded back into separate arguments rather than passed as one. It is the standard way to wrap a tool command and add a default. Tcl reference

  • proc worst {slacks} { lindex [lsort -real $slacks] 0 }
    The value of the last command is returned.

    Hint: There is no return statement, and yet the procedure is useful.

    An explicit return is still clearer, and it is needed to leave early or to return something other than the last value. Its option form sets the code as well, so return -code error raises an error the caller can catch, exactly as error does. Tcl reference

  • namespace eval flow { variable count 0 proc reset {} { variable count; set count 0 } } flow::reset
    A named scope for procedures and variables.

    Hint: Ask what would happen if the procedure were called `reset` at the top level.

    A tool's interpreter already holds hundreds of commands, and a procedure named the same as one of them replaces it silently for the rest of the session. Namespacing a script's own procedures is the way to stay clear of that, and variable is how a procedure reaches the namespace's data. Tcl reference

  • regsub -all {\[(\d+)\]} $name {_\1} flat
    Replaces every match, writing the result into the named variable.

    Hint: The last argument is a variable name, and the pattern has a group in it.

    The return value is the number of substitutions made, and the new string goes into the variable given last; without that argument the string is returned instead. In the replacement, a backslash and a digit stand for the matching group, so bus bits become underscores here. Tcl reference

  • set nums [regexp -all -inline {-?\d+\.\d+} $line]
    A list of every match in the line, instead of one or zero.

    Hint: Ask what is returned when the match variables are left out.

    This is the compact way to pull the numbers out of a report line without a loop. With capture groups, each match contributes the whole match and then each group, so the list has to be read in strides. Tcl reference

  • string trim $line string first ":" $line string range $line 0 $i
    Strip the whitespace, find a character's position, and take a slice.

    Hint: Three small operations that together take a line apart.

    Positions are counted from zero and end may be used in place of a number, as in string range $line 0 end-1. Between these and the pattern commands, most report parsing needs no regular expression at all. Tcl reference

  • dict for {key value} $metrics { puts [format "%-12s %s" $key $value] } dict exists $metrics wns dict keys $metrics
    Walks every pair in insertion order, with the keys available separately.

    Hint: Ask what order the pairs come out in.

    Insertion order is guaranteed, which is what makes a dictionary good for a report whose rows should stay in the order they were built. Testing with dict exists avoids the error that fetching a missing key raises. Tcl reference

  • set metrics [dict create wns -0.06] set area(nand2) 0.798
    A dictionary is a value; an array is a variable.

    Hint: Ask which of the two can be returned from a procedure.

    Because an array is a collection of variables, a procedure has to reach it with upvar or pass it as array get. A dictionary is an ordinary string, so it can be passed, returned, and nested. Arrays remain useful for very large tables that are updated in place. Tcl reference

  • dict set runs base place wns -0.06 dict get $runs base place wns
    A path of keys into a dictionary of dictionaries.

    Hint: Count the words between the command and the value.

    Intermediate levels are created as needed on a set, which makes this a tidy way to collect results per run and per stage. Reading a path that does not exist raises an error, so dict exists takes a path too. Tcl reference

  • dict incr counts $type dict lappend byType $type $violation
    Count into a key, and collect a list under a key.

    Hint: Both work on a key that may not be there yet.

    This is the whole of a violation summary: walk the report, increment by type, and keep the instances under the same key. Neither needs the key to exist first, which removes the usual test before each update. Tcl reference

  • set fh [open $report r] while { [gets $fh line] >= 0 } { ... } close $fh
    The line's length, which is negative only at the end of the file.

    Hint: Ask what the command returns, given what the test compares it with.

    An empty line has length zero, so testing for greater than or equal to zero is right and testing for greater than zero silently stops at the first blank line. Reading a whole file with read is simpler but loads all of it, which matters on a large report. Tcl reference

  • if { [catch { exec grep -c VIOLATED $rpt } out] } { set out 0 }
    A non-zero exit status raises an error, so it has to be caught.

    Hint: The command finds nothing. Ask what that does to the script.

    Anything the command writes to its error stream, and any non-zero status, becomes a Tcl error. That is why counting matches is wrapped like this, and why 2>@1 or a redirect to a file appears in scripts that expect warnings from the command they run. Tcl reference

  • set out [file join $::env(RESULTS_DIR) reports timing.rpt] file mkdir [file dirname $out]
    Builds a path correctly and creates the directories leading to it.

    Hint: Ask what this avoids, compared with building the path with slashes.

    Joining handles a trailing separator and an absolute component correctly, which string concatenation does not. Making a directory creates every missing level and does not complain when it is already there, so no test is needed first. Tcl reference

  • try { set fh [open $f r] process $fh } finally { close $fh }
    The last block runs either way, error or not.

    Hint: Ask what runs if the middle line raises an error.

    The error still propagates; the block only guarantees the cleanup. There is also trap for handling a particular kind of error. In a tool script this is how a report file, a redirect, or a changed setting is restored whatever happens. Tcl reference

  • set code [catch { risky } result opts]
    The status, the result or message, and an error dictionary.

    Hint: Three things come back. Ask what the third one is for.

    The options hold the error code, the stack trace, and the return code, which is what a wrapper needs to re-raise an error faithfully with return -options $opts $result. Catching and reporting only the message throws away everything that makes the failure findable. Tcl reference

  • puts $::errorInfo
    The stack trace of the last error.

    Hint: Ask what more this holds than the message the error carried.

    A tool that prints only a one-line message leaves this behind, and it usually names the procedure and the line. A script that catches errors should record it, since the trace is gone as soon as the next error replaces it. Tcl reference

  • set regs [get_cells *_reg*] llength $regs sizeof_collection $regs
    A collection is not a list, so counting it needs the tool's own command.

    Hint: Ask what the second line counts, in a tool that returns objects.

    Commercial timers and place-and-route tools hand back a collection, which is a handle to a set of objects rather than a string. Counting it with a list command gives one, or nonsense. The open tools return ordinary Tcl lists, which is why a script moved between them needs care here. Tcl reference

  • foreach_in_collection c [get_cells *] { puts [get_object_name $c] }
    The tool's own loop over a collection, and the way to get an object's name.

    Hint: Two commands here are not Tcl's. Ask what each replaces.

    The loop variable holds an object, not a string, so printing it directly gives an internal identifier. Where the open tools let foreach work over a list of objects, the commercial ones need this form, and it is the commonest thing to change when a script crosses between them. Tcl reference

  • set ep [lindex $endpoints 0] report_slack $ep report_slack "dpath/a_reg[9]/D"
    Passing the object avoids the brackets in the name being substituted.

    Hint: Look at what the quoted name contains.

    Inside quotes the brackets are still a command substitution, so the name has to be braced or escaped. Keeping objects in variables and passing them straight through sidesteps the whole problem, which matters because bus bits and generated names are full of special characters. Tcl reference

  • redirect -variable txt { report_timing } with_output_to_variable txt { report_checks }
    Capture a report's text into a variable instead of printing it.

    Hint: Both do the same job, under different names.

    A report command prints and returns nothing, so parsing its output means capturing it first. The first spelling is the commercial one and the second is OpenSTA's. Both take the command as a script, so it must be braced, not built as a string. Tcl reference

  • foreach c $cells { if { [get_property [get_cells $c] is_buffer] } { ... } }
    It queries the database once per cell, not once in total.

    Hint: Ask what the inner call does on every pass of the loop.

    A database query costs far more than a Tcl operation, and a loop over a million instances turns a second into an hour. The fix is to ask once, with a filter in the query, and loop over what comes back. This is the usual reason a constraint script takes longer than the tool stage it configures. Tcl reference

  • set half [expr $period / 2]
    The value is substituted into the expression before it is parsed.

    Hint: Ask what the expression looks like after substitution, if the variable holds text.

    With a plain number it works. If the variable holds 2+1, the expression becomes 2+1 / 2, which is not what was meant, and a value from a file or a tool can hold anything. Bracing the expression also lets it be compiled once rather than reparsed on every pass of a loop. Tcl reference

  • if { $slack < 0 } { puts "failing" }
    The newline ends the command, so the body is not passed to it.

    Hint: Remember that the body is an argument, not a block.

    A command ends at a newline, so this is a conditional with no body, followed by a script that the interpreter tries to run as a command. The opening brace has to sit on the same line, which is why every Tcl script looks the way it does. Tcl reference

  • set count 0 proc bump {} { incr count }
    The procedure has its own scope, so it does not see that variable.

    Hint: Ask which variable the procedure is reaching for.

    A procedure sees only its own variables and those it names with global, variable, or upvar. As written it fails, since there is no local variable to increment. The clearer fix in a tool script is often to pass the value in and return the new one. Tcl reference

  • set reports [glob $dir/*.rpt]
    Matching nothing is an error, which stops the script.

    Hint: Ask what happens on the run where the stage produced nothing.

    The command raises an error when no file matches, which is right for a required file and wrong for an optional one. Adding -nocomplain returns an empty list instead. Unlike a shell, nothing else expands the pattern: it is matched by this command alone. Tcl reference

  • You want to walk a very large report one line at a time, without holding the whole file in memory, and without an extra empty pass at the end.
    while { [gets $fh line] >= 0 } { ... }

    Hint: One of these tests the wrong thing, and runs the body once too often.

    The end of a file is only known once a read has failed, so testing for it first runs the body one extra time with a stale line. Reading the whole file and splitting it is correct but loads everything, which matters on a report of millions of lines. Tcl reference

  • Your procedure is given the name of a variable belonging to its caller, and it has to change that variable rather than a copy.
    upvar 1 $varName local

    Hint: The procedure needs a local name that stands for the caller's variable.

    Linking the names makes every later use of the local name act on the caller's variable. Declaring it global reaches the top level instead, which is wrong when the caller is another procedure, and assigning through the name only works if the variable happens to be visible. Tcl reference