Tcl for EDA Engineers: The Language Every Flow Script Is Secretly Written In
If you work in EDA, you write Tcl. You may not have wanted to — you may have gone into hardware to escape scripting — but Synopsys Design Compiler, PrimeTime, IC Compiler, Innovus, Genus, Tempus, Calibre, Quartus, Vivado, ModelSim, Questa, and just about every major tool exposes its command interface as a Tcl interpreter. When you type report_timing or set_max_delay 3.0 -from A -to B, you are typing Tcl commands. When you write a flow script that runs synthesis and then P&R, you are writing a Tcl program.
Most EDA engineers learn just enough Tcl to get by. They know that set foo 5 assigns 5 to foo, and that $foo references it, and that proc makes a procedure. And then they fight the language for the rest of their careers — subtle bugs with list vs string, mysterious “extra characters after close-brace” errors, confusing quoting rules, surprising behavior when variables contain spaces.
The thing is, Tcl is a small, consistent language once you understand its fundamental rule: everything is a string, and the interpreter runs exactly twelve substitution rules on every command, in a predictable order. Once that clicks, the language stops fighting you. This post is the “learn Tcl properly” version that targets the working EDA engineer — enough to write flow scripts that do not break under weird input, to read and extend other people’s scripts, and to stop confusing curly braces with square brackets.
The one rule that explains everything: Dodekalogue
Tcl’s syntax has exactly twelve rules (Ousterhout called them the “Dodekalogue”). The most important ones for day-to-day work:
- A command is a list of words separated by whitespace, terminated by a newline or a semicolon.
- Words can be grouped with double quotes (
"...") — substitutions happen inside. - Words can be grouped with curly braces (
{...}) — substitutions do not happen inside. The braces are stripped and the raw text is the word. - A
$nameis substituted with the value of the variablename.$name(index)for array elements. - A
[command ...]is replaced with the result of executingcommand. - Backslashes escape:
\n,\t,\\,\$. - Each command returns a string.
That is it. The interpreter does substitution once (in the correct order: backslash, command, variable) then invokes the first word as a command with the remaining words as arguments. No implicit evaluation, no hidden quoting, no operator precedence that applies outside expr.
When you internalize “braces suppress substitution; quotes allow substitution; everything is a string until a command parses it differently,” the language stops being weird.
Assignment, variables, and the difference from every other language
|
|
Critical distinction: in most languages, variables have types. In Tcl, variables are strings, but a command can interpret the string as a list, integer, floating-point, dict, or whatever it needs. The set command does not care about type; it just stores the string you gave it. When you later call expr {$foo + 3}, the expr command parses $foo as an integer.
This is liberating (Tcl handles arbitrary precision integers, rational numbers, and bignums correctly) and occasionally painful (you have to be careful with numeric strings that have leading zeros, because 08 looks like octal).
Command substitution
Square brackets run a command and substitute its result:
|
|
Inside an EDA tool, most of what you do is command substitution: take the output of one command (say, all_registers) and feed it to another (say, set_false_path -from). Square brackets are how you compose commands.
|
|
Read that as: run all_inputs (returns a list of all input ports), run get_pins ... (returns a list of matching pins), then invoke set_false_path with -from the first list and -to the second.
Lists are strings (usually)
A Tcl list is a string with elements separated by whitespace. {a b c} is a three-element list. So is "a b c". So is a\ b\ c. The list commands — lindex, lrange, lappend, llength, lsort, lsearch — treat the string as a list when they need to:
|
|
lappend is special: it is the idiomatic way to grow a list because Tcl can do it in place without reparsing. set things "$things date" works but reformats the list; lappend things date does not.
When EDA tools print a list of things, you are almost always safe to feed it back to another command as a list. The language is designed for this.
Dicts and arrays
Two data structures for key-value work:
|
|
Arrays in Tcl have one quirk: they are always global/scoped — you cannot pass an array as a value into a procedure. Dicts can be passed around like any other value, which makes them generally preferable in modern code. Older EDA scripts use arrays everywhere because dicts arrived later.
Procedures
|
|
Arguments are always passed by value. Default arguments and args (variable-length):
|
|
The args parameter receives a list of any remaining arguments — how most EDA tool commands with many optional flags are implemented.
Pass-by-reference with upvar
Because arguments are copied, you cannot modify a caller’s variable directly. upvar gives you access to a variable in the caller’s scope:
|
|
upvar 1 means “one level up the call stack” (the immediate caller). In a deeper-nested procedure you can go further. This is the idiomatic Tcl way to pass a variable “by reference” — and the cleanest way to work with arrays inside procedures:
|
|
uplevel runs a block of code in a specified caller’s scope, which is how commands like set_multicycle_path can modify the tool’s global state from inside a procedure.
Namespaces
As scripts grow, global variables collide. Tcl namespaces give you scoping:
|
|
Namespaces nest. ::myflow::utils::hash_name is a procedure in the utils namespace inside myflow at the global level. You can namespace import to bring specific commands into the current namespace without the prefix.
In practice, EDA flow scripts tend to live in one global namespace because that is what tools expect and what newcomers can follow. Libraries that want to be reused should use namespaces.
The expr trap
Tcl does not have operator precedence outside the expr command. To evaluate arithmetic or boolean expressions, you must wrap them in expr:
|
|
Always brace the expression. expr $x + $y works (substitutes first, then evaluates), but expr {$x + $y} is both faster (expr compiles braced expressions) and safer (it avoids double-substitution issues when a variable contains something like 1; exec rm -rf /). The EDA code you read in the wild will often have un-braced expressions; the code you write should not.
if and while conditions are evaluated with expr internally, but you still brace them:
|
|
The outer braces are a word group (raw text), which Tcl passes to if/while which then evaluates with expr. This is why if {cond} {body} looks like a language construct; it is actually just a command invocation. There are no reserved words.
Control flow
|
|
switch -exact -- is the defensive form. -- ends option parsing (useful if $mode starts with -); -exact means “no pattern matching.” Without it, switch may treat the value as a pattern. Be explicit.
String comparison: eq vs ==
Do not compare strings with ==. Use eq:
|
|
For integers, == is the only sensible choice. For strings, eq and ne are the string equality operators introduced in Tcl 8.4 and now universally supported. Mixing them up leads to rare bugs — "0" and "0.0" are numerically equal but not string-equal.
regexp and regsub
|
|
Tcl regexps are POSIX extended by default. regexp returns 1/0 indicating match; any additional arguments capture groups. regsub -all replaces all occurrences.
One thing that trips people up: Tcl’s regex uses \w, \d, etc., but you must brace the pattern so the backslashes do not get eaten by the interpreter’s own substitution:
|
|
Always use {...} for regex patterns.
File I/O
|
|
gets returns −1 at EOF. read -nonewline strips the trailing newline, which you usually want.
exec runs an external process:
|
|
exec throws on non-zero exit, redirects and pipes look shell-like, and backticks do not exist — use [exec ...].
The module boundary: source, package require
Split large scripts by sourcing other files:
|
|
For reusable libraries, use packages:
|
|
|
|
package require searches auto_path for a file matching the package, loads it, and verifies the version. More disciplined than source; useful when multiple scripts or teams share libraries.
Error handling
|
|
catch returns 0 on success, 1 on error. result holds the return value or error message. options is a dict with error tracebacks, error codes, etc.
For cleanup, use try...finally (Tcl 8.6+):
|
|
Tools frequently return error strings or throw Tcl errors on bad input. Wrap calls that might fail and produce a meaningful message for your flow’s log.
Tcl conventions in EDA tools
Every EDA tool extends Tcl with commands, but they follow conventions. Worth recognizing:
get_*commands — return collections of objects (pins, cells, nets, clocks). Chained with each other.all_*commands — shorthand for common collections:all_inputs,all_outputs,all_registers,all_clocks.set_*commands — set constraints or properties.report_*commands — print tables.-filteroption — common filter syntax across many tools (likeget_cells -filter "ref_name =~ NAND*").- Collections vs lists — Synopsys tools (PrimeTime, Design Compiler) use a collection object that is not always a real Tcl list. Many commands take either; some distinguish.
query_objects,foreach_in_collectionexist because of this. - Interpolation vs braced expansion — EDA tool commands often have two ways to write a constraint.
set_input_delay 2 -clock CLK [get_ports data]vsset_input_delay 2 -clock CLK $data_ports. Both valid.
Writing maintainable flow scripts
A few patterns that survive years of reuse:
Small, composable procs
|
|
Procedures with obvious names. Block-and-stage parameters threaded through. No hidden global state.
Configuration as data, not code
|
|
Build a top-level config dict; consult it from procedures. When the config needs to change for a different run, you change data, not code.
Logging and run metadata
|
|
Write a log that includes timestamps, intermediate metrics, file paths touched. Your future self, debugging a run from two weeks ago, will thank you.
Defensive constraint application
|
|
When constraints reference signals that may or may not exist in the current design (because of variant builds, conditional IP, etc.), wrap them in a defensive procedure that logs the failure instead of aborting the entire flow.
Script boundaries: what is a “good” Tcl file
Shape your scripts so each file has one purpose:
env.tcl— set up environment variables, library paths, tech settings.sources.tcl— list of RTL and SDC inputs.constraints_<block>.sdc— design constraints for one block.flow_<stage>.tcl— the script that drives one stage (synth, place, cts, route, signoff).utils.tcl— shared procedures.report_<stage>.tcl— post-stage reports.
Avoid one-giant-script-per-project. A 2000-line Tcl file is a future nightmare; five 400-line files with clear boundaries is a team asset.
Common Tcl traps that bite EDA engineers
Unintended string-list conversion
|
|
Fix: quote when you mean a single string, brace or list-ify when you mean a list.
Forgotten expr braces
|
|
Braces suppress substitution, so expr {$expr_str} evaluates the string $expr_str as an expression, which means “the variable expr_str.” Not a math expression. Confusing. Rule: brace only literal expressions you write at the call site.
if condition in strings
|
|
Use expr $cond inside if if you must do this — or restructure to not store expressions in variables.
File paths with spaces or metacharacters
|
|
Always list-ify when passing paths to commands that accept lists.
Global vs local variables
|
|
Tcl procedures do not see global variables by default. Use global foo or variable foo (for namespace members):
|
|
In modern code, prefer passing values as arguments to reduce global state.
source vs package require search paths
|
|
Always use the [info script]-based pattern for source statements inside a script. Relative paths from cwd break when someone runs your script from a different directory.
Debugging Tcl
Tcl has no built-in debugger in most EDA tools, but the tools do:
putseverything. Tcl execution is cheap; printing intermediate values costs nothing.parray arrnameprints an array in full.dict forloops print dicts readably.info vars,info procs,info commands,info body procnameintrospect.trace add variableruns a callback on every write:
|
|
return -code errorexplicitly raises an error with a message.unknownis a built-in Tcl command called when an unknown command is invoked. Can be overridden to log missing commands.
For heavy-duty debugging, TclPro and tclsh -d exist, but in-tool printf is 95% of the workflow.
When to leave Tcl
Tcl’s strengths are composability, simplicity, and being the EDA tool lingua franca. Its weaknesses are everything data-processing-heavy: large data structures, complex string parsing, concurrency, package management.
If you are writing a flow script to drive tools, Tcl is correct. If you are writing a 1000-line report parser, a statistical analyzer, or a complex DRC report summarizer, write it in Python and call it from Tcl via exec:
|
|
This is how most modern EDA teams split work: Tcl for orchestration and tool calls, Python for data munging and web UIs, both coexisting by exec.
Tcl 8.6 features worth using
If your tool runs Tcl 8.6+ (most modern tools do):
try/on/finally— proper exception handling.dict— first-class dictionaries.lmap— functional-style list map.coroutine— cooperative multitasking (useful for concurrent tool commands).apply— anonymous procs / lambdas:
|
|
Older flows written for 8.4 work but are more verbose. Newer tool releases have happily moved to 8.6; use the features.
A short reference you can keep
|
|
Wrapping up
Tcl is an unassuming language that looks odd the first time you see it and becomes one of your most-used tools once you understand it. Every EDA tool you touch is listening for Tcl. Every flow script your team maintains is Tcl. Every constraint file in your project is — at some level — Tcl.
The two investments that pay off are:
- Really understand the substitution rules. The twelve Dodekalogue rules explain why things work the way they do, and once you know them, Tcl stops surprising you.
- Use the modern idioms.
eqfor strings, bracedexpr, dicts for key-value data, namespaces for libraries,try/finallyfor cleanup. The language has grown; your code should too.
Do that, and Tcl stops being the thing you fight and becomes the fabric your flows are woven from. And your scripts will still be running — and still being read — five years from now.
Comments