In short: Every major EDA tool embeds a Tcl interpreter because Tcl is a tiny, embeddable "everything-is-a-string" language, and mastering procs, collection commands, and a handful of flow idioms lets you drive Synopsys, Cadence, and Siemens tools with one portable skill set.
Open a shell in Design Compiler, Innovus, PrimeTime, Genus, IC Compiler II, or Tessent and you are talking to the same language: Tcl. For a VLSI engineer this is one of the highest-leverage things you can learn, because the scripting layer is identical across vendors even when the underlying algorithms differ wildly. This article explains why the whole industry converged on Tcl, then builds up from language fundamentals to a reusable starter you can drop into a place-and-route or static-timing flow.
Why does every EDA tool use Tcl?
Tcl (Tool Command Language, created by John Ousterhout in 1988) was designed from day one to be embedded. The entire interpreter is a C library of roughly 10,000 lines that a tool vendor links into their binary and extends with custom commands. When Synopsys wants report_timing or Cadence wants place_opt, they simply register a new C function as a Tcl command. To the user it looks native; under the hood it is just Tcl_CreateObjCommand().
Three properties sealed the deal:
- Everything is a string. There is no separate type system to marshal across the C boundary. A net name, a slack value, and a list of cells are all strings, which makes the embedding API trivial.
- It is a real programming language. You get
proc,if,for,foreach, associative arrays, andregexp— enough to express a full flow, not just a config file. - It was first. By the mid-1990s Synopsys adopted Tcl for dc_shell; OpenAccess and the OASIS-era tools followed, and the IEEE 1801 (UPF) and SDC constraint formats are themselves Tcl dialects. Network effects did the rest.
Key fact: SDC (Synopsys Design Constraints) is not a bespoke format — it is pure Tcl. Commands like create_clock -period 2.0 [get_ports clk] execute in the tool's interpreter, which is why you can wrap them in loops and conditionals.
The language in five minutes
Tcl's syntax is almost brutally simple: a command is a word followed by arguments, separated by whitespace. Square brackets mean "run this and substitute the result"; braces defer evaluation. That is nearly the whole grammar.
set period 2.0 ;# a variable (a string "2.0")
set clk [get_ports clk] ;# command substitution
puts "Period is $period ns" ;# $ expands a variable
if {$period < 1.0} {
puts "Fast clock!"
}
foreach pin {A B CIN} {
puts "input: $pin"
}
Two rules trip up every beginner. First, {braces} suppress substitution — they are how you pass code blocks and literal expressions. Second, expr is required for math: set x [expr {$a + $b}], never set x $a+$b. Always brace the expression inside expr for both speed and safety.
Procs: your flow's building blocks
A proc is a reusable procedure. In production flows you wrap every repeated step — clock setup, reporting, ECO patterns — so your top-level script reads like pseudocode.
proc report_worst {corner {n 10}} {
puts "=== Worst $n paths @ $corner ==="
report_timing -delay_type max -nworst $n \
-max_paths $n > reports/${corner}_timing.rpt
}
report_worst slow 20
report_worst fast
Note the default argument {n 10} and args for variadic procs. Because Tcl passes everything by value, large flows use upvar and global deliberately to share state — a common interview topic.
Collections: the one abstraction that differs by vendor
Here is the subtlety that separates a Tcl beginner from someone who can actually ship a flow. A plain Tcl list holds strings. But querying a 50-million-instance netlist and returning every cell as a string list would be catastrophically slow and memory-hungry. So vendors invented collections: opaque handles to objects living in the tool's C database.
Synopsys calls them collections and iterates with foreach_in_collection; Cadence Innovus/Genus exposes similar object queries. The critical gotcha: you cannot use a normal foreach on a Synopsys collection, and $coll does not expand to a usable list.
# Synopsys style — correct
set viol [get_cells -filter "is_violating == true"]
foreach_in_collection c $viol {
set nm [get_object_name $c]
puts "size_only $nm"
}
puts "Total: [sizeof_collection $viol] cells"
| Concept | Synopsys (DC/PT) | Cadence (Innovus/Genus) |
|---|---|---|
| Query objects | get_cells / get_pins | get_cells / get_db |
| Iterate | foreach_in_collection | foreach on get_db list |
| Count | sizeof_collection | llength |
| Name | get_object_name | get_db obj .name |
Key fact: On a modern SoC, get_cells * can match tens of millions of objects. A collection returns in milliseconds; converting it to a string list can blow past gigabytes of RAM — this is exactly why the abstraction exists.
A starter you can adapt for PnR and STA
Below is a minimal, vendor-neutral skeleton. In practice you source a project settings file, guard every stage, and write a report after each step so a crashed run is debuggable. This "sourced setup + staged flow + per-stage reports" pattern is what you will see in any real production repository.
# ---- settings.tcl ----
set DESIGN aes_core
set CLK_PER 1.25
set REPORTS ./reports
file mkdir $REPORTS
# ---- run.tcl ----
source settings.tcl
read_verilog ${DESIGN}.v
link_design $DESIGN
create_clock -name clk -period $CLK_PER [get_ports clk]
set_input_delay 0.2 -clock clk [all_inputs]
set_output_delay 0.2 -clock clk [all_outputs]
proc stage {name body} {
puts "\n>>> STAGE: $name"
if {[catch {uplevel 1 $body} err]} {
puts "ERROR in $name: $err"; exit 1
}
}
stage synth { compile_ultra }
stage timing { report_timing -nworst 10 > $::REPORTS/post_syn.rpt }
stage power { report_power > $::REPORTS/power.rpt }
Swap compile_ultra for place_opt/route_opt in Innovus, or drop the synthesis stage entirely in a PrimeTime sign-off script. The scaffolding — stage, catch, per-corner reports — stays identical. That portability is the whole point.
proc that writes a report.Debugging and good habits
Real flows live or die on error handling. Wrap risky commands in catch, use set_message_severity to demote noisy warnings, and keep scripts idempotent so a re-run does not corrupt state. For tool-specific syntax lookups, prefer the interactive man/help built into dc_shell and Innovus over guessing — argument names differ subtly between tool versions and will silently no-op if misspelled. This mirrors the broader VLSI flow we cover in our RTL-to-GDS design flow guide and our deeper static timing analysis primer.
If you want hands-on tool time on genuine Synopsys and Cadence licences, the design-services and lab team at Avecas VLSI design services runs industry flows worth studying alongside the theory.
Want to script real tapeout flows? CourseTron's VLSI Physical Design and STA tracks put you in front of live dc_shell, Innovus, and PrimeTime sessions with guided Tcl labs. Explore the full course catalogue to get started.
Frequently asked questions
Do I need to master all of Tcl to work in VLSI?
No. Around 80% of flow scripting uses a small core: set, proc, foreach/foreach_in_collection, if, regexp, catch, and the tool's own commands. Depth comes from knowing the vendor command set, not obscure language features.
Why can't I just use Python for EDA scripting?
Newer tools expose Python APIs, but the interpreter embedded in the binary and every legacy constraint file (SDC, UPF) is Tcl. You will read and maintain Tcl for years regardless, so it remains the foundational skill.
What is the difference between a list and a collection?
A Tcl list is strings in your interpreter. A collection is an opaque handle to objects in the tool's C database — far faster and smaller for large designs, but it requires vendor commands like foreach_in_collection to iterate.
Why does my math give the wrong answer?
Because Tcl treats everything as a string. Use expr with braced operands: set s [expr {$a + $b}]. Without expr, $a+$b is just the literal text.