May 18, 2026

My notes on Tcl

I am no fan of the Tcl language. I swore years ago that I was done with it, and got rid of the two Tcl books I had.

My current interest has to do with two programs that use Tcl as a "back-end" language. These are the Vivado FPGA design program from Xilinux and the OpenOCD program I use for JTAG devices and such.

Tcl was never intended to be used as a general purpose programming language. That doesn't stop it from being used as such of course. It was designed to be a small footprint scripting language that could be made a part of other programs. A big factor that launched Tcl into wild popularity in times past was the availability of the "Tk" toolkit for Tcl. This made it possible to use Tcl/Tk to quickly and easily write GUI programs. These days there are many much superior options for such things.

I'll point out that the use of Tcl by Vivado and OpenOCD is exactly the sort of thing Tcl was originally intended to be used for.

And for the record, the Tcl language was invented by John Ousterhout at UC Berkeley in 1988. He aimed to create an embeddable small footprint language so that developers would not need to invent their own languages. I remember attending a talk by him he called "bricks and glue" where he described Tcl as the glue to put together bricks provided by whatever application was being developed.

Stick our toe into the water

I am now running Fedora 43 linux on an x86 machine. I have Tcl installed (there are various versions available as standard Fedora packages). It seems to be verion 9.0.2. I also see Tclx which is a dialect with extensions, and there is also "jimtcl" which is a stripped down version used by OpenOCD.

The first "surprise" is that typing "tcl" gives me "Command not found". The thing to do is to type "tclsh" and you will be greeted with a prompt. We can write the customary "hello world" program as follows:

#!/bin/tclsh

puts "hello world"
This gets us off to a start. I won't spend any time talking about Tk. What I will do right away is show you some things to illustrate some of the weaknesses of Tcl. This is not simply to throw mud at Tcl, but I think you are well served to know right up front some things that may cause you pain if you are used to other programming languages.

Try a statement like this:

x = 42
Tcl will tell you "invalid command name "x"" -- what you need to do is:
set x 42
This could be our first Tcl "lesson". Tcl is always looking for a command. The word "set" is a command, but just tossing the variable name "x" at it would require Tcl to recognize (and perhaps initialize) a variable in this context, which is not how Tcl plays the game.

Now try this program:

set x 42
puts x
Tcl will print "x", not 42 -- what you need is:
set x 42
puts $x
Now try this:
set two 2
set sum $two+$two
puts $sum
This prints "2+2". Tcl is full of surprises and unexpected behavior. The way to get the result we want here is as follows:
set two 2
set sum [expr ($two+$two)]
puts $sum
How about that? Why the square brackets? We will get to that in a minute. My aim in all this is to show you that Tcl doesn't behave like any other programming language. The parser is very simple. It does not understand "infix" expressions (such as x=42 or sum=2+2). It views "x" as just another string unless we prefix it with a dollar sign to tell Tcl that what follows is the name of a variable.

Tcl has no types. It stores everthing as a string. Nothing is compiled in the sense we usually think about it. The Tcl interpreter just works on our code as strings, interpreting it as it goes (we will get to some implications of that shortly).

So, why the square brackets? The Tcl parser is simple and needs our help all of the time. We ran into this already with the need to prefix a variable name with a dollar sign. A good general statement is that everything is a string in Tcl until we tell it otherwise. Another general statement is that Tcl will interpret these strings as commands unless we tell it otherwise. The square brackets tell Tcl to run whatever command is inside them and replace the bracketed entity with the result of the command.

What about "expr"? This essentially introduces a sublanguage that knows about infix expressions that we take for granted in other languages.

And before we move on entirely, consider {} (curly brackets). These are really a way of quoting a bunch of text. Whatever is enclosed in curly brackets is collected up as a string and the text is passed along to whatever command might need it. You most often see this in conditionals (if statements) and procedure bodies.

Lurking disaster

Consider this program:
#!/bin/tclsh

puts "All is well"
puts "sleeping"

after 2000 {
	msg = "done"
	puts msg
	exit
}
vwait forever
The savy reader will notice that "puts msg" will not work, what we need is "puts $msg". But I put this error in intentionally to illustrate the following. If we run this program, we see:
All is well
sleeping
invalid command name "msg"
The important thing is that we only see the error after the two second delay. The program starts and runs just fine with this error lurking. Most languages would detect an error like this (or a multitude of others) at compile time. With Tcl there is no compile time. The error is not detected until Tcl actually needs to run that line of code and attempts to interpret it.

This can be a serious problem in bigger programs. An error like this may not be detected until the program has been running for hours, days, even weeks perhaps. Imagine your airline ticket reservation system taking an infrequent branch months after deployment and tripping over a minor coding error. It happens all the time with Tcl programs.

What about conditionals

Leaving some notorious pitfalls and oddities, let's look at some basic constructs that are needed in any programming language. How can we write conditional code?

Remember first of all that Tcl is typeless. Remember also that "everthing is a string". We thus have no boolean type. Strings holding integer values are false if they hold a "0" and are true otherwise. Strings in general are more interesting. The words yes, no, true, false, on, and off are recognized as you might expect. What happens if we test a string that contains "fish"? You get an error, you aren't supposed to do that.

if { expr } {
	code
} elsif { expr2 } {
	code
} else {
	code
}
Note that there are no parenthesis in the above. It is all curly braces. The expressions are surrounded by curly braces. Your deeply ingrained habits from other languages go out the window here.

