Skip to content
VLSI Mentor

Tcl Part I · Module 9 · Loops and Iteration

while: Looping Until Something Changes

foreach and for both need to know what they are iterating before they start — a list, or a range. while does not. It keeps going as long as a condition holds, which makes it the right shape whenever you cannot say in advance how many passes there will be: reading until the input runs out, retrying until something succeeds, consuming a buffer until it is empty. That freedom is also the risk. A for loop carries its own advance; a while loop does not, and nothing in the language checks that your condition will ever become false. This chapter gives you a three-part habit that catches that before you run it.

Foundation11 min readTclPart ILoopswhileTermination

Part I · Module 9 · Chapter 9.5 · while: Looping Until Something Changes

1. The shape

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
set i 0
while {$i < 3} {
    puts "pass $i"
    incr i
}
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
pass 0
pass 1
pass 2

Two arguments: a test expression and a body. Tcl evaluates the test, runs the body if it is true, and repeats. Both are braced, for the reasons Chapter 8.2 gave — and here getting that wrong is worse than an error, which is Chapter 9.7.

2. When while is the right loop

Use while when the number of passes is not known in advance. The classic shape is consuming something until it runs out:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
set queue {a b c d}

while {[llength $queue] > 0} {
    set item [lindex $queue 0]
    set queue [lrange $queue 1 end]
    puts "handling $item ([llength $queue] left)"
}
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
handling a (3 left)
handling b (2 left)
handling c (1 left)
handling d (0 left)

The loop does not know there are four items. It knows to stop when the queue is empty, which is a different and often more honest statement of the job.

3. Retrying until something succeeds

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
proc attempt {n} {
    # pretend the third try is the one that works
    return [expr {$n >= 3}]
}

set tries 0
set ok 0
while {!$ok && $tries < 5} {
    incr tries
    set ok [attempt $tries]
    puts "attempt $tries -> [expr {$ok ? "ok" : "failed"}]"
}

if {$ok} {
    puts "succeeded after $tries attempts"
} else {
    puts "gave up after $tries attempts"
}
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
attempt 1 -> failed
attempt 2 -> failed
attempt 3 -> ok
succeeded after 3 attempts

Note the second half of the condition. !$ok alone would loop forever if the operation never succeeded; && $tries < 5 makes failure terminate too. A retry loop needs both.

4. while {1} with a break is legitimate

Sometimes the exit condition is only computable in the middle of the body:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
set n 0
while {1} {
    incr n
    if {$n * $n > 50} break
}
puts "first n with n squared over 50: $n"
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
first n with n squared over 50: 8

This is not a code smell when the test genuinely belongs mid-body. It is clearer than duplicating the computation once before the loop and once at the bottom. Chapter 9.6 covers break properly.

The thing to avoid is while {1} with the break buried in a nested if several levels down, where a reader cannot see what ends the loop.

5. The three-part termination check

Before you run any while, name three things:

  1. The variable in the condition exists before the loop starts.
  2. The body can change it.
  3. Something guarantees it eventually does.

Applied to section 1:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
set i 0                 ;# 1. exists
while {$i < 3} {
    puts "pass $i"
    incr i              ;# 2. the body changes it
}                       ;# 3. incr always moves toward 3

The first is easy to check and easy to get wrong:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
unset -nocomplain j
while {$j < 3} { incr j }
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
can't read "j": no such variable

That one fails loudly. The third is the one that bites, because nothing reports it. Here is the most common way it goes wrong, carried over from Chapter 9.4 §6:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
set i 0
set seen {}
while {$i < 5} {
    if {$i == 2} continue    ;# never reaches the incr below
    lappend seen $i
    incr i
}

That loop hangs. continue jumps back to the test, the incr at the bottom never runs, and i stays at 2.

6. A safety counter while developing

When the termination argument is not obvious, make the loop prove it:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
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"
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
x = 2187 after 7 passes

If the loop is right, the guard costs nothing. If it is wrong, you get an error with a number instead of a hung terminal. Remove it once the loop is settled, or leave it in if the condition depends on data you do not control.

7. while returns nothing

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
set i 0
set r [while {$i < 2} {incr i}]
puts "'[set r]'"
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
''

Same as foreach and for.

8. A worked example

Consuming a list of tokens with a condition that depends on what it finds:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
set tokens {open a b close open c close end x y}

set depth 0
set groups 0
set i 0

while {$i < [llength $tokens]} {
    set t [lindex $tokens $i]
    incr i

    if {$t eq "end"} break

    switch -- $t {
        open  { incr depth }
        close { incr depth -1 ; if {$depth == 0} { incr groups } }
    }
}

puts "groups closed : $groups"
puts "stopped at    : index $i of [llength $tokens]"
puts "depth         : $depth"
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
groups closed : 2
stopped at    : index 8 of 10
depth         : 0

The loop advances before anything can break or continue, the bound is checked on every pass, and the end token stops it early — all three parts of the termination check are visible in eight lines.

9. Exercises

Exercise 1. What are the two arguments to while, and why are both braced?

Exercise 2. Write a loop halving $n (integer division) until it reaches 0, printing each value.

Exercise 3. Name the three things that must be true for a while to terminate.

Exercise 4. Why does this hang, and give two different fixes?

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
set i 0
while {$i < 5} {
    if {$i == 2} continue
    incr i
}

Exercise 5. Rewrite as a foreach and say why while was the wrong choice:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
set i 0
while {$i < [llength $items]} { puts [lindex $items $i] ; incr i }

Exercise 6. A retry loop is written while {!$ok} { set ok [try_once] }. What is missing and what can happen?

Answers

1. A test expression and a body script. Both are braced so they are evaluated afresh on each pass rather than substituted once when the line is parsed — for the test, failing to brace it freezes the condition and the loop never ends.

2.

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
set n 20
while {$n > 0} {
    puts $n
    set n [expr {$n / 2}]
}
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
20
10
5
2
1

3. The condition's variable exists before the loop; the body can change it; and something guarantees it eventually does.

4. When i is 2, continue returns to the test without reaching incr i, so i never moves again. Fix one: advance before the continue can be reached. Fix two: use for, whose next script runs even on continue.

5.

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
foreach item $items { puts $item }

while was wrong because the number of passes was known in advance — it is the length of the list. while is for when it is not.

6. The loop has no bound on the number of attempts, so if try_once never succeeds it runs forever. It needs a second clause — while {!$ok && $tries < $max} — and a counter, so that permanent failure terminates and can be reported.

10. Remember

  • while {test} {body} — two arguments, both braced, test re-evaluated every pass.
  • Use it when the number of passes is not known in advance: consuming until empty, reading until end of input, retrying until success.
  • while {[gets $fp line] >= 0} is the shape you will meet most; file handles come in Module 12.
  • A retry loop needs a bound as well as a success test, or permanent failure never terminates.
  • while {1} with a visible break is fine when the exit test belongs mid-body.
  • Check three things before running it: the variable exists, the body can change it, and something guarantees it does.
  • continue in a while skips an increment at the bottom of the body and hangs the loop — advance first, or use for.
  • Add a guard counter while developing a loop whose termination is not obvious.
  • while returns the empty string.

Where this fits

Part of the Tcl curriculum.