Skip to content
VLSI Mentor

Perl Part I · Module 9 · Loops

Termination, Infinite Loops and Bounds

A loop whose condition depends on something outside the script — a file another tool is supposed to write, a lock another process is supposed to release, an operation that is supposed to succeed eventually — has no guarantee of ever finishing. When it does not, the script does not crash and does not print anything; it simply never returns, which in a nightly regression means a job that is still running at nine the next morning with nothing in its log. The fix is one counter and one comparison, and it converts the worst failure mode a script has into an ordinary error message with an exit code. There are two ways to get that counter wrong, and the second one is subtle enough that it makes a perfectly healthy input fail while reporting success.

Intermediate15 min readPerlPart IControl FlowLoops

Part I · Module 9 · Chapter 9.8 · Termination, Infinite Loops and Bounds

1. A Loop Nothing Can End

Azvya Education Pvt. Ltd.VLSI Mentor
unbounded.pl
use strict;
use warnings;

my $ready = 0;          # nothing in this loop will ever set it
my $passes = 0;

until ($ready) {
    $passes++;
    die "ABORT: the condition can never change (pass $passes)\n" if $passes > 5;
}

print "never reached\n";
Azvya Education Pvt. Ltd.VLSI Mentor
Captured output — STDERR
ABORT: the condition can never change (pass 6)
Azvya Education Pvt. Ltd.VLSI Mentor
Measured exit status
255

The die on the third line is mine, added only so that this measurement terminates at all. Delete it and the program does not fail — it runs, producing no output, until something outside it intervenes.

That is the shape worth recognising: $ready is tested by the loop and set by nothing. The real version is rarely that obvious, because the thing meant to change the condition is outside the loop body:

Azvya Education Pvt. Ltd.VLSI Mentor
The realistic version
until (-e $netlist) {
    # waiting for the synthesis job to write it
}

If synthesis crashed, -e $netlist is false forever and this loop never exits.

There are three ways a Perl loop fails to terminate, and all three have been measured in this module:

ShapeWhy it never endsMeasured in
while/until on a condition nothing changesthe test can never flipthis chapter
a C-style for with no incrementthe counter never advances§4 below
redo with a guard the body does not changeredo does not advanceChapter 9.5 §3

A foreach over a fixed list cannot hang, which is one more reason Chapter 9.1 recommends it as the default.

2. The Bound

Azvya Education Pvt. Ltd.VLSI Mentor
bounded.pl
use strict;
use warnings;

my $ready = 0;
my $tries = 0;
my $MAX   = 5;

until ($ready) {
    if (++$tries > $MAX) {
        print STDERR "giving up: not ready after $MAX attempts\n";
        exit 2;
    }
    # a real script would re-check something external here
}

print "ready after $tries attempt(s)\n";
Azvya Education Pvt. Ltd.VLSI Mentor
Captured output — STDERR
giving up: not ready after 5 attempts
Azvya Education Pvt. Ltd.VLSI Mentor
Measured exit status
2

Same loop, same condition that never becomes true. The difference is that this one stops, says why, and reports a status a caller can branch on.

Three choices in that code are deliberate:

  • The message names the bound. "not ready after 5 attempts" tells whoever reads the log both what failed and how hard the script tried. "giving up" alone does not.
  • It goes to STDERR, so it is visible even when STDOUT is redirected into a report file.
  • exit 2, not exit 1. Chapter 8.6 §6 established the three-code taxonomy: 0 pass, 1 a real failure, 2 could not check. A timeout is emphatically the third — the script never found out whether the thing it was waiting for was good or bad.

Every loop whose condition depends on anything outside the script needs this. That includes waiting on a file, a lock, a process, a network mount, or a subroutine whose success is not guaranteed.

3. The Off-by-One

One character changes how many attempts you get.

Azvya Education Pvt. Ltd.VLSI Mentor
prepost.pl
use strict;
use warnings;

for my $form ("pre", "post") {
    my $tries = 0;
    my $body  = 0;
    while (1) {
        $body++;
        if ($form eq "pre") { last if ++$tries > 3 }
        else                { last if $tries++ > 3 }
    }
    printf "  %-4s : body ran %d time(s), counter ended at %d\n", $form, $body, $tries;
}
Azvya Education Pvt. Ltd.VLSI Mentor
Captured output
  pre  : body ran 4 time(s), counter ended at 4
  post : body ran 5 time(s), counter ended at 5

