Tcl Part I · Module 9 · Loops and Iteration
Loop Bodies Need Braces: The Infinite-Loop Trap
Chapter 8.2 made the case for bracing an if condition, and most of the failures there were loud — an error message naming the problem. In a loop the same mistake is silent, and it is much worse: a quoted condition is evaluated once, while the line is being parsed, and then frozen. If it was true at that moment it stays true for ever and the loop never ends; if it was false it stays false and the loop never runs. Neither produces an error. This is the single most common way a beginner's Tcl script hangs, and the demonstrations here were all run with a safety counter so you can see exactly what the frozen condition contains.
Foundation11 min readTclPart ILoopsBracesInfinite loop
Part I · Module 9 · Chapter 9.7 · Loop Bodies Need Braces: The Infinite-Loop Trap
1. The rule
Brace the condition of every
whileand everyfor. Brace every loop body.
while {$i < 3} { incr i } ;# correct
while "$i < 3" { incr i } ;# never endsThe second line is not a style problem. It is a hang.
2. What the frozen condition contains
Tcl substitutes a quoted word while parsing the line, before while ever runs (Chapter 2.11). So with i at 0, what while actually receives is:
set i 0
puts "the text handed to while: '[subst {$i < 3}]'"the text handed to while: '0 < 3'0 < 3. Not "$i compared with 3" — the literal characters 0 < 3, with the value baked in. while re-evaluates that text on every pass, and every time it evaluates to true, because there is no longer a variable in it to change.
3. Watching it not terminate
You cannot run an infinite loop and report on it, so this one carries a counter that stops it and says so:
set i 0
set guard 0
while "$i < 3" {
incr i
incr guard
if {$guard >= 50000} {
puts "SAFETY STOP after $guard passes; i is now $i"
break
}
}
puts "the loop did not end on its own"SAFETY STOP after 50000 passes; i is now 50000
the loop did not end on its owni reached fifty thousand. The condition said 0 < 3 the whole time. Braced, the same loop is unremarkable:
set i 0
set guard 0
while {$i < 3} {
incr i
incr guard
}
puts "ended normally: i=$i after $guard passes"ended normally: i=3 after 3 passes4. The other half of the trap
A frozen condition that happens to be false when the line is parsed is just as broken, and much quieter:
set i 99
set n 0
while "$i < 3" {
incr n
}
puts "ran $n times"ran 0 timesNo hang, no error, no output — and no work done. A loop that silently does nothing is harder to notice than one that hangs, because the script finishes normally and simply produces less than it should.
5. for freezes the same way — and sooner than you expect
set i 0
set guard 0
for {set i 0} "$i < 3" {incr i} {
incr guard
if {$guard >= 50000} {
puts "SAFETY STOP after $guard passes; i is now $i"
break
}
}SAFETY STOP after 50000 passes; i is now 49999The test argument of a for is an expression evaluated on every pass, exactly like a while condition, so quoting it freezes it the same way.
There is an extra twist here worth seeing. The condition was frozen using the value i held before the line was parsed — not the value for's own start script sets. Change what i was beforehand and the loop behaves completely differently, even though the start script is identical:
set i 99
set guard 0
for {set i 0} "$i < 3" {incr i} {
incr guard
}
puts "ran $guard times"ran 0 timesZero passes. i was 99 when Tcl parsed the line, so the condition froze as 99 < 3, which is false — and for's own set i 0 never got the chance to matter, because the condition had already been fixed before the loop started.
And if i does not exist at all when the line is parsed, the substitution fails immediately:
unset -nocomplain i
for {set i 0} "$i < 3" {incr i} { break }can't read "i": no such variableThat is the one helpful case: the error names the variable, even though the loop's own start script would have created it a moment later.
6. The body is a different question
Be precise about what the trap is. A quoted body still runs, and still runs on every pass:
set i 0
while {$i < 3} "incr i"
puts "i = $i"i = 3That terminated. The body is re-evaluated each pass whether it is braced or quoted, so a quoted body does not cause a hang.
It is still wrong, for the reason Chapter 8.2 §8 gave: a quoted body is substituted once, when the line is parsed, so any $ or [ ] inside it resolves at the wrong moment and against the wrong values.
set msg "first"
set i 0
while {$i < 3} "puts $msg ; incr i"
set msg "second"first
first
first$msg was baked in when the loop was parsed. Braced, it would be looked up on each pass and would track any change.
7. How to avoid it permanently
The habit that makes this unreachable:
while {CONDITION} {
BODY
}
for {START} {TEST} {NEXT} {
BODY
}- Every condition and every test in
{ }. - Every body in
{ }. - Opening brace on the same line as the keyword — Chapter 2.10 explains why the next line gives
wrong # args: no script following ....
And when a loop's termination is not obvious at a glance, add a guard while you develop it, as Chapter 9.5 §6 showed:
set guard 0
set x 1
while {$x < 1000} {
set x [expr {$x * 3}]
if {[incr guard] > 100} {
error "loop did not terminate after $guard passes"
}
}
puts "x = $x after $guard passes"x = 2187 after 7 passesAn error with a number beats a hung terminal every time.
8. Recognising it in someone else's code
Three signs, in order of how quickly they tell you:
- A quoted condition.
while "..."orfor ... "..." ...— look for the quote where a brace belongs. - A condition with no variable in it.
while {1}is deliberate;while {0 < 3}is almost certainly a frozen condition that someone pasted after it had already been substituted. - A
whilewhose body has no path that changes the condition's variable — including thecontinuecase from Chapter 9.6 §2, where the increment is there but unreachable.
9. Exercises
Exercise 1. With i at 0, what exact text does while "$i < 3" hand to while?
Exercise 2. Why does the quoted version never end, when while does re-evaluate its condition on every pass?
Exercise 3. What does this do, and why is it harder to notice than a hang?
set i 99
while "$i < 3" { puts "working" }Exercise 4. Does a quoted body cause an infinite loop? Say what it does cause.
Exercise 5. Rewrite correctly, and add a guard that reports rather than hangs if the termination argument is wrong:
set n 1
while "$n < 100" { set n [expr {$n * 2}] }Exercise 6. You are reviewing a script that hangs. Name three things to look at, in order.
Answers
1. The literal text 0 < 3. The value of i was substituted while the line was parsed.
2. It does re-evaluate — but what it re-evaluates is the fixed text 0 < 3, which contains no variable. Re-evaluating an expression with no variables in it gives the same answer every time.
3. It runs zero times and prints nothing. The script completes normally, so there is no hang and no error — just missing work, which may not be noticed until something downstream is short of data.
4. No. The body is re-evaluated each pass whether braced or quoted, so a quoted body still terminates. It does cause every $ and [ ] inside it to be substituted once, at parse time, so the body runs with stale values rather than current ones.
5.
set n 1
set guard 0
while {$n < 100} {
set n [expr {$n * 2}]
if {[incr guard] > 50} { error "did not terminate after $guard passes" }
}
puts "n = $n after $guard passes"n = 128 after 7 passes6. First, look for a quoted condition where braces belong. Second, look for a condition containing no variable. Third, check that the body actually reaches the code that changes the condition's variable — a continue above the increment makes it unreachable.
10. Remember
- Brace every loop condition and every loop body. A quoted condition is frozen at parse time.
- The frozen condition contains the value, not the variable —
while "$i < 3"withiat 0 becomes the literal0 < 3. - Frozen true means the loop never ends; frozen false means it never runs. Neither raises an error.
- This is worse than the
ifversion, which usually fails loudly. Here the symptom is a hung terminal or silently missing work. for's test argument freezes identically.- A quoted body does not hang — it is re-evaluated each pass — but every substitution inside it is resolved once, at parse time, so it runs on stale values.
- Add a guard counter to any loop whose termination is not obvious; an error with a number beats a hang.
- When something hangs, check in order: a quoted condition, a condition with no variable, and a body that cannot reach its own increment.
Where this fits
Part of the Tcl curriculum.
