CMake as Tcl in a Trenchcoat

CMake and Tcl have a substantial number of similarities. Both languages:

  • Are imperative.
  • More-or-less natively support only string variables.
  • Represent lists as specially-formatted strings.
  • Have a single namespace for functions.
  • Use function syntax for everything (control flow etc).
  • Support some form of “uplevel” variable setting.

Imperative: This is obvious for Tcl. For CMake, it is not always obvious, as there are multiple steps taken when building a project: First the CMake script runs, creating various objects in memory (the “configuration” step). Then, these objects are serialized into some representation on disk that can be used by an IDE or build executor like Make or Ninja (the “generation” step). Finally, the IDE or build executor runs a build, where CMake is totally detached. To really understand how to use CMake, you need to understand all three steps – but at least the configuration step proper is fully imperative.

String variables: Are all that’s supported by CMake. Tcl does have associative arrays, but they’re not really a first-class type of value (for example, you can’t put an associative array inside of an associative array). Setting aside Tcl’s associative arrays, strings are all you’ve got, just like CMake.

Lists as strings: Tcl’s representation of lists is central to the language, being more-or-less (a few exceptions) used for the language’s very own syntax. In CMake, it is not so central; it appears more as a hack bolted-on. But it does make its way into evaluation semantics. A list of items a, b, and c in Tcl is a b c; in CMake, it is a;b;c. Both languages allow escaping; in Tcl, you could represent “a b”, “c d”, and “e f” as a\ b c\ d e\ f, "a b" "c d" "e f", or {a b} {c d} {e f}. In CMake, you could represent “a;b”, “c;d”, and “e;f” with a\;b;c\;d;e\;f. Lists in either language can be nested through further escaping (but don’t do this in CMake… it ends up being somewhat cursed).

Further regarding lists, they have similar ways to expand lists in command arguments. In Tcl:

# Pass list directly, don't expand arguments
command $list
# Pass list's elements as separate arguments
command {*}$list
# Old way of passing list's elements as separate arguments
eval command $list

In CMake:

# Pass list directly, don't expand arguments
command("${list}")
# Pass list's elements as separate arguments
command(${list})

Have a single namespace for functions: OK, this is a flat-out lie for Tcl – it has a namespace facility fittingly called namespace. But still, commands are “global” in some sense and stringly-named – they are not first-class values (though their names can be). CMake has directory scope, but other than that, it’s about the same.

Function syntax for everything (control flow etc): Neither language has special syntax for… nearly anything. Tcl is a little purer on this front: if is a function, through and through, and the code for its branches are function arguments. There is no special treatment of any kind of functions. For CMake, while the syntax is identical, control flow identifiers do require special treatment – they start “gobbling up” commands until they end. CMake also has a distinction between functions and macros, where macros have similar “special” expansion characteristics. CMake’s behavior is comparable to TEX’s in this regard.

Uplevel variable setting: Both languages offer a form of local variable scoping. However, it’s not strict in either language, and both languages offer functionality to change the calling function’s local variables. Tcl does this through a generic uplevel functionality, which also has uses other than setting a calling function’s locals, but it can surely be used with set to do so as well. In CMake, this functionality is more limited, and is built into certain commands – set() can take a PARENT_SCOPE option to set a variable in the caller’s scope.

Opposites day

Here’s a fairly ordinary CMake project file for a C++ project with a library and an executable, both of which get installed:

cmake_minimum_required(VERSION 3.25.2)
project(Example CXX)
add_library(foo foo.cpp foo.hpp)
add_executable(bar bar.cpp bar.hpp)
target_link_libraries(bar foo)
install(TARGETS foo bar)

Suppose that the same commands were implemented, but a Tcl interpreter were used instead. It might look like this:

cmake_minimum_required VERSION 3.25.2
project Example CXX
add_library foo foo.cpp foo.hpp
add_executable bar bar.cpp bar.hpp
target_link_libraries bar foo
install TARGETS foo bar

(There are surely more interesting examples out there, but this is representative of many simple CMake projects.) Now something a little more cursed. What if we took this Tcl program:

proc fib {n} {
    set a 0
    set b 1
    for {set i 0} {$i < $n} {incr i} {
        set c [expr $a + $b]
        set a $b
        set b $c
    }
    return $a
}

And rewrote it in CMake syntax, assuming Tcl semantics for everything?

proc(fib b [[
    set(a 0)
    set(b 1)
    for ("set(i 0)" "${i} < ${n}" "incr(i)" [=[
        set(c "expr(\${a} + \${b})")
        set(a $b)
        set(b $c)
    ]=])
]])

Be happy this is not the world we live in.

Tags: ,

Comments are closed.