TCL Wat

At FlightAware, I read and write TCL every day. This means I've run into more than a few edge cases in the language, and have even tracked down a few bugs in TCLlib. Here's a collection of the weirdest things I've encountered.

Empty string is a boolean superposition

In TCL, an empty string is regarded as both true and false, due to the language's dynamic typing and the way it handles truthy/falsy values. This can lead to unexpected behavior in conditionals:

if {""} {
    puts "This is true"
} elseif {!""} {
    puts "This is false"
} else {
    puts "This is neither true nor false"
}

Output:

This is neither true nor false

Why is this the way it is? TCL treats empty strings as falsy in boolean contexts, but the empty string itself is still a valid string value. The negation !"" creates a boolean true, but the empty string in the if condition is also falsy. It's... confusing.

Scalars are converted to dictionary keys

When using dict set command to set a value into a dictionary, it can't automatically convert a scalar variable to a dictionary. You have to explicitly create a dictionary first:

set my_dict [dict create]
dict set my_dict key value

This is actually reasonable behavior - TCL doesn't want to assume you meant to create a dict when you might have just made a typo. But it's still annoying when you forget.

Upvar and pass-by-name

upvar is a command that introduces a new variable in the current context which is an alias for a variable in a caller's context. TCL uses pass-by-name for its argument passing, meaning the names of variables are passed, rather than their values:

proc increment {varName} {
    upvar $varName var
    incr var
}

proc swap {a b} {
    upvar $a aVar $b bVar
    set temp $aVar
    set aVar $bVar
    set bVar $temp
}

This is actually pretty cool once you get used to it, but it's definitely not what you'd expect coming from other languages.

Insane string matching

Some TCL commands, like string is, allow matching on shortened or abbreviated strings. While this can lead to concise code, it may also introduce unintended and interesting bugs:

string is true true
# => 1
string is tru true
# => 1
string is tr true
# => 1
string is t true
# => 1
string is fa 0
# => 1

This has no practical benefit and serves only to create bugs. Why would a reasonable person ever want to check if a string "is tr" in production code?

Split nested list, get flat list

When using split on a nested list, the result is a list with members representing the escaped syntax of the nested list(s):

set a {aa bb {cc dd}}
split $a " "
# => {aa bb \{cc dd\}}

Notice that the braces of the nested list {cc dd} have been escaped, resulting in a string that looks like a dict, but is not. This is because under the hood, split operates on the string representation, not the list structure. This has been the source of a number of real bugs at FlightAware.

Lower-case numbers

The string tolower command attempts to convert a string's characters to lowercase. When encountering mixed or non-alphabetic input, the results may be unexpected:

set number_string "42A_B8C"
string tolower $number_string
# => 42a_b8c

Even though the numbers remain the same, the non-alphabetic characters have been modified. This is technically correct behavior, but it's weird that you can "lowercase" a number.

Dict loops don't garbage-collect

When using dict with inside a loop, it overrides variables, but it doesn't automatically unset variables from previous iterations that have no new value:

set data {
    item1 {var1 value1}
    item2 {var2 value2}
}

foreach item {item1 item2} {
    dict with data $item {
        puts "Item: $item, var1: $var1, var2: $var2"
    }
}

Output:

Item: item1, var1: value1, var2: value2
Item: item2, var1: value1, var2: value2

Notice that var1 is still set to value1 in the second iteration. The values were not garbage collected at the end of the dict with body. This has been another real source of bugs.

Comments are not comments

In TCL, comments are technically commands that do nothing, the # command, which is a no-op, to create comments:

# This is a TCL comment

This means that comments can be placed anywhere a TCL command can go, but also that they are part of the evaluation process, albeit with no effect. It's... different.

Uplevel is OP

The uplevel command is used to execute a script in a different scope or level. This can be useful in creating macros or DSLs (Domain Specific Languages) in TCL:

proc print_doubled {script} {
    uplevel "set x [expr {$script * 2}] ; puts \$x"
}

print_doubled {2 + 3}
# => 10

This is actually pretty powerful, but it's also a great way to write code that nobody can understand or debug.

Lack of static typing

TCL is a dynamically typed language, which means variables don't have a fixed type. A variable can change its type during runtime:

set my_var 42
puts "Variable type: [string is integer $my_var]"
# => Variable type: 1
set my_var "hello"
puts "Variable type: [string is integer $my_var]"
# => Variable type: 0

This can lead to flexibility in development, but may also cause unintended consequences or unexpected behavior. Especially when dealing with JSON or other structured data formats. On the surface, this behavior is expected because "everything is a string". But it causes bugs when you understand TCL well enough to know that nothing is a string at the fundamental level.

Subst is not a separate compilation step

In TCL, the subst command is used to perform variable and command substitutions within strings. This can be handy in some situations, but it's important to note that subst is executed at runtime:

set x 10
set y "\$x + 5"
puts "Before substitution: $y"
# => Before substitution: $x + 5
puts "After substitution: [subst $y]"
# => After substitution: 10 + 5

This may be surprising if you're expecting TCL to act like a Lisp, and means that you might miss syntax errors until the code is actually executed.

Two ways of catching errors

TCL provides error handling mechanisms via the catch, try, and error commands:

if {[catch {some_command} result]} {
    puts "Error encountered: $result"
}

try {
    some_command
} on error {errorMsg} {
    puts "Error encountered: $errorMsg"
}

The catch command can be used to capture any errors or exceptions that occur within a script. try and error offer more sophisticated error handling, but they're still not as clean as exceptions in other languages.

One particularly tricky aspect is that these error handling mechanisms will also set the errorCode global variable, which can be both useful and a curse to debugging. When a TCL library function is the source of the error, this global can leak error state between different parts of your code, making it hard to track down where errors are actually coming from. It's especially problematic when functions use error handling as a way of checking for dictionary key existence.


So there you have it - a collection of TCL weirdness that I encounter on a daily basis at FlightAware. Some of these are actually reasonable design decisions when you understand the philosophy behind TCL, but others are just... wat.