++$tries increments then compares; $tries++ compares then increments. With a bound of 3 the pre-increment form ran the body 4 times and the post-increment form 5.

Neither is wrong — they are different loops. What matters is knowing which one you wrote:

WrittenBody runsMAX means
last if ++$tries > $MAX$MAX + 1attempts after the first
last if $tries++ > $MAX$MAX + 2— rarely what anyone means
last if $tries >= $MAX with $tries++ at the topexactly $MAXtotal attempts

The third form is the one to prefer, because $MAX then means exactly what the word says:

Azvya Education Pvt. Ltd.VLSI Mentor
The form where MAX is the attempt count
my $tries = 0;
while (1) {
    $tries++;
    last if $ready;
    if ($tries >= $MAX) { print STDERR "giving up after $MAX attempts\n"; exit 2 }
}

This matters more than style when the bound is a timeout expressed in real units. A loop meant to wait thirty seconds with a one-second interval and written with $tries++ > 30 waits thirty-two. On a single loop that is irrelevant; in a flow that nests three such waits it is the difference between a sensible timeout and one nobody can account for.

4. A Counter That Never Advances

Azvya Education Pvt. Ltd.VLSI Mentor
noadvance.pl
use strict;
use warnings;

my $passes = 0;

for (my $i = 0; $i < 3; ) {        # note: no $i++
    $passes++;
    die "ABORT: counter never advanced (pass $passes)\n" if $passes > 4;
}

print "done\n";
Azvya Education Pvt. Ltd.VLSI Mentor
Captured output — STDERR
ABORT: counter never advanced (pass 5)
Azvya Education Pvt. Ltd.VLSI Mentor
Measured exit status
255

The third part of the for header is empty, so $i stays 0 and $i < 3 stays true. Perl does not warn — an empty increment clause is legal and occasionally deliberate, because the body may be advancing the counter itself.

The realistic version is not an empty clause but a conditional one:

Azvya Education Pvt. Ltd.VLSI Mentor
The version that looks fine
for (my $i = 0; $i < @rows; ) {
    if (should_keep($rows[$i])) {
        $i++;                      # only advances when kept
    } else {
        splice(@rows, $i, 1);      # removes, so do not advance
    }
}

That is a legitimate and correct idiom — removing an element shifts the rest down, so not advancing is right. It is also one editing mistake away from an infinite loop: make should_keep return false while splice fails to shrink the array and nothing moves.

Chapter 9.1 §3 measured the opposite error on the same shape — a loop that does advance while splicing, visiting only [1 3 5] of five elements. Both failure modes come from the index and the array being able to disagree, which is the argument for rebuilding with grep instead:

Azvya Education Pvt. Ltd.VLSI Mentor
No index, no bound, nothing to desynchronise
@rows = grep { should_keep($_) } @rows;

5. The Counter You Forgot to Reset

This is the one that produces a wrong answer rather than a hang.

Azvya Education Pvt. Ltd.VLSI Mentor
counter-reset.pl
use strict;
use warnings;

my @items = ("a", "b", "c");

print "WITHOUT a reset between items:\n";
my $tries = 0;
for my $item (@items) {
    my $got = 0;
    while (!$got) {
        $tries++;
        if ($tries > 4) { print "  $item: gave up at $tries\n"; last }
        $got = 1 if $tries % 2 == 0;
    }
    printf "  %s: got=%d tries=%d\n", $item, $got, $tries;
}

print "WITH a reset between items:\n";
for my $item (@items) {
    my $t   = 0;
    my $got = 0;
    while (!$got) {
        $t++;
        if ($t > 4) { print "  $item: gave up at $t\n"; last }
        $got = 1 if $t % 2 == 0;
    }
    printf "  %s: got=%d tries=%d\n", $item, $got, $t;
}
Azvya Education Pvt. Ltd.VLSI Mentor
Captured output
WITHOUT a reset between items:
  a: got=1 tries=2
  b: got=1 tries=4
  c: gave up at 5
  c: got=0 tries=5
WITH a reset between items:
  a: got=1 tries=2
  b: got=1 tries=2
  c: got=1 tries=2
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet

Item c failed, and it is identical to a and b. All three need two attempts. With the counter declared outside the item loop, a consumed two of the budget, b consumed two more, and c ran out before it could finish — so the loop reported got=0 for an item that was never actually a problem.

6. Why This Matters in Engineering Work

A hung script is the worst outcome available. A crash has a message and a stack; a wrong answer has a number you can question. A hang produces an empty log and a job that holds a licence and a queue slot until someone notices, which in a nightly flow is the next morning.

Every wait in an EDA flow is on something that can fail. Synthesis writes a netlist, a licence server grants a token, an NFS mount appears, a job queue reports completion. Each of those can not-happen, and a loop waiting on one without a bound converts an upstream failure into a hang in your script rather than an error from theirs.

Exit 2 routes the problem correctly. A timeout means the script could not determine the answer, which is Chapter 8.6's UNKNOWN. Reporting it as 1 sends a flow problem to the block owner.

And the shared-counter bug is a false failure, which is expensive in a different way. It makes a clean block look broken, on a run that exits 0, with an answer that changes if you reorder the input. Engineering time spent investigating a block that was never wrong is the most wasteful kind.

7. Common Mistakes

A while or until whose condition nothing in the loop can change. It never ends and prints nothing.

No attempt bound on a wait for anything external. Add a counter and a maximum.

exit 1 on a timeout. Use 2 — the script did not determine an answer.

last if $tries++ > $MAX. The body runs $MAX + 2 times. Prefer incrementing at the top and testing >= $MAX.

A C-style for with a conditional increment that can fail to fire. Rebuild with grep instead.

A retry counter declared outside the per-item loop. Later items inherit the spent budget and fail while identical earlier items passed.

Reporting "failed" without the attempt count. A consumed-budget bug is invisible without it.

A last that skips the per-item recording. The item vanishes from the report instead of appearing as failed.

8. Exercise

Exercise 1 — the unbounded loop. Write until ($ready) where nothing sets $ready, with a die bound at 5 passes so it terminates. Record the message and the exit status. Then remove the die, run it with a timeout you control, and describe what the terminal shows.

Exercise 2 — the bound. Add an attempt counter and exit 2 with a message naming the maximum. Confirm the message reaches STDERR and the exit status is 2.

Exercise 3 — the off-by-one. Run §3's comparison. Record how many times the body ran for each form. Then write the third form from §3's table and confirm the body runs exactly $MAX times.

Exercise 4 — units. Set $MAX = 30 in all three forms and record the body count for each. Say which one you would use for a thirty-second timeout and why.

Exercise 5 — no advance. Write a C-style for with an empty increment clause and a die bound. Record the abort. Then write the conditional-increment version from §4 and make it loop forever by having the removal not shrink the array.

Exercise 6 — the shared counter. Reproduce §5 exactly. Record both blocks of output. Then reorder @items to ("c", "b", "a") and record which item fails now.

Exercise 7 — the fix and the report. Fix Exercise 6 with a per-item counter, then change the report line to include the attempt count for every item, passing or failing. Show the output.

Exercise 8 — the missing record. In the failing version, move the per-item printf so that a last skips it. Record what the report shows for item c and say why that is worse than reporting a failure.

9. Summary

  • A loop whose condition nothing can change never ends, prints nothing, and is the worst failure a script has. A foreach over a fixed list cannot do this.
  • Three shapes can hang: a while/until on a static condition, a C-style for that does not advance, and an unguarded redo.
  • Bound every wait on anything external with an attempt counter, a message naming the maximum, and an exit status.
  • Send the message to STDERR and exit 2 — a timeout is "could not check", not "failed".
  • ++$tries > $MAX runs the body $MAX + 1 times; $tries++ > $MAX runs it $MAX + 2. Measured: 4 and 5 passes for a bound of 3.
  • Prefer incrementing at the top and testing >= $MAX, so $MAX is the attempt count.
  • A conditional increment in a C-style for can fail to fire. Rebuilding with grep removes the index entirely.
  • Declare the retry counter inside the per-item loop. Shared, it made an identical third item fail — measured, with exit 0 and empty STDERR, and the result changes if you reorder the input.
  • Report the attempt count alongside the outcome, and make sure a bailed item still reaches the code that records it.

Chapter 9.9 closes the module with what loops are mostly for: the three accumulation patterns that between them cover almost every loop you will write.

Where this fits

Part of the Perl curriculum.