Skip to content
VLSI Mentor

Tcl Part I · Module 9 · Loops and Iteration

foreach Over Several Lists at Once

A single foreach can walk several lists side by side, taking one element from each on every pass. It is the natural way to pair up names with values, or signals with widths, when they arrived as separate lists. The feature is easy and the detail that matters is what happens when the lists are not the same length — Tcl does not stop at the end of the shortest one. It keeps going to the end of the longest, supplying empty strings for whatever has run out, which is the opposite of what the equivalent feature does in most other languages and a reliable source of quietly wrong results.

Foundation10 min readTclPart ILoopsforeachParallel iteration

Part I · Module 9 · Chapter 9.2 · foreach Over Several Lists at Once

1. Two lists, one loop

Give foreach another variable-and-list pair:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
set names  {mode runs width}
set values {fast 3 8}

foreach name $names value $values {
    puts "$name = $value"
}
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
mode = fast
runs = 3
width = 8

On each pass name takes the next element of the first list and value the next of the second. You can add as many pairs as you like:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
foreach a {1 2} b {x y} c {P Q} {
    puts "$a $b $c"
}
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
1 x P
2 y Q

Each pair is a variable name followed by a list — bare name, $list, as always.

2. The lists do not have to be the same length

This is the part to remember:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
set names  {a b c}
set values {1 2}

foreach n $names v $values {
    puts "n=$n v='$v'"
}
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
n=a v='1'
n=b v='2'
n=c v=''

Three passes, not two. The loop ran to the end of the longest list, and the exhausted one supplied an empty string.

Three passes of a parallel foreach shown left to right. Pass one binds n to a and v to 1, pass two binds n to b and v to 2, and pass three binds n to c while v is empty because the second list has run out.Pass 1n=a, v=1Pass 2n=b, v=2Pass 3n=c, v empty12
Figure 1 — three passes over two lists of different lengths. The number of passes is set by the longer list, so a third pass happens even though the second list has only two elements. On that pass the variable bound to the shorter list holds an empty string rather than the loop ending, which means the body runs once more than many people expect.

It works the same way round:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
foreach n {a b} v {1 2 3} {
    puts "n='$n' v=$v"
}
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
n='a' v=1
n='b' v=2
n='' v=3

Now it is n that comes back empty. The rule is simply: the longest list decides how many passes there are, and every shorter list is padded.

3. Why this catches people out

Most languages with a parallel-iteration feature truncate. Python's zip stops at the shortest input; so do the equivalents in Ruby and JavaScript. Someone carrying that assumption writes:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
set names  {clk rst data}
set widths {1 1}

foreach n $names w $widths {
    puts "port $n is $w bits"
}
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
port clk is 1 bits
port rst is 1 bits
port data is  bits

The third line is wrong and it did not raise anything. Had Tcl truncated, data would simply have been skipped — also wrong, but visibly short. Padding produces a line that looks like output.

4. Checking first

When the lists are supposed to correspond, say so:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
proc pairup {names values} {
    if {[llength $names] != [llength $values]} {
        error "list lengths differ: [llength $names] names, [llength $values] values"
    }
    set out {}
    foreach n $names v $values {
        lappend out "$n=$v"
    }
    return $out
}

puts [pairup {a b} {1 2}]

if {[catch {pairup {a b c} {1 2}} msg]} {
    puts "rejected: $msg"
}
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
a=1 b=2
rejected: list lengths differ: 3 names, 2 values

Three lines of guard, and a mismatch now names both counts instead of producing a blank field somewhere downstream.

5. When padding is what you want

It is not always a hazard. Padding is genuinely useful when a list is optional:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
set ports    {clk rst data valid}
set comments {"the main clock" "active low"}

foreach p $ports c $comments {
    if {$c eq ""} {
        puts [format "  %-6s" $p]
    } else {
        puts [format "  %-6s # %s" $p $c]
    }
}
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
  clk    # the main clock
  rst    # active low
  data  
  valid

Here the shorter list means "no comment for these", and padding expresses that directly. The deciding question is whether a short list is expected: if yes, pad and handle the empty case; if no, check the lengths and fail.

6. Parallel iteration versus a list of records

If two lists always travel together, consider whether they should have been one list of records in the first place:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
# two parallel lists
set names  {clk rst}
set widths {1 1}

# one list of records — they cannot drift apart
set ports {{clk 1} {rst 1}}
foreach p $ports {
    lassign $p name width
    puts "$name is $width bits"
}
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
clk is 1 bits
rst is 1 bits

The record version cannot have a length mismatch, because there is only one list. Parallel foreach is the right tool when the lists genuinely arrive separately — from two different sources, or as a function's two return values — and not as a substitute for structuring the data.

7. A worked example

Comparing two runs field by field:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
set fields {cycles errors warnings}
set before {12000 0 4}
set after  {11500 2 4}

if {[llength $before] != [llength $after]} {
    error "runs have different numbers of fields"
}

puts [format "%-10s %8s %8s %8s" FIELD BEFORE AFTER CHANGE]
foreach f $fields b $before a $after {
    set delta [expr {$a - $b}]
    puts [format "%-10s %8d %8d %+8d" $f $b $a $delta]
}
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
FIELD        BEFORE    AFTER   CHANGE
cycles        12000    11500     -500
errors            0        2       +2
warnings          4        4       +0

Three lists walked together, one line per field, and a length check at the top so a missing field cannot silently become a zero.

8. Exercises

Exercise 1. How many passes does foreach a {1 2 3} b {x y} {...} make, and what is b on the last one?

Exercise 2. Write a loop printing name: value for {mode runs} and {fast 3}.

Exercise 3. Explain the difference between Tcl's behaviour and Python's zip when the lists differ in length, and which failure is easier to notice.

Exercise 4. Add a guard to this so a mismatch is reported rather than padded:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
foreach n $names w $widths { puts "$n $w" }

Exercise 5. Give one situation where the padding is the behaviour you want.

Exercise 6. Two lists in a script keep drifting out of sync as the code grows. What structural change removes the problem entirely?

Answers

1. Three passes — the longer list decides. On the third, b is the empty string.

2.

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
foreach name {mode runs} value {fast 3} {
    puts "$name: $value"
}
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
mode: fast
runs: 3

3. Tcl runs to the end of the longest list and pads the short ones with empty strings; zip stops at the shortest and drops the extras. Truncation is easier to notice, because the output is visibly short — padding produces a full-length result with a blank field in it, which looks like data.

4.

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
if {[llength $names] != [llength $widths]} {
    error "names and widths differ: [llength $names] vs [llength $widths]"
}
foreach n $names w $widths { puts "$n $w" }

5. When the shorter list is genuinely optional — comments, labels or overrides that only some entries have. The empty value then means "none supplied", which is exactly right.

6. Make them one list of records, so there is only one list to keep in step: {{clk 1} {rst 1}} with lassign in the body. A length mismatch becomes impossible rather than merely checked.

9. Remember

  • foreach a $l1 b $l2 {body} walks several lists in parallel; add as many variable-and-list pairs as you need.
  • The longest list decides the number of passes.
  • Shorter lists are padded with empty strings — Tcl does not truncate the way zip does in other languages.
  • A padded empty value is indistinguishable from a real empty element, so the body cannot detect the mismatch.
  • Check llength before the loop whenever the lists are supposed to correspond and come from different sources.
  • Padding is the right behaviour when a list is optional — treat empty as "none supplied".
  • If two lists always travel together, make them one list of records and the mismatch becomes impossible.

Where this fits

Part of the Tcl curriculum.