⚡ Tcl 8.6+ Standard • Complete Scripting & Semiconductor EDA Automation Reference

Tcl Scripting & EDA Automation Complete Cheat Sheet

110 authoritative topics covering core Tcl 8.6+ semantics, Dodekalogue evaluation rules, dictionaries, procs, regex, file I/O, subprocesses, and the industry-standard EDA Semiconductor Automation Suite (Synopsys Design Compiler & PrimeTime, Cadence Innovus & Genus, AMD/Xilinx Vivado, OpenROAD, SDC timing constraints, collections, slack audits, and ECO generators).

Filter: Showing 110 of 110 topics

No matching Tcl topics found

Try searching for a different keyword like dict, regexp, foreach_in_collection, get_cells, proc, or expr.

📜 Module 1: The Tcl Evaluation Model & Fundamentals (The Dodekalogue)

§1

Basic Script Structure & Shebang

⚡ Core Tcl 8.6

A Tcl script is a UTF-8 text file consisting of commands separated by newlines or semicolons (;). The standard portable shebang line invokes tclsh via /usr/bin/env. Tcl does not have a distinct compilation step; code is evaluated directly or dynamically compiled into bytecode on the fly.

tcl
#!/usr/bin/env tclsh
# ==============================================================================
# Script: hello_world.tcl
# Description: Canonical Tcl 8.6+ executable script structure
# ==============================================================================

package require Tcl 8.6

puts "Hello, world! Welcome to Sovereign Tcl Scripting."
puts "Tcl Interpreter Version: [info patchlevel]"

# Semicolons allow multiple commands on a single line:
set project "Apollo"; set revision 4; puts "Project: $project Rev: $revision"
⚠️ Production Trap & Gotcha
Never use hardcoded paths like #!/usr/bin/tclsh in enterprise EDA environments. EDA vendor tools (Synopsys, Cadence, Mentor) ship their own internal tclsh builds located in proprietary trees. Always use /usr/bin/env tclsh or invoke via the tool shell (e.g. dc_shell -f script.tcl or vivado -mode batch -source script.tcl).
§2

The 12 Rules of Tcl (The Dodekalogue & Core Mental Model)

⚡ Core Tcl 8.6

Unlike Python, C++, or Verilog, everything in Tcl is fundamentally a command. There are no reserved syntax keywords (even if, for, proc, and set are standard commands). Evaluation operates in two strict, decoupled phases:
1. Grouping & Substitution Phase: The interpreter parses words separated by whitespace and performs substitutions ($, [], \) from left to right in a single pass.
2. Command Execution Phase: The first word is looked up as the command name, and the remaining words are passed as argument strings without further interpretation.

tcl
# In Tcl: Command word0 word1 word2 ...
# All words are strings until interpreted by the specific command!

set cmd puts
$cmd "Dynamically dispatched puts!"  ;# Rule: variable substitution happens BEFORE execution

# 'if' is just a command that accepts condition and script arguments:
set condition 1
if $condition {
    puts "Evaluated as condition!"
}
⚠️ Production Trap & Gotcha
Because Tcl evaluates in a single pass left-to-right, substitutions inside words occur before the command receives them. If you don't use braces {} to defer substitution, nested logic can evaluate prematurely or suffer code-injection vulnerabilities.
§3