This is the basic game. You are encouraged to always use the curly braces around the code bodies. You are not required to, but unexpected substitutions can happen if you don't. The context of an if statement allows logical expressions with < == and such.

I try this:

set dog "fish"
if { $dog < 99 } {
	puts "true"
}
Tcl accepts this without error. The condition never is true, regardless of the number value. I have no idea what is going on here. There is an explicit way to get the length of a string:
set len [string length "Hello World"]
Here "string" is a multipurpose utility for use with strings and "length" is just one of many things it can do.

What about loops?

You get three: for, foreach, and while. You are encouraged to always use curly braces around the loop body.

If you understand "if" you understand "while" so nothing more will be said.

The "foreach" loop works with a list:

set mylist { good bad ugly }
foreach x $mylist {
	puts $x
}
The above makes an interesting lesson about when to use commas, dollar signs, and curly braces.
The above could be written more succinctly if the list never changes:
foreach x { red green yellow } {
	puts $x
}
A "for" loop is ugly and looks like this:
for {set i 0} {$i < 5} {incr i} {
    puts "Count: $i"
}
Once again, curly braces everywhere. Note that we have introduced the "incr" function in Tcl, which we can use to avoid dragging in "expr" in cases like this. We have "break" and "continue" to exit a loop or jump to the next iteration.

More on curly braces

Don't be fooled, even though these are used in Tcl in a way that simulates "block structure" in other languages, that is not really what is going on.

Curly braces in Tcl are really a flavor of quotes. They are quotes that postpone interpolation. Interpolation is where Tcl sees a dollar sign and injects the value of a variable. Tcl has plain old double quotes that do allow interpolation.

Consider this "for" statement.

for {set i 0} {$i < 5} {incr i} {
    puts "Count: $i"
}
What is really going on here is that we have a command called "for" with 4 arguments. In this example, each of the four arguments is a string enclosed in curly braces. If you play some games, you can get Tcl to complain and coach you as follows:
wrong # args: should be "for start test next command"
Consider the following code, which prints the word "dog" 5 times and gets rid of the curly braces on the "command" argument.
for {set i 0} {$i < 5} {incr i} "puts dog"
Now try this. Tcl complains "can't read "i": no such variable". It is trying to perform the interpolation for $i early when putting the for loop together and there ain't no "i" variable yet. Using curly braces would postpone that interpolation and make that work.
for {set i 0} {$i < 5} {incr i} "puts $i"
To make that even more clear, consider this:
set i 99
for {set i 0} {$i < 5} "incr i" "puts $i"
This runs, printing 99 five times. Notice also that I changed the flavor of the quotes around "incr i". It doesn't matter here what flavor is uses as there is no interpolation going on.

An example from Vivado

My purpose in reviewing Tcl is to be able to work with Vivado software from Xilinx. Here are some lines from a Vivado constraint file:
set_property IOSTANDARD LVCMOS33 [get_ports {GPIO_0_0_tri_io[*]}]
set_property SLEW SLOW [get_ports {GPIO_0_0_tri_io[*]}]
set_property DRIVE 8 [get_ports {GPIO_0_0_tri_io[*]}]
set_property PACKAGE_PIN T11 [get_ports { GPIO_0_0_tri_io[0] }];
This is pretty easy to recognize as Tcl. Apparently Vivado has added "set_property" and "get_ports" to Tcl as commands to the Tcl vocabulary. The only suprising construction here is the "*" wildcard symbol. At this point, I think this is something special that Vivado supports, but I am not entirely sure.

A peek at Lua

On my Fedora 43 system "dnf install lua" gets it so I can play with it. I write the following code:
#!/bin/lua

msg = "Hello world"

for i = 1, 5 do
	print ( 100 + i )
	print ( msg )
end
It should be immediately obvious that this is a more civilized language than Tcl. In its day, Tcl may have been a good choice for an embedded language, but if Lua has a similar footprint (memory requirements), I would clearly chose it for new project. See the next section.

Small footprint?

I find myself tempted towards using Tcl in some embedded projects. It was, after all, intended for such things.

Standard Tcl however has grown and is no longer a good choice. There is JimTcl, which was once a stripped down Tcl, but it also seems to be growing. The claim is that it fits in 100K to 200K. There are other choices such as MicroTcl (160K) and "picol" which is said to be 550 lines of C code!

What about Lua? There are many variants and it offers a more civilized language. There is eLua, which can run in 20K of flash in certain configurations. On the Arduino it has been made to run in 80K of flash.

What about Python? There is MicroPython for embedded systems that runs in 256K of code and 16K of ram. There is also Circuit Python, which seems to be a trimmed version of Micropython without interrupts or thread support. Circuit Python is available as a binary image for some boards that I have, which would make it easy to try out.

I have several Pi Pico boards, as well as some Pi Pico 2 boards sitting next to me on my desk right now. Trying it would be as simple as downloading the UF2 file and flashing it.

Not to be outdone, Lua (in particular MicroLua) is available for the Pi Pico: MicroLua or PicoLua? Hard to tell at this time. Pico looks really tiny and as if the intent is to run 3-5 line code snippets on the fly.

Lua, unlike Python, does not use indentation for code blocks, rather using do/end.
Some Lua cons:

Lua is popular in game development (which surprises me). If I learn Lua and switch to NeoVim that could be a win. Wireshark uses Lua as an embedded language. It is (also to my surprise) used in Adobe Lightroom.

It was written in 1993 (5 years after Tcl) by a trio of dudes in Brazil.


Feedback? Questions? Drop me a line!

Tom's Computer Info / tom@mmto.org