Sept 21, 2015 — Defining until in Tcl

Background

One of Tcl’s greatest strengths is the ability for the programmer to seamlessly create new control structures. In this example, we will define a new control structure called until. Until takes two parameters: the first is a test condidtion and the second is a script to run while the condition is not true. For example, if we wished to print all numbers from 1 to 10, we could write the following:

until {$i == 10} {puts $i; incr i 1}

This of course assumes that the variable i has already been initialized to some number.

Implementation

The first iteration and simplest form of our control structure, in pseudo-code, is as follows:

  • Take the given test condition and run it in the caller’s scope.

    • If the test is untrue

      • Take the given script and run it in the caller’s scope

      • Return to #1

Our first attempt at the code:

proc until {test body} {
  # We run the test in the caller’s scope, but check its return value in this scope
  if { ! [uplevel 1 $test] } {
    uplevel 1 $body
  }
}

Testing our procedure gives the following output:

set i 1
until {expr {$i == 10}} {puts $i; incr i 1}
> 1
> 2

We get two values back. The first, "1" is from our body-script’s put statement. The second, "2" is the return value of the incr command.

Now that we’re sure our until control structure’s process flow works for a single iteration, we’ll modify it to run in a loop-like fasion. We do this by simply replacing the inner if-statement with the built in while command:

proc until {test body} {
  # We run the test in the caller’s scope, but check its return value in this scope
  while { ! [uplevel 1 $test] } {
    uplevel 1 $body
  }
}

Again, we test:

set i 1
until {expr {$i == 10}} {puts $i; incr i 1}
> 1
> 2
> 3
> 4
> 5
> 6
> 7
> 8
> 9

We have one additional step which is to clean up the call signature to until:

until {expr {$i == 10}} {puts $i; incr i 1}

will become:

until {$i == 10} {puts $i; incr i 1}

To make this change, we move the call to expr from the test expression into our procedure:

proc until {test body} {
  # We run the test in the caller’s scope, but check its return value in this scope
  while { ! [uplevel 1 {expr $test}] } {
    uplevel 1 $body
  }
}

Calling our procedure, we get the following:

until {$i == 10} {puts $i; incr i 1}
> can't read "test": no such variable

The reason for this is because the uplevel call in our while loop runs the literal form {expr $test} instead of the expanded form {expr {$i == 10}}. This is due to the use of curly-braces.

If we replace the braces with quotes instead:

proc until {test body} {
  # We run the test in the caller’s scope, but check its return value in this scope
  while { ! [uplevel 1 “expr $test”] } {
    uplevel 1 $body
  }
}

We get another error:

until {$i == 10} {puts $i; incr i 1}
>invalid command name "“{expr"

The solution is actually much simpler than using braces or quotes. Uplevel concatenates all arguments together into a single list, and then performs its operation. That means we can do the following:

proc until {test body} {
  # We run the test in the caller’s scope, but check its return value in this scope
  while { ! [uplevel 1 expr $test] } {
    uplevel 1 $body
  }
}