Comments (#) & The End-of-Line Semicolon Trap

⚠️ Trap & Gotcha

In Tcl, # is not a comment token anywhere on a line; it is only a comment if it appears where a command name is expected (at the start of a line or immediately after a semicolon ;). Tcl does not support C-style multi-line comments (/* ... */).

tcl
# Valid comment: starts at the command position of the line
set clk_period 10.0

# INCORRECT / FATAL SYNTAX ERROR:
# set clk_period 10.0 # Define clock in nanoseconds
# -> Tcl interprets '#' and subsequent text as extra arguments to 'set'!

# CORRECT INLINE COMMENT: Must terminate command with semicolon ';' first:
set clk_period 10.0 ;# Clock period in ns (semicolon starts new command position)

# Multi-line comment idiom:
# Block line 1
# Block line 2
# Block line 3
⚠️ Production Trap & Gotcha
Adding a comment after a command without a preceding semicolon (e.g. puts "run" # comment) will cause Tcl to throw wrong # args: should be "puts ?-nonewline? ?channelId? string" because # is passed as a literal string argument to puts!
§4

Variable Assignment: set & unset

⚡ Core Tcl 8.6

Variables are created or mutated using set varName ?value?. If a value argument is provided, set assigns it and returns the value. If only the variable name is passed, set reads and returns the variable's current value. Variables can be destroyed using unset.

tcl
# Assignment:
set design_name "alu_core"
set num_stages  4
set target_freq 1.25e9

# Reading via 'set' without $ (useful when constructing variable names):
set current_design [set design_name]

# Chained assignment:
set a [set b [set c 0]]

# Unsetting variables:
unset target_freq
# Safe unsetting without error if variable doesn't exist (Tcl 8.4+):
unset -nocomplain target_freq uninitialized_var
⚠️ Production Trap & Gotcha
When assigning, do not prefix the variable name with $! Writing set $var 10 evaluates $var first and assigns to whatever variable name was held inside $var (indirect reference), which is almost always a bug.
§5

Variable Substitution: $var vs ${var}

⚡ Core Tcl 8.6

The $ character triggers variable substitution. The interpreter matches letters, digits, and underscores following $ as the identifier. When variable names contain special characters (hyphens, dots, colons, spaces) or are concatenated directly against alphanumeric suffixes, enclose the identifier in braces: ${var_name}.

tcl
set prefix "cpu"
set suffix "_inst"

# Ambiguous concatenation trap:
# puts "$prefix_inst"  ;# ERROR: looks for variable named 'prefix_inst'!

# Disambiguated concatenation using ${...}:
puts "${prefix}_inst"        ;# Outputs: cpu_inst
puts "${prefix}${suffix}"    ;# Outputs: cpu_inst

# Variable names containing hyphens or punctuation:
set {max-fanout} 32
puts "${max-fanout}"         ;# Outputs: 32
⚠️ Production Trap & Gotcha
In EDA scripts, netlist hierarchical names frequently use punctuation (e.g. u_core/u_alu/clk). Trying to read $u_core/u_alu/clk stops at the slash. Always wrap hierarchical or punctuated paths in ${...} or pass variable names to get_cells directly.
§6

Command Substitution: [command]

⚡ Core Tcl 8.6

Square brackets [...] invoke command substitution. The script within brackets is recursively evaluated as a complete Tcl script, and the standard output/return value of the last command replaces the bracketed text inside the parent command word.

tcl
# Execute 'expr' and substitute result:
set area [expr {1024 * 768}]

# Deep nesting:
set log_file [file join [pwd] "reports" [format "run_%04d.log" 42]]
puts "Writing to: $log_file"

# Multiple command substitutions in a single string:
puts "Active design: [get_current_design] contains [llength [get_cells]] instances."
⚠️ Production Trap & Gotcha
In EDA shells (PrimeTime, Innovus, Vivado), square brackets [get_cells ...] are used for command substitution, but Verilog bus indices also use square brackets (e.g. data[7:0])! If unescaped, Tcl attempts to execute a command named 7:0! Always escape Verilog bus brackets in strings (e.g. data\[7:0\] or {data[7:0]}).
§7

Backslash Substitution & Line Continuation (\)

⚡ Core Tcl 8.6

Backslashes prevent interpretation of special characters (\$, \[, \], \{, \}, \") or insert non-printing control characters (\n newline, \t tab, \r carriage return, \xHH hex, \uHHHH unicode). A backslash at the immediate end of a line acts as line continuation, turning the newline and leading whitespace of the next line into a single space.

tcl
# Escaping special characters:
puts "The price is \$100.00 and bracket literal is \[test\]"

# Clean multi-line command formatting via backslash continuation:
create_clock -name "SYS_CLK" \
             -period 5.0 \
             -waveform {0.0 2.5} \
             [get_ports "clk_in"]

# Line continuation inside long lists:
set eda_tools [list \
    "dc_shell" \
    "icc2_shell" \
    "pt_shell" \
    "innovus" \
]
⚠️ Production Trap & Gotcha
Trailing whitespace after a backslash (\ ) is a silent, fatal bug! The backslash will escape the trailing space instead of the newline, causing Tcl to terminate the command prematurely and treat the next line as an unknown command.
§8

Braces {} vs Double Quotes "" (The Fundamental Distinction)

⚠️ Trap & Gotcha

This is the single most critical concept in Tcl:
• Double Quotes ("...") group words together while enabling variable ($), command ([]), and backslash (\) substitution.
• Braces ({...}) group words together while completely suppressing all substitutions. Everything inside braces is treated as pure literal text.

tcl
set tool "PrimeTime"

# Double Quotes: Substituted immediately
puts "Welcome to $tool (PID: [pid])"
# Output: Welcome to PrimeTime (PID: 14820)

# Braces: Raw literals, zero substitution
puts {Welcome to $tool (PID: [pid])}
# Output: Welcome to $tool (PID: [pid])

# Why this matters for 'expr' and 'proc':
# Code blocks for procs, loops and if-statements MUST be enclosed in braces
# so they are passed intact to the command rather than evaluated upfront!
⚠️ Production Trap & Gotcha
If you write if "$x > 10" { ... }, Tcl performs substitution during initial parsing. If $x contains special characters or spaces, it breaks the grammar. Always use braces: if {$x > 10} { ... }.
§9

Argument Expansion ({*}) vs Legacy eval

⚡ Core Tcl 8.6

Introduced in Tcl 8.5, the expansion prefix {*} (the "splat" operator) unpacks a list into individual argument words before executing the command. It eliminates the need for unsafe, injection-prone eval strings.

tcl
set options [list -exact -nocase]

# Unpacks the 2 elements of $options as separate arguments:
set match [string match {*}$options "FOO*" "foobar"]

# Clean tool command dispatch:
set cmd [list exec gcc -O3 -Wall main.c -o firmware.bin]
{*}$cmd  ;# Executes gcc with exact arguments preserved

# Comparison with dangerous legacy eval:
# eval $cmd  ;# DANGEROUS: Forces second round of evaluation, breaks if filenames contain spaces!
⚠️ Production Trap & Gotcha
Never use eval "cmd $args" in modern Tcl. eval concatenates arguments into a string and re-parses it, meaning filenames with spaces, dollar signs, or brackets will trigger unwanted substitutions or syntax crashes. Always use {*}$args.
§10

Selective Substitution: subst Command

⚡ Core Tcl 8.6

The subst command performs substitutions on a string without executing the resulting string as a command. Flags allow selective disabling of command substitution (-nocommands), variable substitution (-novariables), or backslashes (-nobackslashes). It is the canonical tool for template interpolation and code generation.

tcl
set module "spi_master"
set width  32

# Template with selective variable substitution:
set template {
module $module #(
    parameter DATA_WIDTH = $width
) (
    input  wire clk,
    input  wire [width-1:0] data_in
);
    // Generated automatically on [clock format [clock seconds]]
endmodule
}

# Only substitute $vars, do NOT execute commands inside brackets:
set generated_hdl [subst -nocommands $template]
puts $generated_hdl
⚠️ Production Trap & Gotcha
If you call subst $template without -nocommands on user-supplied or external files, any square brackets (e.g. [exec rm -rf ...] or Verilog bus indices [31:0]) will attempt execution! Always pass -nocommands when rendering HDL templates.

2 Variables, Scopes & Arrays

§11

Checking Variable Existence: info exists

Core Tcl 8.6

In Tcl, reading an uninitialized variable raises a fatal runtime error (can't read "var": no such variable). info exists varName checks whether a variable is defined in the current scope without throwing errors, returning 1 if defined, or 0 otherwise.

Tcl
# Safe initialization pattern
if {![info exists clk_freq_mhz]} {
    set clk_freq_mhz 100.0  ;# Default fallback
    puts "INFO: clk_freq_mhz unset, using default: $clk_freq_mhz"
}

# Works for both scalars and array elements:
if {[info exists setup_slack(clk_core)]} {
    puts "Slack recorded: $setup_slack(clk_core) ns"
}
⚠️ Production Trap & Gotcha
Do NOT pass a dollar sign to info exists! Write info exists my_var, NOT info exists $my_var. Passing $my_var evaluates the variable content first and checks whether a variable with THAT name exists (indirect lookup).
§12

Global Variables: global & The $::var Namespace Syntax

Core Tcl 8.6

Procedures in Tcl execute in an isolated local stack frame. To read or write a top-level global variable from inside a proc, you must explicitly bind it using global varName or access it directly using the fully-qualified root namespace prefix $::varName.

Tcl
set DESIGN_NAME "soc_top"
set VIOLATION_COUNT 0

proc check_status {} {
    global DESIGN_NAME VIOLATION_COUNT
    puts "Auditing design: $DESIGN_NAME"
    incr VIOLATION_COUNT
}

# Alternative: Direct namespace resolution (idiomatic in EDA scripts)
proc reset_counter {} {
    set ::VIOLATION_COUNT 0
    puts "Direct global access: $::DESIGN_NAME counter reset to $::VIOLATION_COUNT"
}

check_status
reset_counter
⚠️ Production Trap & Gotcha
Overusing global makes EDA scripts brittle and difficult to debug across sourced sub-flows. Prefer passing arguments or encapsulating state in dedicated namespaces or dictionaries.
§13

Upvar: Passing by Reference to Caller Frame (upvar 1)

Core Tcl 8.6

Tcl does not have raw memory pointers. Instead, upvar ?level? otherVar myVar binds a local variable name directly to a variable in a parent call stack frame. This enables true call-by-reference, custom control structures, and in-place mutation of arrays or large data structures.

Tcl
# In-place increment of an external variable (level 1 = direct caller)
proc double_in_place {var_name} {
    upvar 1 $var_name local_ref
    set local_ref [expr {$local_ref * 2}]
}

set max_fanout 16
double_in_place max_fanout
puts "Doubled max_fanout: $max_fanout"  ;# Outputs: 32

# Passing arrays by reference:
proc print_timing_summary {array_name} {
    upvar 1 $array_name stats
    foreach corner [array names stats] {
        puts "Corner [format %-10s $corner]: $stats($corner) ns"
    }
}
⚠️ Production Trap & Gotcha
When passing the variable name to a proc that uses upvar, pass the plain name without $ (e.g. double_in_place max_fanout). Passing $max_fanout binds to whatever string value is stored inside max_fanout.
§14

Namespace Variables: variable Command

Core Tcl 8.6

The variable command declares or links a variable belonging to the enclosing namespace. Inside procs within that namespace, calling variable varName provides local access to the namespace-scoped variable without polluting the global scope.

Tcl
namespace eval ::sta_auditor {
    # Declare namespace variables with default initial values
    variable wns_threshold 0.0
    variable tns_total     0.0
    variable audited_clocks [list]

    proc configure {threshold} {
        variable wns_threshold
        set wns_threshold $threshold
    }

    proc report {} {
        variable wns_threshold
        variable tns_total
        puts "Auditor configured for threshold: $wns_threshold ns (TNS: $tns_total ns)"
    }
}

::sta_auditor::configure -0.150
::sta_auditor::report
§15

Environment Variables: $::env(...) Array

Core Tcl 8.6

Operating system environment variables are exposed directly through the built-in associative array ::env. Modifying ::env(KEY) automatically updates the environment for all subsequent child processes spawned by exec.

Tcl
# Querying environment variables safely
if {[info exists ::env(PDK_ROOT)]} {
    set pdk_path $::env(PDK_ROOT)
} else {
    set pdk_path "/opt/pdks/sky130A"
    puts "WARNING: PDK_ROOT unset, defaulting to $pdk_path"
}

# Exporting a new variable to child processes (e.g. for synthesis tool)
set ::env(OPENLANE_VERBOSE) "1"
set ::env(NUM_THREADS) "8"

puts "Target PDK: $pdk_path"
puts "System User: [expr {[info exists ::env(USER)] ? $::env(USER) : $::env(USERNAME)}]"
⚠️ Production Trap & Gotcha
Accessing $::env(NON_EXISTENT) throws a runtime error. Always guard lookups with if {[info exists ::env(VAR)]} before reading.
§16

Associative Arrays: Hash Tables in Tcl

Core Tcl 8.6

Tcl arrays are true hash tables (string keys mapped to string values). They are NOT ordered lists. Arrays cannot be passed directly by value to procedures—they must be accessed using upvar or serialized with array get / array set.

Tcl
# Element assignment
set corner_freq(ss_125c) 800
set corner_freq(tt_25c)  1200
set corner_freq(ff_m40c) 1600

# Bulk population from key-value pair list
array set buffer_strengths {
    BUF_X1  1.0
    BUF_X2  2.1
    BUF_X4  4.4
    BUF_X8  8.9
}

# Array operations
puts "Total buffer variants: [array size buffer_strengths]"
puts "Available corners: [array names corner_freq]"

# Iterating over array keys
foreach corner [lsort [array names corner_freq]] {
    puts "Corner $corner => $corner_freq($corner) MHz"
}

# Serializing array to flat list:
set flat_list [array get buffer_strengths]
⚠️ Production Trap & Gotcha
Writing puts $corner_freq throws can't read "corner_freq": variable is array. In Tcl, an array name alone has no scalar value. You must specify a key: $corner_freq($key), or serialize with [array get corner_freq].
§17

Multi-Dimensional Array Emulation: $matrix($r,$c)

Core Tcl 8.6

Tcl does not have native multi-dimensional array structures. Because array keys can be any arbitrary string, multiple dimensions are conventionally emulated by joining indices with a comma (e.g. $arr($row,$col)). The comma is simply a character in the key string.

Tcl
# 2D Grid floorplan density mapping (die coordinate x, y)
for {set x 0} {$x < 4} {incr x} {
    for {set y 0} {$y < 4} {incr y} {
        set grid_density($x,$y) [expr {rand() * 100.0}]
    }
}

# Accessing a coordinate
puts "Tile (2,3) Density: [format %.1f $grid_density(2,3)]%"

# Pattern matching over multi-dimensional keys
puts "All tiles at Row 2:"
foreach key [array names grid_density "2,*"] {
    puts "  Key $key = [format %.1f $grid_density($key)]%"
}
⚠️ Production Trap & Gotcha
Do NOT insert spaces after the comma! $grid_density($x,$y) and $grid_density($x, $y) are two completely different hash keys because the space is preserved literally.
§18

Variable Tracing: trace add variable (Read, Write, Unset Hooks)

Core Tcl 8.6

Variable traces allow you to register callback procedures that trigger automatically whenever a variable is read, written, or unset. This is invaluable in EDA frameworks for logging design changes, enforcing read-only parameters, or tracking state mutations during simulation.

Tcl
# Callback proc signature: name1 (array/scalar), name2 (elem/empty), op (read/write/unset)
proc audit_write {name1 name2 op} {
    upvar 1 $name1 var
    if {$name2 ne ""} {
        puts "AUDIT: $name1($name2) modified -> $var($name2)"
    } else {
        puts "AUDIT: $name1 modified -> $var"
    }
}

set current_stage "synthesis"
# Attach write watcher:
trace add variable current_stage write audit_write

set current_stage "floorplan"  ;# Triggers: AUDIT: current_stage modified -> floorplan
set current_stage "cts"        ;# Triggers: AUDIT: current_stage modified -> cts

# Remove trace when done:
trace remove variable current_stage write audit_write
⚠️ Production Trap & Gotcha
Modifying the traced variable inside its own write-trace callback without temporarily disabling the trace triggers an infinite recursion loop resulting in stack overflow.

3 Expressions, Operators & Arithmetic

§19

The Golden Rule of expr: Double-Bracing Expressions

Trap & Security

Always enclose expressions passed to expr in curly braces: expr {$a + $b}. Unbraced expressions expr $a + $b suffer from double evaluation: the Tcl parser substitutes variables first, then expr evaluates the resulting string. This destroys bytecode compilation speed (up to 30x slower) and introduces critical code-injection vulnerabilities.

Tcl
# ❌ DANGEROUS & SLOW: Double evaluation vulnerability
set user_input "100; exec rm -rf /"
# expr $user_input + 5  <-- EXECUTES MALICIOUS COMMAND!

# ✅ SAFE & JIT-COMPILED: Always double-brace
set a 10
set b 20
set sum [expr {$a + $b}]
set hypot [expr {hypot($a, $b)}]
set is_valid [expr {$sum > 0 && $a != $b}]

# Benchmarking: Braced vs Unbraced in loops
# expr {$i + 1} executes via direct bytecode instructions.
# expr $i + 1 forces re-tokenization on every iteration.
⚠️ Production Trap & Gotcha
Rule of thumb for all Tcl production code: NEVER write an unbraced expr. Even while {$i < 10} and if {$x > 0} rely on expr internally; bracing them ensures one-time compilation.
§20

Arithmetic & Comparison Operators

Core Tcl 8.6

Tcl expr supports standard C-style operators with standard operator precedence: addition (+), subtraction (-), multiplication (*), division (/), remainder/modulus (%), exponentiation (**), and comparisons (<, <=, >, >=).

Tcl
set clk_period_ns 2.5
set setup_slack   -0.125

# Basic arithmetic
set total_latency [expr {$clk_period_ns * 4 + 0.35}]
set power_of_two  [expr {2 ** 10}]          ;# 1024 (exponentiation)
set mod_remainder [expr {1024 % 32}]        ;# 0

# Comparisons return 1 (true) or 0 (false)
set is_violating  [expr {$setup_slack < 0.0}]
set in_range      [expr {$total_latency >= 10.0 && $total_latency <= 20.0}]

puts "Violating: $is_violating, Latency: $total_latency ns"
⚠️ Production Trap & Gotcha
Integer division truncates! In Tcl, expr {5 / 2} yields 2, not 2.5. If you need fractional division, ensure at least one operand is floating-point: expr {5.0 / 2} or expr {double($a) / $b}.
§21

String Equality: eq / ne vs == / != Trap

Trap & Gotcha

Always use eq and ne for string comparisons inside expr. The traditional operators == and != attempt to coerce both operands to numbers first. If two strings resemble numeric notations (e.g. hex 0x10 vs 16, or leading zeros 08), == causes unexpected equality or syntax errors.

Tcl
set str1 "0x10"
set str2 "16"

# ❌ TRAP: Coerces to integers! 0x10 == 16 evaluates to true (1)
puts [expr {$str1 == $str2}]   ;# Prints: 1

# ✅ CORRECT: Lexicographical string comparison
puts [expr {$str1 eq $str2}]   ;# Prints: 0

set tool "innovus"
# Always use eq/ne for EDA tool/state checks:
if {$tool eq "innovus"} {
    puts "Configuring Cadence Innovus flow..."
} elseif {$tool eq "pt_shell"} {
    puts "Configuring Synopsys PrimeTime STA..."
}
§22

Bitwise Operators: &, |, ^, ~, <<, >>

Core Tcl 8.6

Tcl supports low-level bitwise operations identical to C and Verilog: bitwise AND (&), OR (|), XOR (^), unary bitwise NOT (~), left shift (<<), and right shift (>>). Essential for bitfield masking, register modeling, and hardware address decoding.

Tcl
set csr_reg 0x000000A5   ;# 10100101b
set ENABLE_BIT [expr {1 << 0}]   ;# Bit 0
set IRQ_MASK   [expr {0xF << 4}]  ;# Bits 7:4

# Bit testing:
set is_enabled [expr {($csr_reg & $ENABLE_BIT) != 0}]

# Bit setting (OR):
set csr_reg [expr {$csr_reg | (1 << 2)}]   ;# Set bit 2

# Bit clearing (AND with NOT):
set csr_reg [expr {$csr_reg & ~(1 << 0)}]  ;# Clear bit 0

# Extracting field:
set irq_field [expr {($csr_reg & $IRQ_MASK) >> 4}]
puts "Updated CSR: [format 0x%08X $csr_reg], IRQ field: $irq_field"
§23

Ternary Conditional Operator: cond ? expr1 : expr2

Core Tcl 8.6

The ternary operator evaluated inside expr provides compact inline branching. Only the selected branch expression is evaluated (short-circuit execution), preventing division by zero or unwarranted side effects in unchosen paths.

Tcl
set slack -0.045
set status [expr {$slack >= 0.0 ? "MET" : "VIOLATED"}]

set count 0
# Safe division using ternary short-circuiting:
set avg [expr {$count > 0 ? ($total / $count) : 0.0}]

puts "Timing Status: $status (Slack: ${slack}ns)"
§24

Math Functions: sqrt, ceil, floor, round, hypot, log

Core Tcl 8.6

Tcl expr includes standard C standard math library functions directly: sqrt(), hypot(x,y), ceil(), floor(), round(), abs(), log(), log10(), exp(), sin(), cos(), tan(), and rand().

Tcl
# Euclidean wirelength between two cell coordinates (x1, y1) and (x2, y2)
set x1 120.5; set y1 340.0
set x2 250.0; set y2 510.5

set dx [expr {$x2 - $x1}]
set dy [expr {$y2 - $y1}]
set wirelength [expr {hypot($dx, $dy)}]  ;# sqrt(dx^2 + dy^2)

# Rounding to grid steps
set grid_pitch 0.190
set snapped_x  [expr {round($x1 / $grid_pitch) * $grid_pitch}]

puts "Wirelength: [format %.3f $wirelength] um, Snapped X: [format %.3f $snapped_x] um"
§25

Type Conversions: int(), wide(), double()

Core Tcl 8.6

While Tcl strings are polymorphic, expr supports explicit type casting functions: double() forces 64-bit IEEE floating-point conversion, int() truncates to integer, and wide() casts to a 64-bit signed integer.

Tcl
set clock_count "12500000"
set sim_time_ps "450239485739"

# Force floating-point precision for frequency calculation
set freq_ghz [expr {double($clock_count) / (double($sim_time_ps) / 1000.0)}]

# Truncate floating number to whole cycles
set whole_cycles [expr {int(floor($freq_ghz * 100))}]

puts "Simulated freq: [format %.4f $freq_ghz] GHz (Whole cycles: $whole_cycles)"
§26

Large Integers & Arbitrary Precision in Tcl 8.5+

Core Tcl 8.6

Starting with Tcl 8.5, Tcl incorporates LibTomMath for seamless arbitrary-precision arithmetic (bignums). Integer calculations never silently overflow 32-bit or 64-bit boundaries; they transparently scale up to consume available system memory.

Tcl
# Computing 2^128 (128-bit hardware address space) without overflow:
set addr_space [expr {2 ** 128}]
puts "128-bit Address Max: $addr_space"

# Factorial of 50
proc bignum_factorial {n} {
    set res 1
    for {set i 2} {$i <= $n} {incr i} {
        set res [expr {$res * $i}]
    }
    return $res
}
puts "50! = [bignum_factorial 50]"
§27

Floating-Point Rounding & tcl_precision Deprecation

Trap & Gotcha

In legacy Tcl, developers used set tcl_precision 17 to control float formatting. This variable is officially deprecated in modern Tcl and causes thread contention and precision bugs. Use format "%.Nf" $val to format floating-point numbers explicitly.

Tcl
# ❌ AVOID: Obsolete global float precision setting
# set ::tcl_precision 12

# ✅ RECOMMENDED: Format output explicitly via format
set capacitance_pf [expr {1.0 / 3.0}]
puts "Raw IEEE float: $capacitance_pf"
puts "EDA clean print: [format "%.4f" $capacitance_pf] pF"
puts "Scientific notation: [format "%.3e" $capacitance_pf] F"
⚠️ Production Trap & Gotcha
Comparing floating-point values directly with == is notoriously dangerous due to IEEE 754 precision artifacts (e.g. 0.1 + 0.2 != 0.3). Always test differences against an epsilon: expr {abs($a - $b) < 1e-9}.

4 Control Flow, Branching & Loops

§28

If / Elseif / Else Conditionals

Core Tcl 8.6

The if command evaluates a test expression and executes the corresponding body script. In Tcl, curly braces for condition tests and action bodies are command arguments—meaning the opening brace must stay on the same line as if, elseif, or else.

Tcl
set setup_slack -0.045
set drc_violations 0

if {$setup_slack >= 0.0 && $drc_violations == 0} {
    puts "STAGE PASSED: Timing and DRC clean."
} elseif {$setup_slack < 0.0 && $setup_slack > -0.100} {
    puts "STAGE WARNING: Minor timing violation ($setup_slack ns). Running ECO optimizer..."
} else {
    puts "STAGE FAILED: Major timing closure failure ($setup_slack ns)."
}
⚠️ Production Trap & Gotcha
Placing the opening brace on a new line (e.g. if {$test}\n{) causes an error because Tcl sees the newline as the command terminator. Always place { on the same line as if / else, or use a backslash \.
§29

Switch: Exact, Glob (-glob), and Regex (-regexp)

Core Tcl 8.6

The switch command branches based on pattern matching against a target string. Matching modes include -exact (default), -glob (wildcards *, ?), and -regexp (full regular expressions). Use -- before the string to prevent flags in input strings from masquerading as switch options.

Tcl
set cell_ref "DFF_X2_A9TR"

# Glob matching pattern:
switch -glob -- $cell_ref {
    "BUF_*" { puts "Type: Buffer ($cell_ref)" }
    "INV_*" { puts "Type: Inverter ($cell_ref)" }
    "DFF_*" { puts "Type: Sequential Flip-Flop ($cell_ref)" }
    default { puts "Type: Combinational Gate ($cell_ref)" }
}

# Regular Expression pattern with captures:
set pin_name "clk_core_buf_i"
switch -regexp -- $pin_name {
    {^clk_.*_i$} { puts "Input Clock Pin: $pin_name" }
    {^rst_.*_n$} { puts "Active-low Reset Pin: $pin_name" }
    default      { puts "Standard Data Pin" }
}
§30

Switch Fall-Through with Hyphen (-)

Core Tcl 8.6

By default, Tcl switch bodies do not fall through to subsequent cases. To share a single action body across multiple pattern arms, specify a single hyphen - as the branch script.

Tcl
set file_ext ".vh"

switch -exact -- $file_ext {
    ".v"  -
    ".sv" -
    ".vh" {
        set lang "verilog"
        puts "Parsed as Verilog/SystemVerilog source."
    }
    ".vhd" -
    ".vhdl" {
        set lang "vhdl"
        puts "Parsed as VHDL source."
    }
    default {
        error "Unsupported source file format: $file_ext"
    }
}
§31

For Loops: Initialization, Test, Next

Core Tcl 8.6

The for loop accepts four arguments: initialization script, test expression, next-step script, and body. Always brace both the test expression and next step to ensure JIT bytecode optimization and correct variable evaluation on each iteration.

Tcl
# Standard counting loop with incr
for {set i 0} {$i < 8} {incr i} {
    puts "Configuring pipeline stage $i (reg_stage_${i}_q)"
}

# Stepping through memory bus width in powers of 2
for {set width 8} {$width <= 128} {set width [expr {$width * 2}]} {
    puts "Supported AXI bus width: $width bits"
}
§32

While Loops & The Condition Evaluation Trap

Trap & Gotcha

A while loop repeatedly executes its body as long as the condition expression evaluates to non-zero. You MUST brace the condition! If the condition is unbraced, Tcl substitutes the variable value once before the loop begins, creating a catastrophic infinite loop.

Tcl
# ❌ FATAL TRAP: Unbraced condition evaluates ONCE to "1" => INFINITE LOOP!
# set count 3
# while $count > 0 { puts $count; incr count -1 }

# ✅ CORRECT: Bracing allows re-evaluation of $count on every iteration
set count 3
while {$count > 0} {
    puts "Hold-time fix iteration: $count"
    incr count -1
}

# Polling simulation status with timeout
set timeout_cycles 100
set sim_running 1
while {$sim_running && $timeout_cycles > 0} {
    incr timeout_cycles -1
    # Check simulation status...
    if {$timeout_cycles == 95} { set sim_running 0 }
}
§33

Foreach: Single Variable Iteration

Core Tcl 8.6

foreach is the standard idiom for iterating through elements of a Tcl list. It is fast, clean, handles whitespace properly, and avoids index arithmetic.

Tcl
set clock_domains [list "clk_sys" "clk_ddr" "clk_pcie" "clk_dsp"]

foreach clk $clock_domains {
    puts "Constraining clock domain: $clk"
    # SDC constraint example:
    # create_clock -name $clk -period 2.5 [get_ports $clk]
}
§34

Foreach: Multi-Variable & Parallel List Iteration (Zip)

Core Tcl 8.6

Tcl foreach has built-in superpowers: it can take multiple variables from a single list (e.g. key-value pairs) OR iterate across multiple independent lists in parallel (similar to Python zip()).

Tcl
# 1. Pairwise iteration over key-value flat list:
set pad_assignments [list "PAD_A0" 3.3 "PAD_A1" 3.3 "PAD_HS" 1.8]
foreach {pad voltage} $pad_assignments {
    puts "Pin $pad set to IO voltage ${voltage}V"
}

# 2. Parallel iteration across two separate lists (Parallel Zip):
set clocks  [list "clk_core" "clk_mem" "clk_axi"]
set periods [list 1.0        2.5       4.0]

foreach clk $clocks period $periods {
    puts "Domain [format %-10s $clk] -> Period: ${period}ns"
}
⚠️ Production Trap & Gotcha
If the lists passed to parallel foreach have uneven lengths, the shorter lists supply empty string values "" for the remaining iterations. Always verify list lengths with llength if strict synchronization is required.
§35

Lmap: Functional List Mapping & Filtering (Tcl 8.6+)

Core Tcl 8.6

Introduced in Tcl 8.6, lmap operates like foreach but collects the return value of each iteration into a new list (list comprehension / map). Calling continue skips collecting that item, turning lmap into an elegant filter.

Tcl
# Transform (Map): Convert list of MHz frequencies to period in ns
set freqs_mhz [list 100 200 400 800]
set periods_ns [lmap f $freqs_mhz {
    expr {1000.0 / $f}
}]
puts "Periods: $periods_ns"  ;# {10.0 5.0 2.5 1.25}

# Filter: Retain only violating timing paths (slack < 0.0)
set slacks [list 0.12 -0.04 0.35 -0.15 0.00]
set violations [lmap s $slacks {
    if {$s >= 0.0} continue
    set s
}]
puts "Violating slacks: $violations"  ;# {-0.04 -0.15}
§36

Loop Control: break & continue

Core Tcl 8.6

break immediately terminates the innermost enclosing loop. continue skips the remainder of the current loop iteration and advances immediately to the next evaluation cycle.

Tcl
set net_list [list "net_clk" "net_vss" "net_data0" "net_vdd" "net_data1"]

foreach net $net_list {
    # Skip power rails using continue:
    if {$net eq "net_vdd" || $net eq "net_vss"} {
        continue
    }

    # Stop parsing if critical clock net inspected:
    if {$net eq "net_clk"} {
        puts "Found critical root clock: $net"
    }
}

5 Procedures, Arguments & Call Frames

§37

Defining Procedures: proc Syntax

Core Tcl 8.6

Procedures are defined with proc name argList body. If no explicit return statement is executed, the procedure automatically returns the value produced by the last executed command in the body.

Tcl
# Standard procedure definition:
proc calc_clock_period_ns {freq_mhz} {
    if {$freq_mhz <= 0} {
        error "Invalid frequency: $freq_mhz (must be > 0)"
    }
    return [expr {1000.0 / $freq_mhz}]
}

set p [calc_clock_period_ns 500]
puts "Period for 500 MHz: ${p}ns"
§38

Default Argument Values

Core Tcl 8.6

To specify default argument values, format the parameter in the argument list as a two-element sub-list: {argName defaultValue}. All parameters with default values must appear at the end of the argument list.

Tcl
proc create_sdc_clock {clk_port {period_ns 10.0} {waveform {0.0 5.0}}} {
    puts "SDC: create_clock -name $clk_port -period $period_ns -waveform {$waveform} \[get_ports $clk_port\]"
}

# Invocation using defaults:
create_sdc_clock "clk_slow"

# Overriding period:
create_sdc_clock "clk_fast" 2.5

# Overriding both period and duty cycle waveform:
create_sdc_clock "clk_asym" 4.0 {0.0 1.0}
§39

Variadic Procedures: args Special Parameter

Core Tcl 8.6

If the last formal argument in a procedure's parameter list is named args, Tcl bundles all remaining arguments passed by the caller into a single list assigned to $args. This powers variable-argument functions and command wrappers.

Tcl
proc report_critical_pins {net_name args} {
    puts "Net: $net_name (Pins connected: [llength $args])"
    foreach pin $args {
        puts "  -> $pin"
    }
}

report_critical_pins "net_reset" "u_cpu/rst_n" "u_mem/rst_n" "u_dma/rst_n"
§40

Return Values & Explicit Error Exits: return -code

Core Tcl 8.6

The return command supports advanced control codes: -code ok (0), -code error (1), -code return (2), -code break (3), or -code continue (4). Using return -code error -errorcode ... "msg" raises structured exceptions catchable by callers.

Tcl
proc validate_voltage {corner volt} {
    if {$volt < 0.70 || $volt > 1.35} {
        return -code error -errorcode [list EDA VOLTAGE_OUT_OF_SPEC $corner] \
            "Voltage ${volt}V at corner $corner exceeds operating range \[0.70V .. 1.35V\]"
    }
    return 1
}

# Catching the structured error:
if {[catch {validate_voltage "worst_case" 0.65} errMsg options]} {
    puts "Caught Error: $errMsg"
    puts "Error Code: [dict get $options -errorcode]"
}
§41

Tailcall Optimization in Tcl 8.6+

Core Tcl 8.6

tailcall cmd ?args...? replaces the current procedure's execution stack frame with the execution of the target command. This eliminates call stack frame buildup, enabling infinite recursive state machines and deep tree traversals with zero stack-overflow risk.

Tcl
# Tail-recursive accumulator for traversing deep hierarchical design nets
proc count_down_tail {n acc} {
    if {$n <= 0} { return $acc }
    # tailcall replaces current frame: constant memory O(1) stack depth!
    tailcall count_down_tail [expr {$n - 1}] [expr {$acc + $n}]
}

puts "Sum 1..100000 = [count_down_tail 100000 0]"
§42

Anonymous Procedures: apply & Lambdas

Core Tcl 8.6

apply {args body ?namespace?} ?values...? evaluates an anonymous lambda procedure without registering a permanent command name in the interpreter symbol table. Ideal for throwaway callbacks and custom comparator predicates.

Tcl
# Direct inline lambda execution:
set square [apply {{x} {expr {$x * $x}}} 12]
puts "12 squared = $square"  ;# 144

# Lambda stored in variable:
set to_kelvin {{celsius} {expr {$celsius + 273.15}}}
puts "125 C in Kelvin: [apply $to_kelvin 125.0] K"

# Used with custom lsort:
set cells [list "BUF_X4" "BUF_X1" "BUF_X16" "BUF_X2"]
# Custom sort by size extracted after 'X':
set sorted [lsort -command [list apply {{a b} {
    scan $a "BUF_X%d" sa
    scan $b "BUF_X%d" sb
    expr {$sa - $sb}
}}] $cells]
puts "Sorted buffers: $sorted"  ;# BUF_X1 BUF_X2 BUF_X4 BUF_X16
§43

Introspection: info procs, args, default, and body

Core Tcl 8.6

Tcl features dynamic runtime introspection. You can inspect currently registered procedures, discover their argument signatures, query default parameter values, and extract procedure bodies as raw strings for dynamic wrapping or logging.

Tcl
proc report_timing {clock_name {max_paths 10} {detail "summary"}} {
    puts "Reporting up to $max_paths for $clock_name"
}

# Dynamic procedure signature inspector:
proc inspect_proc {pname} {
    puts "Procedure: $pname"
    foreach arg [info args $pname] {
        if {[info default $pname $arg defVal]} {
            puts "  Param: $arg (default: '$defVal')"
        } else {
            puts "  Param: $arg (REQUIRED)"
        }
    }
}

inspect_proc report_timing
§44

Stack Introspection: info level & info frame

Core Tcl 8.6

info level returns the current procedure call stack depth or the exact command and arguments of any stack level. info frame provides source file names and precise line numbers, enabling custom debuggers and stack tracers.

Tcl
# EDA Log tracer printing caller file and line
proc log_checkpoint {msg} {
    # Frame -1 gives caller information
    set frame_info [info frame -1]
    set file [expr {[dict exists $frame_info file] ? [file tail [dict get $frame_info file]] : "interactive"}]
    set line [expr {[dict exists $frame_info line] ? [dict get $frame_info line] : "?"}]

    puts "\[$file:$line\] CHECKPOINT: $msg"
}

log_checkpoint "Beginning clock tree synthesis insertion"

6 Lists & List Operations

§45

Constructing Lists: list, [list $a $b], and String Conversion

Core Tcl 8.6

Always use the list command to construct lists containing variables with spaces or special characters. Creating lists via string concatenation (e.g. set l "$a $b") corrupts nested spacing and causes element boundary bugs.

Tcl
# ❌ FRAGILE: String concatenation with whitespace issues
set pin1 "u_cpu/clk in"
set pin2 "u_mem/clk"
set bad_list "$pin1 $pin2"
puts "Length bad_list: [llength $bad_list]"  ;# Prints 3! "in" became element 2

# ✅ ROBUST: list command preserves element boundaries
set good_list [list $pin1 $pin2]
puts "Length good_list: [llength $good_list]" ;# Prints 2!
puts "Element 0: [lindex $good_list 0]"         ;# "u_cpu/clk in"
§46

Indexing Elements: lindex, end, and end-1

Core Tcl 8.6

lindex list ?index...? extracts elements by zero-based index. Special index keywords include end (last element) and end-N (relative from end). Passing multiple index arguments extracts directly from nested lists.

Tcl
set stages [list "synth" "floorplan" "placement" "cts" "route" "sta"]

puts "First stage: [lindex $stages 0]"       ;# synth
puts "Last stage:  [lindex $stages end]"     ;# sta
puts "Pre-route:   [lindex $stages end-2]"   ;# cts

# Multi-dimensional indexing into nested list:
set matrix [list [list 1 2 3] [list 4 5 6] [list 7 8 9]]
puts "Row 1, Col 2: [lindex $matrix 1 2]"   ;# 6
§47

List Length: llength

Core Tcl 8.6

llength list counts the number of top-level elements in a list. In Tcl 8.5+, lists maintain internal object representations with cached lengths, making llength an instant O(1) operation.

Tcl
set cell_instances [list "u0" "u1" "u2" "u3" "u4"]
set total [llength $cell_instances]

if {$total == 0} {
    puts "WARNING: Module contains no sub-instances."
} else {
    puts "Audit: $total cell instances detected."
}
§48

Appending & Modifying: lappend, lset, linsert, lreplace

Core Tcl 8.6

lappend varName ?val...? appends items directly to a variable in-place (O(1) amortized). lset varName ?index...? newVal modifies an element in-place. linsert and lreplace return new modified copies.

Tcl
# Fast in-place appending (note: pass variable NAME, not $var)
set drc_errors [list]
lappend drc_errors "METAL1_MIN_WIDTH_01"
lappend drc_errors "VIA2_SPACING_04" "OVERLAP_CHECK_09"
puts "Errors: $drc_errors"

# In-place element modification:
set clocks [list "clk_100" "clk_200" "clk_400"]
lset clocks 1 "clk_250"
puts "Updated clocks: $clocks"  ;# clk_100 clk_250 clk_400

# Inserting at index 1 (returns new list):
set stages [list "synth" "route"]
set stages [linsert $stages 1 "floorplan" "placement"]

# Replacing elements from index 1 to 2:
set stages [lreplace $stages 1 2 "opt_placement"]
⚠️ Production Trap & Gotcha
Writing lappend $my_list $item is wrong! lappend expects the variable NAME lappend my_list $item. Adding $ attempts to append into a variable named after the contents of my_list.
§49

Slicing Lists: lrange

Core Tcl 8.6

lrange list first last extracts a contiguous sub-list from index first through last (inclusive). Out-of-bound indices are automatically clamped to valid bounds.

Tcl
set path_delays [list 0.12 0.45 0.78 1.02 1.34 1.89 2.10]

# Top 3 slowest paths:
set top3 [lrange $path_delays end-2 end]
puts "Top 3 slowest delays: $top3"

# All elements except first and last:
set middle [lrange $path_delays 1 end-1]
§50

Sorting Lists: lsort Options & Modes

Core Tcl 8.6

lsort ?options? list sorts list items. Key options: -ascii (default), -integer, -real, -increasing, -decreasing, -unique (deduplicates), and -index n (sorts multi-column sublists by column n).

Tcl
# 1. Numeric sorting (ascending)
set slacks [list 0.25 -0.05 -0.89 0.12 -0.01]
set worst_first [lsort -real -increasing $slacks]
puts "Worst slacks: $worst_first"  ;# -0.89 -0.05 -0.01 0.12 0.25

# 2. Deduplication using -unique
set visited_cells [list "u_cpu" "u_mem" "u_cpu" "u_dma" "u_mem"]
set unique_cells [lsort -unique $visited_cells]
puts "Unique cells: $unique_cells"  ;# u_cpu u_dma u_mem

# 3. Multi-column table sorting by column index 1 (Slack column):
set timing_table [list \
    [list "path_01" -0.150 "clk_core"] \
    [list "path_02"  0.050 "clk_core"] \
    [list "path_03" -0.420 "clk_mem"]]

set sorted_table [lsort -real -index 1 $timing_table]
foreach row $sorted_table {
    puts "Path [lindex $row 0]: [lindex $row 1] ns ([lindex $row 2])"
}
⚠️ Production Trap & Gotcha
Never use plain lsort on numbers! By default, lsort uses ASCII lexicographical ordering, meaning 100 sorts before 20! Always pass -integer or -real.
§51

Searching Lists: lsearch Modes

Core Tcl 8.6

lsearch ?options? list pattern searches a list and returns the index of the first match (or -1 if not found). Flags include -exact, -glob (default), -regexp, -inline (returns matching value instead of index), and -all (returns all matches).

Tcl
set netlist_pins [list "u_cpu/clk" "u_cpu/data_in\[0\]" "u_cpu/data_in\[1\]" "u_cpu/rst_n"]

# Check existence (-exact):
set idx [lsearch -exact $netlist_pins "u_cpu/rst_n"]
if {$idx != -1} {
    puts "Found reset pin at index $idx"
}

# Filter all matching bus pins (-all -inline):
set bus_pins [lsearch -all -inline -glob $netlist_pins "u_cpu/data_in*"]
puts "Data bus pins: $bus_pins"  ;# {u_cpu/data_in[0] u_cpu/data_in[1]}
§52

List Reversal: lreverse & Destructuring: lassign

Core Tcl 8.6

lreverse list reverses element order in O(n) time. lassign list varName ?varName...? unpacks elements into distinct variables in a single command, returning any unassigned surplus elements.

Tcl
# Unpacking coordinate tuple (x1, y1, x2, y2):
set bbox [list 10.0 25.0 110.0 225.0]
lassign $bbox x1 y1 x2 y2
puts "BBox: ($x1, $y1) to ($x2, $y2)"

# Capturing head and remainder:
set remaining_stages [lassign [list "synth" "floorplan" "cts" "route"] current_stage next_stage]
puts "Now: $current_stage, Next: $next_stage, Queued: $remaining_stages"

# Reversing pipeline stage execution:
set reversed_flow [lreverse [list "stage1" "stage2" "stage3"]]
puts "Rollback order: $reversed_flow"  ;# stage3 stage2 stage1
§53

Splitting & Joining: split and join & 2D Matrices

Core Tcl 8.6

split string ?splitChars? breaks a string into a list on delimiter boundaries (e.g. parsing CSV lines or hierarchy paths). join list ?joinString? collapses list items into a single delimited string.

Tcl
# 1. Parsing hierarchical EDA cell instance path
set full_path "top/u_core/u_alu/adder_32/cout"
set path_tokens [split $full_path "/"]
puts "Hierarchy depth: [llength $path_tokens]"
puts "Leaf instance:   [lindex $path_tokens end]"
puts "Parent module:   [join [lrange $path_tokens 0 end-1] "/"]"

# 2. Parsing CSV report line:
set csv_line "BUF_X4,120.4,350.2,MET"
lassign [split $csv_line ","] cell x y status
puts "Cell $cell at ($x, $y) -> $status"
⚠️ Production Trap & Gotcha
Consecutive split delimiters produce empty elements! E.g. split "a//b" "/" produces {a {} b}. Use [lsearch -all -inline -not [split $str "/"] ""] if empty tokens should be discarded.

7 Dictionaries (Tcl 8.5+)

§54

Dict Creation & Access: dict create, get, set, unset

Core Tcl 8.6

Unlike legacy arrays, Tcl dict objects are true first-class values. They can be stored in scalar variables, passed by value into procedures, nested arbitrarily, and returned directly. Accessing non-existent keys throws an error unless guarded.

Tcl
# Creation using dict create or literal key-value list:
set chip_cfg [dict create \
    design_name "crypto_core" \
    metal_layers 6 \
    target_clock_mhz 800.0]

# Reading a key:
set design [dict get $chip_cfg design_name]
puts "Design: $design"

# Updating/setting a key (in-place on variable):
dict set chip_cfg metal_layers 7
dict set chip_cfg process_node "16nm"

# Removing a key:
dict unset chip_cfg process_node

puts "Updated configuration dict: $chip_cfg"
§55

Checking Keys & Querying: dict exists, keys, values, size

Core Tcl 8.6

dict exists $dict key ?key...? checks for the presence of a key without raising an exception. dict keys and dict values extract keys and values matching optional glob patterns. dict size returns the number of key-value pairs.

Tcl
set timing_metrics [dict create wns -0.05 tns -1.25 num_viols 18]

# Safe access with dict exists:
if {[dict exists $timing_metrics wns]} {
    set wns [dict get $timing_metrics wns]
    puts "WNS: ${wns}ns"
}

puts "Metric count: [dict size $timing_metrics]"
puts "Available keys: [dict keys $timing_metrics]"
puts "Pattern keys:    [dict keys $timing_metrics *ns]"  ;# wns tns
§56

In-Place Mutation: dict incr & dict lappend

Core Tcl 8.6

dict incr varName key ?increment? increments an integer counter directly inside a dictionary variable without reading and re-setting. dict lappend varName key ?val...? appends items directly to a list stored inside a dictionary value.

Tcl
set drc_summary [dict create]

# Increment counters atomically (creates key if non-existent!):
dict incr drc_summary "MIN_SPACING"
dict incr drc_summary "MIN_SPACING"
dict incr drc_summary "MIN_AREA" 5

puts "DRC Counts: $drc_summary"  ;# MIN_SPACING 2 MIN_AREA 5

# Appending to lists inside dict:
set pin_map [dict create]
dict lappend pin_map "clk_core" "u0/clk" "u1/clk"
dict lappend pin_map "clk_core" "u2/clk"

puts "Clock fanout: [dict get $pin_map clk_core]"
§57

Iteration: dict for

Core Tcl 8.6

dict for {keyVar valVar} $dict body iterates efficiently over all key-value entries in insertion order. In Tcl, dictionaries preserve deterministic key insertion order (unlike arrays which are pseudo-random hashes).

Tcl
set clock_targets [dict create \
    clk_cpu   1200.0 \
    clk_pcie  500.0  \
    clk_uart  40.0]

dict for {clk freq} $clock_targets {
    set period_ns [expr {1000.0 / $freq}]
    puts "Clock [format %-10s $clk]: [format %7.1f $freq] MHz (T = [format %.3f $period_ns] ns)"
}
§58

Local Variable Binding: dict update & dict with

Core Tcl 8.6

dict update dictVar key localName body unpacks specific keys into local variables and writes modifications back to the dictionary upon body completion. dict with dictVar body automatically maps ALL dictionary keys to local variables.

Tcl
set cell_info [dict create ref "INV_X2" drive 2.0 area 0.450]

# dict update: Map 'area' to local 'a', modify in-place:
dict update cell_info area a {
    set a [expr {$a * 1.15}]  ;# 15% cell padding
}
puts "Updated cell info: $cell_info"

# dict with: Read/write all keys directly
dict with cell_info {
    puts "Current cell $ref has drive strength $drive and area $area"
    set drive [expr {$drive * 2}]
}
puts "Post-upsize: $cell_info"
§59

Nested Dictionaries: Multi-Corner STA Modeling

EDA & VLSI

Dictionaries support multi-level nested keys naturally. Passing multiple key arguments to dict get, dict set, or dict exists traverses down nested hierarchy levels without intermediate parsing.

Tcl
set corners [dict create]

# Deep nested assignment: dict set var k1 k2 k3 value
dict set corners "ss_125c" "setup" "wns" -0.120
dict set corners "ss_125c" "setup" "tns" -4.500
dict set corners "ff_m40c" "hold"  "wns" -0.015

# Querying deep nested values:
set ss_wns [dict get $corners "ss_125c" "setup" "wns"]
puts "Slow-Slow 125C Setup WNS: ${ss_wns}ns"

# Deep existence check:
if {[dict exists $corners "ss_125c" "hold" "wns"]} {
    puts "Found hold metrics for ss_125c"
} else {
    puts "Notice: ss_125c hold not analyzed."
}
§60

Filtering Dictionaries: dict filter (-key, -value, -script)

Core Tcl 8.6

dict filter $dict creates a new dictionary by filtering entries matching pattern criteria: -key pattern, -value pattern, or -script {kVar vVar} {expr} (custom boolean expression evaluated per entry).

Tcl
set slacks [dict create \
    path_01  0.25 \
    path_02 -0.15 \
    path_03 -0.42 \
    path_04  0.08]

# Filter only violating paths with -script:
set violations [dict filter $slacks script {path slack} {
    expr {$slack < 0.0}
}]
puts "Violations dict: $violations"  ;# path_02 -0.15 path_03 -0.42

# Filter keys by glob pattern:
set filtered_keys [dict filter $slacks key "*01" "*04"]
puts "Endpoints 01 & 04: $filtered_keys"
§61

Dict vs Array: Performance, Serialization & Passing

Core Tcl 8.6

Understanding when to use dict vs array is critical in production Tcl architecture. Modern EDA frameworks strongly prefer dict for state encapsulation and API returns.

Tcl
# Dicts can be returned directly from procs:
proc get_power_breakdown {} {
    return [dict create dynamic 45.2 leakage 12.8 total 58.0]
}

# Arrays REQUIRE upvar or global to return:
proc get_power_breakdown_arr {out_arr_name} {
    upvar 1 $out_arr_name arr
    set arr(dynamic) 45.2
    set arr(leakage) 12.8
}

# Dicts can be easily nested, converted to JSON, or serialized:
set p [get_power_breakdown]
puts "Total Power: [dict get $p total] mW"
⚠️ Production Trap & Gotcha
Arrays support trace add variable; dictionaries do NOT support traces on individual keys. If you need fine-grained write traces per key, use an array; for all data passing and API returns, use dictionaries.

8 Strings & Formatting

§62

Length & Slicing: string length & string range

Core Tcl 8.6

string length str returns the number of UTF-8/Unicode characters. string range str first last extracts a substring from index first through last (supports end and end-N).

Tcl
set cell_name "u_cpu_alu_add32_inst"

puts "Name length: [string length $cell_name]"
# Extract prefix (0..4):
puts "Prefix: [string range $cell_name 0 4]"          ;# u_cpu
# Extract suffix (strip 'u_'):
puts "Stripped: [string range $cell_name 2 end]"       ;# cpu_alu_add32_inst
# Last 4 characters:
puts "Tail: [string range $cell_name end-3 end]"       ;# inst
§63

Search & Matching: string first, last, and match

Core Tcl 8.6

string first needle haystack ?startIndex? finds the first occurrence index (-1 if absent). string last finds the final occurrence. string match ?-nocase? pattern str performs fast wildcard glob matching (*, ?, [abc]).

Tcl
set hier_path "top/core/dcache/ram_array\[128\]"

# Finding delimiters
set slash_pos [string first "/" $hier_path]       ;# 3
set last_slash [string last "/" $hier_path]      ;# 14

# Glob matching:
if {[string match -nocase "*dcache*" $hier_path]} {
    puts "Hierarchy belongs to data cache domain"
}
if {[string match "*\\\[*\\\]" $hier_path]} {
    puts "Path points to a bus/array index element"
}
§64

Equality & Comparison: string equal & string compare

Core Tcl 8.6

string equal ?-nocase? ?-length n? str1 str2 returns 1 if strings match exactly. string compare returns -1, 0, or 1 for lexicographical ordering.

Tcl
set netA "CLK_NET"
set netB "clk_net"

# Case-insensitive equality:
if {[string equal -nocase $netA $netB]} {
    puts "Nets match ignoring case sensitivity."
}

# Prefix check up to 4 characters:
if {[string equal -length 4 "DFF_X1" "DFF_X4"]} {
    puts "Both cells share 'DFF_' flip-flop prefix."
}
§65

Case Modification: string tolower, toupper, totitle

Core Tcl 8.6

Transforms character casing across strings: string tolower, string toupper, and string totitle (capitalizes first letter, lowercases remainder).

Tcl
set raw_corner "sS_125C_wOrSt"
set normalized [string tolower $raw_corner]
puts "Normalized: $normalized"  ;# ss_125c_worst

set macro_name [string toupper "sram_256x32"]
puts "Macro: $macro_name"      ;# SRAM_256X32
§66

Trimming Characters: string trim, trimleft, trimright

Core Tcl 8.6

string trim str ?chars? strips characters from both ends. Defaults to stripping whitespace (spaces, tabs, newlines, carriage returns). trimleft and trimright strip from their respective sides only.

Tcl
set dirty_line "   slack = -0.045 ns \r\n"
set clean_line [string trim $dirty_line]
puts "Clean: '$clean_line'"

# Stripping specific characters (e.g. leading slashes or braces)
set pin_expr "{u_cpu/clk_i}"
set raw_pin  [string trim $pin_expr "{}"]
puts "Raw pin: $raw_pin"  ;# u_cpu/clk_i
§67

Translation & Replacement: string map & string replace

Core Tcl 8.6

string map ?options? mappingDict str performs simultaneous multiple substring replacements in a single linear pass (no recursive replacement conflicts). string replace str first last ?newStr? replaces a slice by index.

Tcl
# Multi-token template interpolation with string map:
set tpl "create_clock -name %CLK% -period %PERIOD% \[get_ports %PORT%\]"
set rendered [string map [list \
    "%CLK%"    "clk_sys" \
    "%PERIOD%" "2.5"     \
    "%PORT%"   "core_clk_p"] $tpl]

puts "SDC: $rendered"
# SDC: create_clock -name clk_sys -period 2.5 [get_ports core_clk_p]

# Sanitizing Verilog escaped identifiers:
set v_net "\net_bus[0] "
set sanitized [string map [list "\\" "" " " ""] $v_net]
§68

Validation: string is (integer, double, boolean, etc.)

Core Tcl 8.6

string is class ?-strict? str tests whether a string conforms to a data type without throwing syntax errors. Supported classes include integer, wideinteger, double, boolean, alnum, alpha, ascii, digit, space, and print.

Tcl
proc set_clock_period {val} {
    # -strict ensures empty string "" is NOT counted as valid number
    if {![string is double -strict $val]} {
        error "Invalid clock period '$val': must be a floating point number!"
    }
    puts "Clock period accepted: ${val}ns"
}

set_clock_period "2.5"       ;# OK
# set_clock_period "two_ns"  ;# Throws error safely
⚠️ Production Trap & Gotcha
Without -strict, string is integer "" returns 1 (true)! Always pass -strict when validating user arguments or parsed file inputs.
§69

Formatted Output: format (printf-style Formatting)

Core Tcl 8.6

The format formatString ?arg...? command generates strings formatted with standard ANSI C sprintf specifiers. Essential for building clean, aligned ASCII summary tables in EDA reports.

Tcl
# Pretty-printed EDA timing report table:
puts [string repeat "=" 60]
puts [format "%-20s | %12s | %10s | %6s" "Clock Domain" "Freq (MHz)" "Slack (ns)" "Status"]
puts [string repeat "-" 60]

set rows [list \
    [list "clk_core"  1200.0  0.045 "MET"] \
    [list "clk_ddr"    800.0 -0.120 "VIOL"] \
    [list "clk_pcie"   250.0  0.890 "MET"]]

foreach row $rows {
    lassign $row clk freq slack status
    puts [format "%-20s | %12.2f | %10.3f | %6s" $clk $freq $slack $status]
}
puts [string repeat "=" 60]
§70

Parsing Strings: scan (sscanf Extraction)

Core Tcl 8.6

scan string format ?varName...? parses data from strings according to C sscanf conversion specifiers. Returns the count of successfully matched conversions.

Tcl
# Extracting physical DEF coordinates: ( 12500 34200 )
set def_token "( 12500 34200 )"
if {[scan $def_token "( %d %d )" x y] == 2} {
    puts "Parsed Cell Placement: X=$x, Y=$y"
}

# Parsing library cell name and drive strength:
set cell_type "AND2_X4_HVT"
if {[scan $cell_type "%\[^_\]_X%d_%s" gate drive vt] == 3} {
    puts "Gate: $gate, Drive: X$drive, VT: $vt"
}

9 Regular Expressions & Pattern Matching

§71

Regexp Syntax & Capturing Groups

Core Tcl 8.6

regexp ?switches? exp string ?matchVar? ?subMatchVar...? matches a regular expression against a string. If match variables are supplied, matchVar receives the full match, and subsequent variables receive parenthesized capture groups.

Tcl
set log_line "VIOLATION: Setup slack on endpoint 'u_core/reg_q[3]' is -0.145 ns"

# Extract endpoint and slack value using regex captures:
set pattern {slack on endpoint '([^']+)' is ([-+]?[0-9]*\.?[0-9]+)}

if {[regexp $pattern $log_line full_match endpoint slack_val]} {
    puts "Captured Endpoint: $endpoint"
    puts "Captured Slack:    $slack_val ns"
}
⚠️ Production Trap & Gotcha
Always enclose the regex pattern in curly braces {...}! If you use double quotes, Tcl evaluates backslashes and square brackets before the regexp engine sees them, corrupting character classes like \d and bus indices like \[0\].
§72

Regexp Flags: -nocase, -line, -indices

Core Tcl 8.6

Key regexp switches: -nocase (case insensitive), -line (enables multiline anchoring where ^/$ match line breaks), -indices (stores start and end character offsets instead of matched substrings), and -- (ends switch processing).

Tcl
set report "Error: Setup timing failed.\nWARNING: Max slew violation."

# Multiline match with -line and -nocase
if {[regexp -line -nocase {^error:\s*(.*)$} $report -> err_msg]} {
    puts "Found multiline error: $err_msg"
}

# Finding character indices offset:
if {[regexp -indices {timing failed} $report idx]} {
    puts "Match found from character [lindex $idx 0] to [lindex $idx 1]"
}
§73

Global Extraction: regexp -all -inline

Core Tcl 8.6

Passing -all -inline turns regexp into a global extractor, returning all matches and capture groups across the entire string as a flat list. This is the fastest way to tokenize log files or extract netlist pins.

Tcl
set verilog_line "wire [31:0] data_bus, addr_bus, ctrl_bus;"

# Extract all bus identifier names:
set bus_names [regexp -all -inline {\w+_bus} $verilog_line]
puts "Extracted buses: $bus_names"  ;# data_bus addr_bus ctrl_bus

# Extracting key=value pairs:
set text "WNS=-0.045 TNS=-1.200 VIOLS=15"
set pairs [regexp -all -inline {(\w+)=([^\s]+)} $text]
foreach {full key val} $pairs {
    puts "  Key: $key => Value: $val"
}
§74

Anchors & Word Boundaries: ^, $, \m, \M, \y

Core Tcl 8.6

Tcl regex engines feature fine-grained boundary anchors: ^ (start of string/line), $ (end of string/line), \m (start of word), \M (end of word), and \y (any word boundary).

Tcl
set net_name "clk_core"

# Exact whole-string anchor:
if {[regexp {^clk_[a-z0-9_]+$} $net_name]} {
    puts "Valid clock naming format: $net_name"
}

# Word boundary matching:
set line "set clk [get_ports clk_i]"
# Match isolated word 'clk' without matching 'clk_i':
regexp -all -inline {\mclk\M} $line
§75

Greedy vs Non-Greedy Quantifiers (* vs *?, + vs +?)

Core Tcl 8.6

By default, quantifiers (*, +, {n,m}) are greedy—they consume as many characters as possible. Adding a ? (e.g. *?, +?) makes them non-greedy (lazy), stopping at the earliest possible match.

Tcl
set code "/* comment 1 */ data = 1; /* comment 2 */"

# ❌ GREEDY: Consumes from first /* to last */
regexp {/\*.*\*/} $code bad_match
puts "Greedy match: $bad_match"

# ✅ NON-GREEDY: Stops at first closing */
regexp -all -inline {/\*.*?\*/} $code
§76

String Substitution: regsub & In-Place -all

Core Tcl 8.6

regsub ?switches? exp string subSpec ?varName? matches a regex against string and replaces matches with subSpec. Passing -all replaces every occurrence across the string.

Tcl
# Replacing all buffer cells in a netlist line:
set netlist_line "BUF_X1 u1 (.A(n1), .Y(n2)); BUF_X1 u2 (.A(n2), .Y(n3));"
set updated_line [regsub -all {BUF_X1} $netlist_line "BUF_X4"]
puts "Upsized netlist: $updated_line"

# Stripping out Verilog bus subscripts [31:0] -> _31_0_
set pin "data_out\[31\]"
set safe_name [regsub -all {\[([0-9]+)\]} $pin {_\1}]
puts "Safe symbol name: $safe_name"  ;# data_out_31
§77

Backreferences in regsub: \0, \1, \2

Core Tcl 8.6

Inside the replacement string subSpec of regsub, \0 references the entire matched text, \1 references the first parenthesized group, \2 references the second, and so on.

Tcl
# Reformatting timing path slack report line:
set line "Endpoint: reg_stage_q Slack: -0.150 ns"
set reformatted [regsub {Endpoint:\s*(\S+)\s*Slack:\s*([-\d.]+)\s*ns} $line \
    {PATH_METRIC|cell=\1|slack=\2ns}]

puts "$reformatted"
# PATH_METRIC|cell=reg_stage_q|slack=-0.150ns
§78

Character Classes & POSIX Shorthands (\d, \s, \w)

Core Tcl 8.6

Tcl supports standard regex shorthand characters: \d (digit), \D (non-digit), \s (whitespace), \S (non-whitespace), \w (word char [a-zA-Z0-9_]), \W (non-word), and POSIX bracket classes [[:xdigit:]], [[:space:]].

Tcl
# Validating hexadecimal address in memory map (e.g. 0x00FF8000)
set addr "0x00FF8000"
if {[regexp {^0x[[:xdigit:]]{8}$} $addr]} {
    puts "Valid 32-bit hex address format: $addr"
}

# Extracting all words separated by arbitrary whitespace:
set input "clk_core    clk_mem \t clk_axi\nclk_uart"
set words [regexp -all -inline {\S+} $input]
puts "Words: $words"
§79

Production EDA Log Parser: Timing Violations Extractor

Production Template

A complete, reusable procedure to parse Synopsys PrimeTime or Cadence Tempus timing reports, capturing Startpoint, Endpoint, Path Group, and Slack into structured dictionaries.

Tcl
proc parse_timing_report_chunk {report_text} {
    set violations [list]
    # Split text into timing path blocks separated by 'Startpoint:'
    set paths [regexp -all -inline -line {(?n)^  Startpoint: .*?(?=^  Startpoint:|\Z)} $report_text]

    foreach path_block $paths {
        set path_data [dict create]
        if {[regexp -line {^\s*Startpoint:\s*(\S+)} $path_block -> start]} {
            dict set path_data startpoint $start
        }
        if {[regexp -line {^\s*Endpoint:\s*(\S+)} $path_block -> end]} {
            dict set path_data endpoint $end
        }
        if {[regexp -line {^\s*Path Group:\s*(\S+)} $path_block -> grp]} {
            dict set path_data path_group $grp
        }
        if {[regexp -line {^\s*slack\s*\((VIOLATED|MET)\)\s*([-\d.]+)} $path_block -> status slack]} {
            dict set path_data status $status
            dict set path_data slack [expr {double($slack)}]
            lappend violations $path_data
        }
    }
    return $violations
}

10 File I/O, File System & Channels

§80

Opening Channels: open (Modes r, w, a, r+, w+)

Core Tcl 8.6

open fileName ?access? ?permissions? opens a file or pipeline and returns an opaque channel identifier (e.g. rc0, file4). Standard modes: r (read, default), w (truncate/write), a (append), r+ (read/write existing), w+ (truncate/read/write).

Tcl
# Open file for writing reports
set fh [open "run_sta.log" w]
puts $fh "Timestamp: [clock format [clock seconds] -format {%Y-%m-%d %H:%M:%S}]"
puts $fh "Tool: OpenROAD Timing Engine"
close $fh

# Open file for appending execution records
set log_fh [open "regression.summary" a]
puts $log_fh "RUN_042: PASSED (Setup Slack: +0.025ns)"
close $log_fh
⚠️ Production Trap & Gotcha
Always close channels with close $fh inside a try ... finally block! Failing to close channels leaks OS file descriptors and causes data corruption due to unwritten OS write buffers.
§81

Reading Files: read vs gets Line-by-Line Iteration

Core Tcl 8.6

read $fh ?numChars? slurps the entire file (or N characters) into memory at once. gets $fh ?varName? reads a single line, stripping the newline character. Line-by-line reading with gets maintains constant low memory when processing gigabyte-scale netlists or GDS/DEF stream files.

Tcl
# Approach 1: Slurp entire file into a single string (small SDC / config files)
set fh [open "constraints.sdc" r]
set sdc_content [read $fh]
close $fh

# Approach 2: Memory-efficient line-by-line streaming (multi-GB synthesis logs/DEF)
set fh [open "innovus.log" r]
set viol_count 0
while {[gets $fh line] >= 0} {
    if {[string match "*VIOLATION*" $line]} {
        incr viol_count
    }
}
close $fh
puts "Total violations detected: $viol_count"
§82

Writing Files: puts & Channel Flushing (flush)

Core Tcl 8.6

puts ?-nonewline? ?channelId? string writes data to an open channel or stdout. flush channelId forces all queued user-space buffer data out to the operating system immediately.

Tcl
# Writing without newline for progress meters:
puts -nonewline stdout "Running placement optimization... "
flush stdout  ;# Force output to console immediately before heavy task

# Heavy computation loop...
after 1000
puts stdout "DONE [format %.2f 1.25]s"

# Writing formatted ECO commands to script file:
set eco_fh [open "fix_hold.eco.tcl" w]
puts $eco_fh "# Automated ECO Hold buffer insertion"
puts $eco_fh "insert_buffer -net {u_cpu/data_reg_q} -cell {CLKBUF_X2} -new_net {net_eco_001}"
flush $eco_fh
close $eco_fh
§83

Channel Configuration: fconfigure

Core Tcl 8.6

fconfigure channelId ?name? ?value...? controls I/O channel behavior. Key properties: -buffering (full, line, none), -translation (auto, binary, crlf, lf), -encoding (utf-8, iso8859-1, binary), and -blocking (0 or 1).

Tcl
# 1. Binary mode for parsing OASIS/GDSII layout stream files:
set gds_fh [open "chip_layout.gds" r]
fconfigure $gds_fh -translation binary -encoding binary
set gds_header [read $gds_fh 4]
close $gds_fh

# 2. Line-buffered real-time log stream:
set log_fh [open "live_synthesis.log" w]
fconfigure $log_fh -buffering line -encoding utf-8 -translation lf
puts $log_fh "INIT: Synthesis engine spawned"  ;# Flushed immediately
§84

Standard Channels: stdin, stdout, stderr

Core Tcl 8.6

Tcl pre-defines three special channel handles: stdin (standard input), stdout (standard output), and stderr (standard error). You can write diagnostic errors directly to stderr without terminating execution.

Tcl
# Writing diagnostic warning to standard error:
puts stderr "ERROR-TIM-01: Critical setup violation detected in clk_fast domain!"

# Reading interactive input from terminal:
puts -nonewline stdout "Proceed with ECO buffer insertion? \[y/N\]: "
flush stdout
gets stdin user_response

if {[string tolower $user_response] eq "y"} {
    puts stdout "Applying ECO..."
} else {
    puts stdout "Aborted by user."
}
§85

Path Manipulation: file join, split, dirname, tail

Core Tcl 8.6

Never concatenate filesystem paths using manual slashes or plus signs. Always use file join and file split to guarantee cross-platform portability between Linux server farms and Windows developer workstations.

Tcl
set root_dir "/opt/designs"
set sub_dir  "soc_top/runs/run_01"
set filename "final_timing.rpt"

# 1. Robust path joining:
set full_path [file join $root_dir $sub_dir $filename]
puts "Full Path: $full_path"

# 2. Path component deconstruction:
puts "Dirname:   [file dirname $full_path]"    ;# /opt/designs/soc_top/runs/run_01
puts "Tail:      [file tail $full_path]"       ;# final_timing.rpt
puts "Extension: [file extension $full_path]"  ;# .rpt
puts "Rootname:  [file rootname $full_path]"   ;# .../final_timing
§86

Canonical Paths: file normalize & pwd

Core Tcl 8.6

file normalize path resolves all relative dots (., ..), symbolic links, and duplicate slashes, returning an absolute, canonical path string. pwd returns the current working directory.

Tcl
# Current directory
set cwd [pwd]
puts "Working directory: $cwd"

# Resolving messy relative paths:
set rel_path "../../tech/sky130/libs/../lef/sky130_fd_sc_hd.lef"
set abs_clean [file normalize $rel_path]
puts "Normalized: $abs_clean"

# Native OS formatting (e.g. backslashes on Windows for external tools)
puts "OS Native: [file nativename $abs_clean]"
§87

Filesystem Metadata: file exists, size, mtime

Core Tcl 8.6

Tcl provides built-in filesystem inspection queries: file exists, file isfile, file isdirectory, file size (bytes), file mtime (POSIX modification epoch), file readable, and file writable.

Tcl
set netlist "syn/outputs/soc_top.v"

if {[file exists $netlist]} {
    set bytes [file size $netlist]
    set mtime [file mtime $netlist]
    set date_str [clock format $mtime -format {%Y-%m-%d %H:%M:%S}]
    puts "Netlist: $netlist ($bytes bytes, modified $date_str)"
} else {
    error "Target netlist $netlist does not exist!"
}
§88

File Operations: file copy, rename, delete, mkdir

Core Tcl 8.6

Manage files and directories without spawning external shell commands: file mkdir ?dir...? (creates intermediate parents automatically), file copy ?-force? src dst, file rename ?-force? src dst, and file delete ?-force? ?path...?.

Tcl
set out_dir "build/stage_sta"

# Ensure output directory exists (recursive mkdir -p):
file mkdir $out_dir

# Archive previous timing reports:
if {[file exists "$out_dir/timing.rpt"]} {
    file copy -force "$out_dir/timing.rpt" "$out_dir/timing.rpt.bak"
}

# Clean temporary files safely:
file delete -force "temp_eco.tcl"
§89

Globbing & Recursive Directory Walker

Production Template

glob ?-nocomplain? ?-directory dir? ?pattern...? finds files matching wildcard patterns. Combine with recursion to traverse entire directory trees looking for HDL sources or reports.

Tcl
# Recursive directory tree walker finding all Verilog & SystemVerilog files:
proc find_hdl_files {root_dir} {
    set results [list]
    # Find matching files in current dir (-nocomplain suppresses error if empty):
    foreach f [glob -nocomplain -directory $root_dir -types f *.v *.sv] {
        lappend results $f
    }
    # Recursively traverse child sub-directories:
    foreach d [glob -nocomplain -directory $root_dir -types d *] {
        set child_name [file tail $d]
        if {$child_name eq "." || $child_name eq ".." || $child_name eq ".git"} continue
        set results [concat $results [find_hdl_files $d]]
    }
    return $results
}

set verilog_files [find_hdl_files "rtl"]
puts "Discovered [llength $verilog_files] HDL sources."

11 Subprocesses, Errors & Namespaces

§90

Executing External Tools: exec Pipelines

Core Tcl 8.6

exec ?switches? arg ?arg...? executes external OS binaries or command pipelines (e.g. invoking Yosys, Verilator, or Git). Tcl connects commands with pipes (|) just like a Unix shell and captures output.

Tcl
# Running git to query current commit hash:
set git_rev [string trim [exec git rev-parse --short HEAD]]
puts "Design git commit: $git_rev"

# Pipeline execution: count total cell instances in Verilog netlist:
set cell_count [exec grep -c "^  \\w\\+ u_" "synth/output.v" | tr -d " "]
puts "Synthesized cell instances: $cell_count"
§91

Standard Error Redirection in exec: 2>@1, >&

Core Tcl 8.6

By default, if an external program writes anything to standard error (even innocent banner warnings), exec treats it as a failure and throws an exception. Redirect stderr to stdout using 2>@1 or >& to merge streams cleanly.

Tcl
# Redirect standard error into standard output stream:
set tool_output [exec yosys -V 2>@1]
puts "Yosys Version: $tool_output"

# Redirect combined stdout & stderr directly into a disk log file:
exec openlane --design soc_top >& "openlane_run.log"
§92

The Non-Zero Exit Code Trap in exec

Trap & Gotcha

When a spawned child process exits with a non-zero exit code (e.g. exit 1 or grep finding no matches), exec immediately raises a fatal Tcl error. Always wrap exec with catch or try and inspect $::errorCode for the real exit status.

Tcl
# Safe subprocess invocation pattern:
set status [catch {exec grep "NO_SUCH_STRING" "design.v"} output]

if {$status != 0} {
    # Check if failure was just a non-zero exit code:
    if {[lindex $::errorCode 0] eq "CHILDSTATUS"} {
        set exit_code [lindex $::errorCode 2]
        puts "Process exited with code $exit_code (grep found no match)."
    } else {
        puts "System invocation error: $output"
    }
} else {
    puts "Found match: $output"
}
§93

Background Subprocesses: exec & and Process IDs

Core Tcl 8.6

Appending an ampersand & to the end of an exec command line spawns the external process in the background without blocking Tcl execution, returning a list of operating system Process IDs (PIDs).

Tcl
# Launch simulation in background
set pids [exec verilator --trace-fst -cc dut.v &]
puts "Spawned background simulation process: PID [lindex $pids 0]"

# Current Tcl interpreter PID:
puts "Orchestrator PID: [pid]"
§94

Structured Error Handling: try ... finally (Tcl 8.6+)

Core Tcl 8.6

Tcl 8.6 introduced modern structured exception handling with try body ?handler...? ?finally script?. The finally block is guaranteed to execute regardless of normal exit, error exceptions, return, or break.

Tcl
set fh [open "sta_run.log" w]

try {
    puts $fh "Running critical static timing analysis..."
    # Perform operations that might raise errors:
    if {[expr {rand() < 0.1}]} {
        throw {EDA TIMING TIMEOUT} "Timing engine exceeded max runtime"
    }
    puts "STA completed cleanly."
} trap {EDA TIMING} {msg opt} {
    puts stderr "Caught EDA timing error: $msg"
} on error {msg opt} {
    puts stderr "Unexpected general failure: $msg"
} finally {
    # Guaranteed cleanup of channel descriptor:
    close $fh
    puts "Log channel closed safely."
}
§95

Legacy Error Handling: catch Command

Core Tcl 8.6

catch script ?resultVar? ?optionsVar? intercepts any abnormal completion code (1=error, 2=return, 3=break, 4=continue) and returns 0 on success or non-zero on exception. optionsVar receives error metadata.

Tcl
if {[catch {
    set file_size [file size "non_existent.def"]
} errMsg errOpts]} {
    puts "Handled error gracefully: $errMsg"
    puts "Stack trace:\n[dict get $errOpts -errorinfo]"
}
§96

Raising Exceptions: error and throw

Core Tcl 8.6

error message ?errorInfo? ?errorCode? throws an error string. In Tcl 8.6+, throw type message provides clean, structured exceptions where type is a standardized hierarchy list.

Tcl
proc enforce_timing_closure {wns} {
    if {$wns < 0.0} {
        # Structured exception with taxonomy list
        throw [list EDA STA VIOLATION WNS] \
            "Design failed timing closure: worst negative slack is ${wns}ns"
    }
}

# Legacy syntax:
# error "Fatal setup timing failure: ${wns}ns" "" [list EDA STA VIOLATION]
§97

Namespaces: namespace eval, export, import

Core Tcl 8.6

Namespaces provide distinct symbol scopes for commands and variables, preventing collisions in large multi-engineer EDA script codebases. Procs can be exported with namespace export and imported with namespace import.

Tcl
namespace eval ::sta_toolkit {
    namespace export audit_setup audit_hold

    variable target_library "sky130_fd_sc_hd"

    proc audit_setup {corner} {
        variable target_library
        puts "Auditing setup timing against $target_library at corner $corner"
    }

    proc audit_hold {corner} {
        puts "Auditing hold timing at corner $corner"
    }
}

# Qualified invocation:
::sta_toolkit::audit_setup "ss_125c"

# Selective command importing:
namespace import ::sta_toolkit::audit_setup
audit_setup "tt_25c"
§98

Packages & Modular Libraries: package provide & require

Core Tcl 8.6

Tcl organizes shared libraries using the package management system. Files register their identity with package provide name version, and consuming scripts declare dependencies using package require name ?version?.

Tcl
# Inside library file: eda_utils.tcl
package provide eda_utils 1.4

namespace eval ::eda_utils {
    proc banner {title} {
        puts [string repeat "=" 50]
        puts "  $title"
        puts [string repeat "=" 50]
    }
}

# Inside top-level flow runner:
# package require Tcl 8.6
# package require eda_utils 1.4
# ::eda_utils::banner "STARTING PHYSICAL SYNTHESIS"

12 EDA & VLSI Semiconductor Automation

§99

The EDA Tcl Paradigm: Why EDA Tools Embed Tcl

EDA & VLSI

Every Tier-1 electronic design automation (EDA) software suite—including Synopsys (Design Compiler, PrimeTime, ICC2), Cadence (Genus, Innovus, Tempus), AMD/Xilinx (Vivado), and open-source flows (OpenROAD, Yosys)—embeds a native C/C++ Tcl interpreter. Tcl serves as the universal interactive shell, command-line interface, constraints format (SDC), and flow automation engine across the global semiconductor industry.

Tcl (EDA Shell)
# Universal EDA Shell Session Header Pattern:
puts "INFO: Starting EDA Semiconductor Scripting Flow..."
puts "Host Machine: $::tcl_platform(os) ($::tcl_platform(machine))"

# Tool-specific environment variable inspection:
if {[info exists ::synopsys_program_name]} {
    puts "Environment: Synopsys $::synopsys_program_name ($::sh_product_version)"
} elseif {[info exists ::env(CDS_LOAD_ENV)] || [info exists ::innovus_version]} {
    puts "Environment: Cadence Digital Implementation Suite"
} elseif {[info exists ::env(VIVADO_VERSION)]} {
    puts "Environment: AMD / Xilinx Vivado Design Suite"
} elseif {[info exists ::openroad_version]} {
    puts "Environment: OpenROAD Silicon Compiler"
}
§100

EDA Collections vs Tcl Lists: Pointers & sizeof_collection

EDA & VLSI

In modern chips containing tens of millions of gates, representing netlist objects as native Tcl string lists would consume tens of gigabytes of RAM. EDA tools invent Collections: lightweight C++ pointers/handles to internal C++ netlist database objects. Standard Tcl list commands like llength and lindex do not work on collections—you must use sizeof_collection and collection iterators.

Tcl (EDA)
# Query cells from database (returns an opaque collection handle):
set all_invs [get_cells -hierarchical -filter "ref_name =~ INV_*"]

# ❌ WRONG: llength returns 1 because the collection handle is a single token "_sel42"!
puts "Bogus list length: [llength $all_invs]"

# ✅ CORRECT: sizeof_collection queries internal C++ netlist database:
set inv_count [sizeof_collection $all_invs]
puts "Verified Inverter Count: $inv_count"

# Converting collection to native Tcl string list of names (when needed):
set inv_names [get_object_name $all_invs]
puts "First 3 names: [lrange $inv_names 0 2]"
⚠️ Production Trap & Gotcha
Converting a 5-million cell collection into a Tcl string list via get_object_name can exhaust interpreter heap memory. Keep objects in collections whenever possible!
§101

Iterating Collections: foreach_in_collection

EDA & VLSI

foreach_in_collection varName $collection body is the mandatory idiom for iterating over database collection elements. On each cycle, varName is bound to a single-element collection handle pointing to the underlying C++ database record.

Tcl (EDA)
# Iterate over all sequential flip-flop cells in the design:
set dff_cells [get_cells -hierarchical -filter "is_sequential == true"]

puts "Auditing [sizeof_collection $dff_cells] sequential elements..."
set async_rst_count 0

foreach_in_collection cell $dff_cells {
    set c_name [get_object_name $cell]
    # Check if cell has asynchronous clear pin:
    set rst_pin [get_pins -quiet -of_objects $cell -filter "name == CDN || name == RN"]
    if {[sizeof_collection $rst_pin] > 0} {
        incr async_rst_count
    }
}

puts "Flip-flops with async resets: $async_rst_count"
§102

Filtering & Sets: filter_collection, add, remove

EDA & VLSI

EDA provides high-performance C++ set operations on collections without writing nested loops: filter_collection filters by boolean attribute expressions, add_to_collection computes unions, and remove_from_collection computes set differences.

Tcl (EDA)
set all_bufs [get_cells -hierarchical -filter "is_buffer == true"]

# Filter out low-drive buffers (drive strength X1):
set high_drive_bufs [filter_collection $all_bufs "ref_name !~ *X1*"]

# Set Union: Combine clock buffers and high-drive buffers
set clk_bufs [get_cells -hierarchical -filter "ref_name =~ CLKBUF_*"]
set target_pool [add_to_collection $high_drive_bufs $clk_bufs]

# Set Difference: Exclude "dont_touch" cells
set dont_touch_cells [get_cells -hierarchical -filter "dont_touch == true"]
set eligible_cells [remove_from_collection $target_pool $dont_touch_cells]

puts "Optimization candidates: [sizeof_collection $eligible_cells]"
§103

Core Object Accessors: get_cells, nets, pins, ports

EDA & VLSI

The five cornerstone accessors in semiconductor database query scripts: get_cells (instances/gates), get_nets (interconnect wires), get_pins (hierarchical/leaf cell terminals), get_ports (chip/macro top boundary IO pads), and get_clocks (SDC timing clock domains).

Tcl (EDA)
# 1. Top-level boundary ports:
set reset_ports [get_ports -filter "direction == in && name =~ *rst*"]

# 2. Cell instance pins connected to clock:
set clk_pins [get_pins -hierarchical -filter "name == CLK || name == CK"]

# 3. Nets with high capacitance load:
set heavy_nets [get_nets -hierarchical -filter "capacitance > 0.050"]

# 4. SDC Clocks defined on design:
set all_sdc_clocks [get_clocks]
puts "Active Clocks: [get_object_name $all_sdc_clocks]"
⚠️ Production Trap & Gotcha
get_ports queries chip boundaries; get_pins queries cell instances. Querying get_pins [get_ports clk] fails because top IO ports are not cell pins!
§104

Connectivity Tracing: all_fanin & all_fanout

EDA & VLSI

all_fanin and all_fanout perform graph traversal across combinational logic cones. Flags include -to/-from (start/endpoint), -flat, -startpoints_only (finds driving flip-flops/ports), and -endpoints_only.

Tcl (EDA)
# Identify all registers driving into ALU critical comparator pin:
set target_pin [get_pins "u_core/u_alu/cmp_gt_i1/B"]

set driving_regs [all_fanin -to $target_pin -startpoints_only -flat]
puts "Driving Startpoints: [sizeof_collection $driving_regs]"

# Trace all load sinks driven by master interrupt line:
set irq_net [get_nets "master_irq_n"]
set irq_sinks [all_fanout -from $irq_net -endpoints_only -flat]
puts "Interrupt fanout sinks: [sizeof_collection $irq_sinks]"
§105

Object Introspection: get_attribute & list_attributes

EDA & VLSI

get_attribute $object attrName extracts exact physical, electrical, and timing properties from netlist objects (e.g. area, slack, capacitance, is_hierarchical, origin, bbox). list_attributes -class ... lists all supported attributes.

Tcl (EDA)
set cell [get_cells "u_cpu/u_alu/mul_32_inst"]

# Inspect electrical and physical attributes:
set ref_lib_cell [get_attribute $cell ref_name]
set cell_area    [get_attribute $cell area]
set is_seq       [get_attribute $cell is_sequential]

puts "Cell $cell:"
puts "  Library Ref: $ref_lib_cell"
puts "  Silicon Area: $cell_area um^2"
puts "  Sequential:   $is_seq"

# Querying net capacitance and fanout load:
set net [get_nets "clk_core_tree_net"]
set net_cap [get_attribute $net total_capacitance]
puts "Clock Net Capacitance: ${net_cap}pF"
§106

SDC Timing Constraints: create_clock, IO Delays, False Paths

EDA & VLSI

The Synopsys Design Constraints (SDC) specification is written purely in Tcl syntax. Core timing rules include defining real and virtual clocks, IO boundary budgets, clock uncertainty/jitter, and timing exceptions (false paths & multicycle paths).

SDC / Tcl
# 1. Primary Clock Definition (500 MHz -> Period 2.0ns, 50% duty cycle)
create_clock -name "clk_sys" -period 2.000 [get_ports "sys_clk_i"]

# 2. Generated Clock (Divide-by-2 derived clock from PLL)
create_generated_clock -name "clk_div2" -source [get_ports "sys_clk_i"] \
    -divide_by 2 [get_pins "u_pll/clk_out_q"]

# 3. Clock Uncertainty (Jitter + Margin)
set_clock_uncertainty -setup 0.080 [get_clocks "clk_sys"]
set_clock_uncertainty -hold  0.030 [get_clocks "clk_sys"]

# 4. Input & Output Delay Constraints (60% board budget external)
set_input_delay  -clock "clk_sys" -max 0.600 [get_ports "data_in[*]"]
set_output_delay -clock "clk_sys" -max 0.500 [get_ports "data_out[*]"]

# 5. Timing Exceptions: False Paths (Asynchronous reset & CDC)
set_false_path -from [get_ports "rst_n_i"]
set_clock_groups -asynchronous -group [get_clocks "clk_sys"] -group [get_clocks "clk_div2"]
§107

Template: Automated Timing Slack Auditor & Path Analyzer

Production Template

A production-grade timing path auditor proc for PrimeTime / Innovus / OpenROAD that iterates over worst timing paths, calculates Total Negative Slack (TNS), Worst Negative Slack (WNS), and generates a structured closure summary.

Tcl (EDA Automation)
proc audit_timing_closure {clock_domain {max_paths 100}} {
    puts "=========================================================="
    puts " AUDITING TIMING CLOSURE: Domain '$clock_domain'"
    puts "=========================================================="

    # Query timing paths collection
    set paths [get_timing_paths -max_paths $max_paths -slack_lesser_than 0.0]
    set path_count [sizeof_collection $paths]

    if {$path_count == 0} {
        puts "✅ TIMING CLEAN: Zero setup violations in domain '$clock_domain'."
        return [dict create status "CLEAN" wns 0.0 tns 0.0 count 0]
    }

    set wns 0.0
    set tns 0.0
    set idx 0

    foreach_in_collection path $paths {
        set slack [get_attribute $path slack]
        set start [get_object_name [get_attribute $path startpoint]]
        set end   [get_object_name [get_attribute $path endpoint]]

        if {$slack < $wns} { set wns $slack }
        set tns [expr {$tns + $slack}]

        if {$idx < 5} {
            puts [format "Viol #%02d | Slack: %8.3f ns | From: %-30s -> To: %s" \
                [expr {$idx + 1}] $slack [file tail $start] [file tail $end]]
        }
        incr idx
    }

    puts "----------------------------------------------------------"
    puts [format "SUMMARY: WNS = %8.3f ns | TNS = %8.3f ns | Viols = %d" $wns $tns $path_count]
    puts "=========================================================="

    return [dict create status "VIOLATED" wns $wns tns $tns count $path_count]
}
§108

Template: High-Fanout Net (HFN) Auditor & Inserter

Production Template

Excessive fanout causes large transition slews, electromigration risks, and setup timing degradation. This production procedure audits all non-clock nets in the design against a maximum fanout threshold.

Tcl (EDA Automation)
proc audit_high_fanout_nets {{fanout_threshold 32}} {
    puts "Scanning netlist for nets with fanout > $fanout_threshold..."
    # Query all design nets, excluding power, ground, and clock nets
    set candidate_nets [get_nets -hierarchical -filter "is_power == false && is_ground == false"]
    set violating_nets [list]

    foreach_in_collection net $candidate_nets {
        # Check if net is part of a clock tree:
        if {[get_attribute -quiet $net is_clock_network] eq "true"} continue

        # Query all load pins connected to this net
        set load_pins [get_pins -quiet -leaf -of_objects $net -filter "direction == in"]
        set fanout_count [sizeof_collection $load_pins]

        if {$fanout_count > $fanout_threshold} {
            set net_name [get_object_name $net]
            lappend violating_nets [list $net_name $fanout_count]
        }
    }

    # Sort descending by fanout count:
    set sorted_viols [lsort -integer -decreasing -index 1 $violating_nets]
    puts "Found [llength $sorted_viols] High-Fanout Nets (HFN):"
    foreach item [lrange $sorted_viols 0 9] {
        lassign $item nName fCount
        puts [format "  Net: %-45s | Fanout: %5d loads" $nName $fCount]
    }
    return $sorted_viols
}
§109

Template: Automated Post-Layout Hold/Setup ECO Generator

Production Template

An automated Engineering Change Order (ECO) generator script that extracts hold-time violating endpoints and synthesizes tool-specific buffer insertion commands written to an executable ECO Tcl script.

Tcl (EDA Automation)
proc generate_hold_eco_script {out_eco_filename {delay_cell "DLY2_X1"} {target_slack 0.050}} {
    set eco_fh [open $out_eco_filename "w"]
    puts $eco_fh "# ========================================================"
    puts $eco_fh "# Automated Post-Route Hold Timing ECO Fix Script"
    puts $eco_fh "# Target Delay Cell: $delay_cell"
    puts $eco_fh "# Generated: [clock format [clock seconds]]"
    puts $eco_fh "# ========================================================\n"

    # Query all hold violations (min timing paths)
    set hold_paths [get_timing_paths -delay_type min -max_paths 500 -slack_lesser_than 0.0]
    set eco_count 0

    foreach_in_collection path $hold_paths {
        set slack [get_attribute $path slack]
        set endpoint_pin [get_attribute $path endpoint]
        set pin_name [get_object_name $endpoint_pin]

        # Get the input net connected to this violating D-pin
        set net [get_nets -of_objects $endpoint_pin]
        set net_name [get_object_name $net]

        set eco_inst "u_eco_hold_${eco_count}"
        set eco_net  "net_eco_hold_${eco_count}"

        # Write tool-agnostic physical ECO commands:
        puts $eco_fh "# Hold Violation: $slack ns on $pin_name"
        puts $eco_fh "ecoAddRepeater -term $pin_name -cell $delay_cell -name $eco_inst -newNetName $eco_net"
        incr eco_count
    }

    close $eco_fh
    puts "SUCCESS: Generated $eco_count hold-fixing ECO operations in '$out_eco_filename'."
    return $eco_count
}
§110

Template: Batch Regression Runner & Log Normalizer

Production Template

A complete regression orchestrator script that executes multi-corner PnR or STA jobs in parallel subprocesses, normalizes heterogeneous log outputs into standard JSON/CSV summary reports, and verifies tape-out readiness.

Tcl (EDA Automation)
proc run_batch_corner_regression {corners_list run_dir} {
    file mkdir $run_dir
    set report_summary [dict create]

    puts "=========================================================="
    puts " LAUNCHING MULTI-CORNER REGRESSION: [llength $corners_list] Corners"
    puts "=========================================================="

    foreach corner $corners_list {
        set corner_log [file join $run_dir "sta_${corner}.log"]
        puts "Executing STA for corner '$corner' -> $corner_log"

        # Spawn batch STA job (e.g. OpenROAD or PrimeTime batch)
        set cmd [list openroad -exit -metrics [file join $run_dir "${corner}.json"] \
                     "scripts/sta_${corner}.tcl"]

        set start_time [clock seconds]
        set exit_status [catch {exec {*}$cmd >& $corner_log} exec_err]
        set elapsed [expr {[clock seconds] - $start_time}]

        # Audit log for critical metrics:
        set wns 0.0
        set log_fh [open $corner_log r]
        while {[gets $log_fh line] >= 0} {
            if {[regexp {worst slack\s*([-\d.]+)} $line -> captured_slack]} {
                set wns [expr {double($captured_slack)}]
            }
        }
        close $log_fh

        set status_label [expr {$wns >= 0.0 ? "PASS" : "FAIL"}]
        dict set report_summary $corner [dict create \
            status $status_label wns $wns runtime $elapsed exit_code $exit_status]

        puts [format "Corner %-12s | Status: %-4s | WNS: %7.3f ns | Runtime: %3ds" \
            $corner $status_label $wns $elapsed]
    }

    # Export normalized CSV report:
    set csv_fh [open [file join $run_dir "regression_summary.csv"] w]
    puts $csv_fh "Corner,Status,WNS_ns,Runtime_s"
    dict for {c info} $report_summary {
        dict with info {
            puts $csv_fh "$c,$status,$wns,$runtime"
        }
    }
    close $csv_fh
    puts "=========================================================="
    puts " REGRESSION FINISHED: Summary written to $run_dir/regression_summary.csv"
    puts "=========================================================="
    return $report_summary
}