41 min read

> "Computers are good at following instructions, but even better at repetition."

Chapter 6: Loops and Iteration: REPEAT, WHILE, and FOR

"Computers are good at following instructions, but even better at repetition." — Anonymous

In Chapter 5, we learned how to make decisions — how to steer our programs down one path or another using IF, CASE, and Boolean logic. That was a critical step: without branching, every program would execute the same instructions in the same order, every single time. But decision-making alone still leaves us with a fundamental limitation. Consider a program that reads exam scores and computes an average. With only what we know so far, we would need to write a separate ReadLn statement for every single score. Ten students? Ten ReadLn calls. A hundred students? A hundred. A thousand? You see the problem.

What we need is the ability to say: do this action repeatedly — perhaps a fixed number of times, perhaps until some condition changes, perhaps forever (or at least until the user decides to quit). This is iteration, and it is arguably the computer's single greatest superpower. A human being grows tired, makes mistakes, loses focus. A computer will happily execute the same block of code a billion times without complaint, without error, and without asking for a coffee break.

Pascal gives us three loop constructs, each elegantly suited to different situations:

  • REPEAT..UNTIL — do something, then check whether to stop
  • WHILE..DO — check first, then (maybe) do something
  • FOR..TO (and FOR..DOWNTO) — do something a specific number of times

By the end of this chapter, you will be able to write all three, know when to reach for each one, nest them inside each other, debug them when they misbehave, and use them to build real programs — including the first skeleton of our Crypts of Pascalia game loop and a major upgrade to the PennyWise expense tracker.

Prerequisites: You should be comfortable with Boolean expressions and IF statements (Chapter 5). You will also need input/output skills from Chapter 4.


6.1 Why Loops Matter

Let us start with a motivating problem. Suppose we want to print the numbers 1 through 10, each on its own line. With what we know so far, we would write:

WriteLn(1);
WriteLn(2);
WriteLn(3);
WriteLn(4);
WriteLn(5);
WriteLn(6);
WriteLn(7);
WriteLn(8);
WriteLn(9);
WriteLn(10);

That works. It is also tedious, error-prone, and completely inflexible. What if the user wants 1 to 1000? What if they want to decide at runtime how many numbers to print? We cannot hard-code a million WriteLn statements — and even if we could, the absurdity of the approach is self-evident.

A loop solves this in three lines:

for i := 1 to 10 do
  WriteLn(i);

Change the 10 to 1000 — or to a variable the user supplies — and the code handles it without modification. That is the power of iteration: you write the instruction once, and the computer carries it out as many times as needed.

This is not a minor convenience; it is a fundamental capability. Without loops, computers would be little more than expensive calculators. With loops, they become engines of automation. Consider how loops appear in virtually every domain of software:

  • Games run a continuous loop: read input, update the game state, render the frame, and repeat — sixty times per second.
  • Data processing iterates over records: read a line from a file, parse it, transform it, write the result, and move to the next line.
  • User interfaces loop waiting for events: a click, a keystroke, a window resize, a timer expiration.
  • Web servers loop forever, listening for incoming connections, processing each request, and returning to the listening state.
  • Scientific simulations iterate through time steps, each step computing new values based on the previous state.
  • Search engines loop through billions of indexed pages, scoring and ranking each one against the user's query.

In the spirit of Theme 5 (Algorithms + Data Structures = Programs), loops are the engine that drives algorithms. Searching, sorting, counting, accumulating, filtering — virtually every algorithm you will ever encounter rests on some form of iteration. Remove loops from a programmer's toolkit, and algorithms become impossible.

And in the spirit of Theme 6 (Simplicity), Pascal's three loop constructs are clean, distinct, and easy to reason about. There is no confusion about which one to use once you understand the differences, and no hidden complexity lurking beneath the surface. Each construct has a clear purpose, and together they cover every iterative situation you will encounter.

In the spirit of Theme 3 (Living Language), the loop constructs we study in this chapter date back to Pascal's creation in 1970. They have influenced virtually every programming language that followed. C, Java, Python, JavaScript — all have their own versions of for, while, and do..while (Pascal's repeat..until). Master these concepts here, and you will recognize them everywhere.

Let us begin with the most forgiving of the three.


6.2 The REPEAT..UNTIL Loop

Syntax

repeat
  { one or more statements }
until BooleanExpression;

The REPEAT..UNTIL loop executes the statements inside its body at least once, then evaluates the Boolean expression. If the expression is True, the loop stops. If it is False, the loop goes back to the top and executes the body again.

Key insight: UNTIL means "stop when true." The loop continues as long as the condition is False.

This is called a post-test loop (or exit-controlled loop) because the test happens after the body executes. The guarantee that the body runs at least once makes REPEAT..UNTIL the natural choice whenever you need to do something before you can possibly evaluate the stopping condition.

Think about it this way: if you are asking the user a question and waiting for a valid answer, you must ask the question at least once before you can check the answer. You cannot check an answer that has not been given yet. This is precisely the situation REPEAT..UNTIL was designed for.

A Simple Example

program RepeatDemo;
var
  Number: Integer;
begin
  repeat
    Write('Enter a positive number: ');
    ReadLn(Number);
    if Number <= 0 then
      WriteLn('That is not positive. Try again.');
  until Number > 0;

  WriteLn('You entered: ', Number);
end.

Walk through this carefully, step by step:

  1. The program prints the prompt "Enter a positive number: " and reads a number from the keyboard. This must happen before we can check whether the number is positive — we have no number to check yet! This is the fundamental reason we use REPEAT..UNTIL here.
  2. Suppose the user enters -3. The body has finished executing, so now we evaluate the condition: Number > 0 is False (because -3 is not greater than 0). Since the condition is False, we go back to the top and execute the body again.
  3. The program prints the prompt again and reads another number. Suppose the user enters 0. Again, Number > 0 is False, so we loop again.
  4. This time the user enters 7. The condition Number > 0 is True. The loop terminates, and execution continues with the WriteLn statement after the loop.

Notice that REPEAT..UNTIL does not require begin..end around its body. The keywords repeat and until themselves serve as the delimiters — they are like built-in begin and end. You can place as many statements between them as you like, and they are all considered part of the loop body. This is a syntactic convenience that the other two loop constructs do not share.

The Input Validation Pattern

The example above demonstrates one of the most common patterns in programming: input validation. The user might enter bad data — a negative number when you need a positive one, a letter when you need a digit, a value outside the acceptable range. We need to keep asking until we get something acceptable.

REPEAT..UNTIL is perfect for input validation because we must read input at least once before we can validate it. Here is a more robust example that validates a range:

repeat
  Write('Enter your age (1-120): ');
  ReadLn(Age);
  if (Age < 1) or (Age > 120) then
    WriteLn('Invalid age. Please enter a value between 1 and 120.');
until (Age >= 1) and (Age <= 120);

This is compact, readable, and correct. A WHILE loop would work too, but we would need to initialize Age to some dummy value first (like 0 or -1) to make the initial condition test work — an awkward workaround that REPEAT..UNTIL avoids entirely. Why introduce a fake value when the language gives us a construct that naturally handles the situation?

Let us try another validation example. Suppose we want the user to enter exactly 'Y' or 'N':

repeat
  Write('Continue? (Y/N): ');
  ReadLn(Response);
  if (Response <> 'Y') and (Response <> 'y') and
     (Response <> 'N') and (Response <> 'n') then
    WriteLn('Please enter Y or N.');
until (Response = 'Y') or (Response = 'y') or
      (Response = 'N') or (Response = 'n');

Yes, the condition is verbose — we will learn more elegant ways to handle this in later chapters (using sets and functions). But the loop structure itself is clean: prompt, read, validate, repeat if necessary.

The Menu Loop Pattern

Another classic use of REPEAT..UNTIL is the menu-driven program:

repeat
  WriteLn('--- Main Menu ---');
  WriteLn('1. New game');
  WriteLn('2. Load game');
  WriteLn('3. Options');
  WriteLn('4. Quit');
  Write('Your choice: ');
  ReadLn(Choice);

  case Choice of
    1: StartNewGame;
    2: LoadGame;
    3: ShowOptions;
    4: WriteLn('Goodbye!');
  else
    WriteLn('Invalid choice. Please enter 1-4.');
  end;
until Choice = 4;

The menu displays at least once (you have to show the menu before the user can pick from it), the user makes a choice, the program acts on it, and the loop repeats until the user chooses to quit. This is the pattern we will use for PennyWise at the end of this chapter, and it is one of the most commonly used loop structures in interactive programs.

Counting with REPEAT..UNTIL

While REPEAT..UNTIL is not primarily designed for counting, you can certainly use it that way:

Counter := 1;
repeat
  WriteLn('Iteration ', Counter);
  Counter := Counter + 1;
until Counter > 10;

This prints iterations 1 through 10. The body executes first (printing and incrementing), then the condition Counter > 10 is checked. When Counter reaches 11, the condition is True and the loop exits.

However, for counted iteration, the FOR loop (Section 6.4) is almost always a better choice. It communicates intent more clearly — "I want exactly 10 iterations" — and manages the counter variable automatically, eliminating an entire class of bugs. Use REPEAT..UNTIL when the number of iterations is unknown and you need at least one execution.

Execution Flow Diagram

To visualize how REPEAT..UNTIL works, think of it as a flowchart:

     [Enter loop]
          |
          v
   +-------------+
   | Execute body |<-----+
   +-------------+       |
          |               |
          v               |
   +----------------+     |
   | Test condition |     |
   +----------------+     |
     |            |       |
   True         False ----+
     |
     v
  [Continue after loop]

The body always executes before the test. If the test is False, we loop back. If True, we exit.


6.3 The WHILE..DO Loop

Syntax

while BooleanExpression do
  Statement;

Or, with a compound statement (which you will need almost every time):

while BooleanExpression do
begin
  { one or more statements }
end;

The WHILE..DO loop evaluates the Boolean expression first. If it is True, the body executes. Then the expression is tested again. This continues until the expression becomes False, at which point execution jumps to the statement after the loop.

Key insight: WHILE means "continue when true." The loop runs as long as the condition holds.

This is a pre-test loop (or entry-controlled loop). The critical difference from REPEAT..UNTIL: the body might never execute if the condition is False from the start. This is not a weakness — it is a feature. There are many situations where zero iterations is the correct behavior.

Execution Flow Diagram

     [Enter loop]
          |
          v
   +----------------+
   | Test condition  |<-----+
   +----------------+       |
     |            |          |
   True         False        |
     |            |          |
     v            v          |
   +----------+  [Exit]     |
   |  Execute  |             |
   |  body     |-------------+
   +----------+

Notice the key difference from REPEAT..UNTIL: the test comes before the body. If the condition starts as False, the body is skipped entirely.

A Simple Example

program WhileDemo;
var
  Count: Integer;
begin
  Count := 1;
  while Count <= 5 do
  begin
    WriteLn('Count is ', Count);
    Count := Count + 1;
  end;
  WriteLn('Done! Count is now ', Count);
end.

Output:

Count is 1
Count is 2
Count is 3
Count is 4
Count is 5
Done! Count is now 6

Let us trace through this execution carefully:

  1. Count starts at 1. The condition Count <= 5 is True, so the body executes: we print "Count is 1" and increment Count to 2.
  2. Back at the top: Count <= 5 is True (2 <= 5), so the body executes again. Print, increment to 3.
  3. This continues for Count values of 3, 4, and 5.
  4. After printing "Count is 5" and incrementing to 6: Count <= 5 is False (6 <= 5 is false). The loop exits.
  5. The WriteLn after the loop prints "Done! Count is now 6".

The WHILE vs. REPEAT: The Zero-Iteration Case

Consider reading lines from a file. If the file is empty, we should do nothing — there is nothing to read:

while not Eof(InputFile) do
begin
  ReadLn(InputFile, Line);
  ProcessLine(Line);
end;

If the file is empty, Eof(InputFile) is True immediately, so not Eof(InputFile) is False, and the loop body never executes. This is exactly correct — there is nothing to process. No error, no special case, no workaround. The WHILE loop handles it naturally.

A REPEAT..UNTIL loop would try to read from an empty file on its first iteration, potentially causing an error or processing garbage data. This is the classic scenario where WHILE..DO is the right choice: when zero iterations is a legitimate possibility.

Another zero-iteration scenario: suppose you are processing a list of students who failed an exam. If everyone passed, the list is empty, and the correct behavior is to do nothing. A WHILE loop handles this naturally; a REPEAT..UNTIL would need special-case code to avoid processing a non-existent first student.

The Sentinel Loop Pattern

A sentinel is a special value that signals the end of input. It is like a stop sign in a stream of data: everything before it is real data; the sentinel itself means "I am done." For example, entering -1 to indicate "no more grades":

Write('Enter a grade (-1 to stop): ');
ReadLn(Grade);

while Grade <> -1 do
begin
  Total := Total + Grade;
  Count := Count + 1;
  Write('Enter a grade (-1 to stop): ');
  ReadLn(Grade);
end;

if Count > 0 then
  WriteLn('Average: ', Total / Count :0:2)
else
  WriteLn('No grades entered.');

Notice the structure carefully: we read before the loop (the priming read), then read again at the bottom of the loop body (the update read). This ensures we always check the sentinel before processing. If the user enters -1 immediately (the zero-iteration case), the body never executes, and we correctly report "No grades entered."

The priming read is essential. Without it, the variable Grade would contain whatever garbage value happened to be in memory, and the loop condition check would be meaningless. This is a common pattern you will see again and again:

Priming read
WHILE (not sentinel) DO
BEGIN
  Process the data
  Update read
END

Common mistake: Forgetting the priming read. If you do not read a value before the WHILE test, the variable might contain garbage, leading to unpredictable behavior. Equally common: forgetting the update read at the bottom of the loop, which causes an infinite loop (the variable never changes).

The Choice of Sentinel

The sentinel value must be something that could never appear as legitimate data. If you are reading positive test scores, -1 is a safe sentinel (no test score is negative). If you are reading temperatures, -1 could be a real temperature, so you might use -999 instead, or use a character sentinel ("Enter Q to quit").

Choosing a poor sentinel — one that could be confused with real data — is a subtle bug that may not manifest until unusual data appears. Think carefully about your sentinel before coding the loop.

Important: Single Statement vs. Compound

Unlike REPEAT..UNTIL, the WHILE..DO construct controls only a single statement by default. If you need multiple statements in the body (and you almost always do), you must wrap them in begin..end:

{ WRONG — only the WriteLn is in the loop! }
i := 1;
while i <= 10 do
  WriteLn(i);
  i := i + 1;  { This is NOT inside the loop! }

The indentation is misleading. Pascal does not care about indentation — it uses begin..end to determine block structure. In this code, only WriteLn(i) is the body of the WHILE loop. The line i := i + 1 is after the loop. Since i never changes, the condition i <= 10 is always True, and the loop prints 1 forever. This is an infinite loop, and it is one of the most common beginner mistakes in Pascal.

The fix is straightforward:

{ RIGHT }
i := 1;
while i <= 10 do
begin
  WriteLn(i);
  i := i + 1;
end;

Defensive habit: Always write begin..end in your WHILE loops, even if the body is currently a single statement. This protects you when you later add a second statement — a change that is almost inevitable as your programs grow.

A More Realistic Example: Password Validation

Here is a WHILE loop used for repeated processing with a flag-controlled termination:

MaxAttempts := 3;
AttemptsLeft := MaxAttempts;
Authenticated := False;

while (AttemptsLeft > 0) and (not Authenticated) do
begin
  Write('Enter password: ');
  ReadLn(Password);
  AttemptsLeft := AttemptsLeft - 1;

  if Password = SecretPassword then
    Authenticated := True
  else
  begin
    WriteLn('Incorrect password.');
    if AttemptsLeft > 0 then
      WriteLn('You have ', AttemptsLeft, ' attempt(s) remaining.');
  end;
end;

if Authenticated then
  WriteLn('Access granted. Welcome!')
else
  WriteLn('Too many failed attempts. Account locked.');

This loop has two exit conditions combined with and: we stop when we run out of attempts OR when the user authenticates. The WHILE condition continues only when both sub-conditions are met (attempts remaining AND not yet authenticated). When either fails, the loop exits.


6.4 The FOR Loop

Syntax

for Variable := StartValue to EndValue do
  Statement;

for Variable := StartValue downto EndValue do
  Statement;

The FOR loop is designed for counted iteration — when you know (or can compute) exactly how many times the loop should run. The loop variable is automatically initialized, incremented (or decremented), and tested. You do not manage any of this yourself, which eliminates an entire class of bugs related to forgetting to initialize, forgetting to increment, or incrementing by the wrong amount.

The FOR loop is the workhorse of Pascal programming. When the number of iterations is known, always prefer FOR over WHILE or REPEAT. It is cleaner, safer, and communicates your intent more clearly.

FOR..TO (Counting Up)

for i := 1 to 5 do
  WriteLn('Iteration ', i);

Output:

Iteration 1
Iteration 2
Iteration 3
Iteration 4
Iteration 5

The loop variable i takes on each value from 1 to 5 inclusive. The body executes once for each value. After the body executes with i = 5, the loop terminates. The variable i advances by exactly 1 each iteration — you cannot change the step size. (If you need a step size other than 1, use a WHILE loop or compute the value from the loop variable, as we will see shortly.)

FOR..DOWNTO (Counting Down)

for i := 5 downto 1 do
  WriteLn('Countdown: ', i);
WriteLn('Liftoff!');

Output:

Countdown: 5
Countdown: 4
Countdown: 3
Countdown: 2
Countdown: 1
Liftoff!

The DOWNTO variant decrements the loop variable by 1 each iteration. Use it whenever you need to count backwards.

How Many Iterations?

For FOR i := A to B do, the body executes B - A + 1 times (if B >= A). For FOR i := A downto B do, it executes A - B + 1 times (if A >= B). If the count would be zero or negative, the body simply never executes — no error occurs.

Some examples:

Loop Iterations
for i := 1 to 10 do 10
for i := 0 to 9 do 10
for i := 5 to 5 do 1
for i := 10 to 5 do 0 (body skipped)
for i := 1 downto 5 do 0 (body skipped)
for i := 100 downto 1 do 100

Rules and Restrictions

The FOR loop in Pascal has several rules that differ from C-style languages. Understanding these rules is essential — they are not arbitrary restrictions but deliberate design decisions that make FOR loops predictable and safe:

  1. The loop variable must be ordinal. It can be Integer, Char, Boolean, or an enumerated type — but not Real. This makes sense because the loop variable advances by exactly 1 (or -1) each iteration, and fractional increments are not defined for ordinal types. If you need to iterate through real-valued steps (like 0.0, 0.1, 0.2, ...), use a FOR loop over integers and compute the real value: pascal for i := 0 to 10 do begin x := i * 0.1; { x takes values 0.0, 0.1, 0.2, ..., 1.0 } WriteLn(x:0:1); end;

  2. The loop variable must be local. In Free Pascal (and standard Pascal), the loop variable should be declared in the same block as the loop. This prevents side effects and makes the loop self-contained.

  3. Do not modify the loop variable inside the body. Assigning to the loop variable within the loop body is either forbidden or produces undefined behavior, depending on the compiler. Pascal intentionally restricts this to keep FOR loops predictable. The compiler guarantees that the loop variable advances by exactly 1 each iteration — if you could change it mid-iteration, that guarantee would be broken.

  4. The loop variable is undefined after the loop. The Pascal standard does not guarantee the value of the loop variable after the loop finishes. Some compilers leave it at the final value; some leave it at one past the final value; others do not specify. Never rely on the loop variable's value after the loop. If you need to know where the loop stopped, use a separate variable: pascal LastProcessed := 0; for i := 1 to N do begin ProcessItem(i); LastProcessed := i; end; WriteLn('Last processed: ', LastProcessed);

  5. The bounds are evaluated once. The start and end values are computed before the loop begins. Changing a variable used in the bounds inside the loop body does not affect how many times the loop runs. This is another safety feature: once the loop starts, its iteration count is fixed. pascal N := 5; for i := 1 to N do begin N := 100; { This does NOT change the loop count } WriteLn(i); end; { Prints 1, 2, 3, 4, 5 — not 1 through 100 }

  6. Zero iterations are possible. If StartValue > EndValue in a FOR..TO loop (or StartValue < EndValue in FOR..DOWNTO), the body never executes. This is consistent behavior, not an error.

FOR with Char

Because Char is an ordinal type, you can iterate over characters:

for ch := 'A' to 'Z' do
  Write(ch, ' ');
WriteLn;

Output: A B C D E F G H I J K L M N O P Q R S T U V W X Y Z

This is elegant and concise. Try writing that without a loop — you would need 26 Write statements. And if you wanted lowercase too:

for ch := 'a' to 'z' do
  Write(ch);
WriteLn;

You can even use FOR with enumerated types. If you defined type Season = (Spring, Summer, Autumn, Winter), you could write for s := Spring to Winter do — but we have not covered enumerated types yet (that is Chapter 7 material).

FOR with Compound Statements

Like WHILE, a FOR loop controls a single statement by default. Use begin..end for multiple statements:

for i := 1 to 10 do
begin
  Square := i * i;
  Cube := i * i * i;
  WriteLn(i:4, Square:8, Cube:12);
end;

The same defensive-coding advice applies here: always use begin..end if there is any chance you will add more statements later.

Stepping by Values Other Than 1

Pascal's FOR loop always steps by exactly 1 (or -1 for DOWNTO). If you need a different step size — say, counting by 5s or by 10s — there are two approaches:

Approach 1: Compute from the loop variable.

{ Print 0, 5, 10, 15, ..., 100 }
for i := 0 to 20 do
  WriteLn(i * 5);

Approach 2: Use a WHILE loop.

{ Print 0, 5, 10, 15, ..., 100 }
i := 0;
while i <= 100 do
begin
  WriteLn(i);
  i := i + 5;
end;

Approach 1 is generally preferred when the computation is simple, because it retains the clarity and safety of the FOR loop. Approach 2 is necessary when the stepping logic is more complex.

When to Use FOR

Use FOR when: - You know the number of iterations before the loop starts (or can compute it). - You are iterating over a range of integers, characters, or enumeration values. - You want the compiler to manage the loop variable for you. - You do not need to exit the loop early based on a complex condition (though Break can handle this — see Section 6.7).

Do not use FOR when: - The number of iterations depends on user input that arrives during the loop (use REPEAT or WHILE). - You need the loop to possibly execute zero times based on a dynamic condition (use WHILE). - You need at least one execution before testing (use REPEAT..UNTIL). - You need a step size other than 1 and the computation from the loop variable is awkward (use WHILE).


6.5 Choosing the Right Loop

One of the most common questions beginners ask is: "Which loop should I use?" This is a good question, and the answer is not "it does not matter." While all three constructs can theoretically accomplish the same tasks (they are all computationally equivalent), choosing the right one makes your code clearer, more correct, and easier to maintain.

The Three Questions

Ask yourself these questions in order:

  1. Do I know how many times the loop should run? - Yes -> Use FOR. It is the cleanest, safest construct for counted iteration. - No -> Go to question 2.

  2. Must the loop body execute at least once? - Yes -> Use REPEAT..UNTIL. It guarantees at least one execution. - No or Not sure -> Go to question 3.

  3. Might the loop need to execute zero times? - Yes -> Use WHILE..DO. It naturally handles the zero-iteration case. - Not sure -> Default to WHILE..DO. It is safer — if zero iterations happen unexpectedly, the program handles it gracefully rather than processing garbage data.

Comparison Table

Feature REPEAT..UNTIL WHILE..DO FOR
Test location After body (post-test) Before body (pre-test) Before body (pre-test)
Minimum executions 1 0 0
Condition sense Stops when True Continues when True N/A (counted)
begin..end needed? No Yes (for multiple statements) Yes (for multiple statements)
Loop variable managed? No (you manage it) No (you manage it) Yes (automatic)
Best for Menus, input validation, "do then check" File processing, sentinel values, "check then do" Counted iteration, ranges, array processing

The UNTIL vs. WHILE Condition Trap

One of the most subtle sources of bugs when switching between REPEAT..UNTIL and WHILE..DO is the inverted condition logic. These two constructs use opposite senses:

  • REPEAT..UNTIL stops when the condition is True.
  • WHILE..DO continues when the condition is True.

This means the conditions are logical negations of each other. If you translate a REPEAT loop to a WHILE loop (or vice versa), you must invert the condition:

REPEAT version WHILE version
until Count > 10 while Count <= 10
until Answer = 'Q' while Answer <> 'Q'
until (X = 0) or (Y = 0) while (X <> 0) and (Y <> 0)

Notice the third row: by De Morgan's Law, negating an or produces an and, and the individual conditions are also negated. Getting this wrong is a very common bug.

A Practical Guideline

In practice, experienced Pascal programmers tend to reach for these constructs in roughly this order of frequency:

  1. FOR — when the count is known. Clean, safe, hard to get wrong. The compiler does the bookkeeping.
  2. WHILE — when the count is unknown and zero iterations are possible. The safer of the two indefinite loops.
  3. REPEAT — when you need at least one iteration. Perfect for menus, input validation, and game loops.

All three are important, and you will use all three regularly. The key is matching the construct to the problem's requirements — not forcing your preferred construct into every situation.


6.6 Nested Loops

A nested loop is a loop inside another loop. The inner loop completes all of its iterations for each single iteration of the outer loop. This is a fundamental technique for working with two-dimensional structures, generating patterns, and implementing many algorithms.

The Multiplication Table

The classic example of nested loops is the multiplication table:

program MultTable;
var
  Row, Col: Integer;
begin
  { Print header }
  Write('     |');
  for Col := 1 to 10 do
    Write(Col:5);
  WriteLn;
  Write('-----+');
  for Col := 1 to 10 do
    Write('-----');
  WriteLn;

  { Print table body }
  for Row := 1 to 10 do
  begin
    Write(Row:4, ' |');
    for Col := 1 to 10 do
      Write((Row * Col):5);
    WriteLn;
  end;
end.

For each value of Row (1 through 10), the inner loop runs through all values of Col (1 through 10). That gives us 10 x 10 = 100 multiplications, organized in a neat grid.

The outer loop controls the rows; the inner loop controls the columns within each row. This is a pattern you will see repeatedly when working with tables, grids, matrices, and any two-dimensional data structure.

Pattern Generation: Right Triangle

Nested loops excel at generating visual patterns. Here is a right triangle of asterisks:

for Row := 1 to 5 do
begin
  for Col := 1 to Row do
    Write('* ');
  WriteLn;
end;

Output:

*
* *
* * *
* * * *
* * * * *

Notice that the inner loop's upper bound (Row) depends on the outer loop's variable. This is perfectly legal and very common. For Row 1, the inner loop runs once. For Row 2, it runs twice. For Row 5, it runs five times. The total number of asterisks printed is 1 + 2 + 3 + 4 + 5 = 15.

Centered Pyramid

A more sophisticated pattern requires three loops per row — one for leading spaces, one for stars, and one for the newline:

N := 5;
for Row := 1 to N do
begin
  for j := 1 to (N - Row) do
    Write(' ');
  for j := 1 to (2 * Row - 1) do
    Write('*');
  WriteLn;
end;

Output:

    *
   ***
  *****
 *******
*********

Working through the math for Row 3: leading spaces = 5 - 3 = 2, stars = 2(3) - 1 = 5. That produces *****, which is correct.

Counting Iterations in Nested Loops

When reasoning about nested loops, multiply the iteration counts. If the outer loop runs m times and the inner loop runs n times, the innermost statement executes m x n times. This matters for performance:

  • A single loop over 1,000 elements: 1,000 iterations.
  • A doubly nested loop over 1,000 elements: 1,000,000 iterations.
  • A triply nested loop over 1,000 elements: 1,000,000,000 iterations.

That last number — one billion — might take several seconds or even minutes to execute. Be mindful of this when processing large data sets. Deeply nested loops can transform a fast program into a slow one very quickly.

Mixing Loop Types

You can nest loops of different types. A FOR inside a WHILE inside a REPEAT is perfectly valid. The choice of loop type at each level should be governed by the same criteria we discussed in Section 6.5. For example, a game might use:

  • REPEAT..UNTIL for the outer game loop (play at least one game)
  • WHILE..DO for processing a list of enemies (might be empty)
  • FOR for iterating over the player's inventory (fixed size)

A Note on Depth and Readability

You can nest loops to any depth. However, deeply nested loops (three or more levels) often signal that the inner logic should be extracted into a separate procedure. We will explore procedures in Chapter 8, which will give us tools to tame complexity. As a rule of thumb: if you cannot explain what a block of nested loops does in one sentence, it is probably too complex and should be refactored.


6.7 Loop Control: Break and Continue

Standard Pascal (as defined by Wirth and the ISO standard) does not include Break or Continue. However, Free Pascal — and virtually all modern Pascal compilers — provides them as extensions. They are widely used in practice, though experienced programmers use them judiciously.

Break

Break immediately exits the innermost enclosing loop. Execution continues with the first statement after that loop.

for i := 1 to 100 do
begin
  if Data[i] = Target then
  begin
    WriteLn('Found at position ', i);
    Break;
  end;
end;

Without Break, the loop would continue searching even after finding the target — wasting time on unnecessary iterations. Break lets us stop as soon as we have what we need. This is particularly valuable when searching through large data sets.

In nested loops, Break only exits the innermost loop:

for i := 1 to 10 do
begin
  for j := 1 to 10 do
  begin
    if SomeCondition(i, j) then
      Break;  { Exits the j-loop, NOT the i-loop }
    WriteLn(i, ' ', j);
  end;
  { Execution continues here after Break }
end;

If you need to exit multiple levels of loops, you will need a flag variable or a goto statement (which we strongly discourage) — or, better yet, move the nested loops into a procedure and use Exit to return from it.

Continue

Continue skips the rest of the current iteration and jumps to the next iteration. For FOR loops, the loop variable is incremented. For WHILE and REPEAT loops, execution jumps to the condition test.

for i := 1 to 100 do
begin
  if Scores[i] < 0 then
    Continue;  { Skip invalid scores }
  Total := Total + Scores[i];
  Count := Count + 1;
end;

Without Continue, we would wrap the processing code in an if statement:

for i := 1 to 100 do
begin
  if Scores[i] >= 0 then  { Only process valid scores }
  begin
    Total := Total + Scores[i];
    Count := Count + 1;
  end;
end;

Both versions are correct. The Continue version avoids an extra level of nesting, which can improve readability when the processing code is long. The if version is arguably clearer for short code blocks. Use whichever you find more readable in context.

Use Sparingly

Both Break and Continue can make loop logic harder to follow, especially when multiple Break or Continue statements appear in a single loop. Before reaching for them, consider whether restructuring the loop or adjusting the condition would produce clearer code.

Acceptable uses: - Early exit from a search loop (Break). - Skipping invalid data in a processing loop (Continue). - Exiting a FOR loop when an error condition is detected (Break).

Questionable uses: - Using Break as the primary termination mechanism for a FOR loop. If the loop might terminate early most of the time, a WHILE loop with a proper condition is clearer. - Multiple Break or Continue statements in a single loop — this can obscure the flow and make the loop hard to reason about. - Using Continue to skip over code that could be handled by a simple if statement.

The theme of simplicity (Theme 6) applies here. If Break or Continue makes your code simpler and easier to understand, use it. If it makes the logic convoluted, restructure instead. When in doubt, write the version without Break/Continue first, then consider whether the alternative is clearer.


6.8 Common Loop Patterns

Over decades of programming, certain loop patterns have emerged again and again. These are not just Pascal patterns — they appear in every imperative programming language. Learning to recognize and name these patterns will accelerate your programming and make your code more idiomatic. When you see a problem, you will think "that is an accumulator pattern" or "that is a search pattern," and the code will practically write itself.

Pattern 1: Counter

Counting how many items satisfy a condition:

PassCount := 0;
for i := 1 to NumStudents do
  if Score[i] >= 60 then
    PassCount := PassCount + 1;
WriteLn('Passing students: ', PassCount);

The counter variable is initialized to 0 before the loop and incremented inside the loop when the condition is met. After the loop, it holds the final count.

Pattern 2: Accumulator

Summing or accumulating a result across iterations:

Total := 0;
for i := 1 to NumItems do
  Total := Total + Price[i];
WriteLn('Total cost: $', Total:0:2);

The accumulator is a generalization of the counter. Instead of adding 1 each time, you add the current value. The pattern extends naturally to products (initialize to 1, multiply each iteration), running averages, and string concatenation.

Here is a product accumulator computing factorial:

Factorial := 1;
for i := 2 to N do
  Factorial := Factorial * i;

Pattern 3: Sentinel Loop

Processing input until a special value appears:

WriteLn('Enter numbers (0 to stop):');
Write('> ');
ReadLn(Value);

while Value <> 0 do
begin
  Process(Value);
  Write('> ');
  ReadLn(Value);
end;

The sentinel value (0 in this case) is never processed — it only signals termination. The priming read before the loop and the update read at the bottom are both essential. Choose a sentinel that cannot appear as legitimate data.

Looking for a specific item:

Found := False;
i := 1;
while (i <= N) and (not Found) do
begin
  if Data[i] = Target then
    Found := True
  else
    i := i + 1;
end;

if Found then
  WriteLn('Found at position ', i)
else
  WriteLn('Not found.');

We use WHILE here (not FOR) because we want to stop as soon as we find the target. Notice how i is only incremented when the target is not found — so when we exit with Found = True, i still points to the matching position. This careful management of the index variable is characteristic of the search pattern.

An alternative using Break:

Found := False;
for i := 1 to N do
  if Data[i] = Target then
  begin
    Found := True;
    FoundIndex := i;
    Break;
  end;

Both approaches are correct. The WHILE version is more traditional; the FOR with Break version may be clearer in some contexts.

Pattern 5: Menu Loop

Displaying options and processing choices repeatedly:

repeat
  WriteLn;
  WriteLn('1. Add record');
  WriteLn('2. View records');
  WriteLn('3. Delete record');
  WriteLn('0. Exit');
  Write('Choice: ');
  ReadLn(Choice);

  case Choice of
    1: AddRecord;
    2: ViewRecords;
    3: DeleteRecord;
    0: { do nothing, loop will exit };
  else
    WriteLn('Invalid choice. Try again.');
  end;
until Choice = 0;

This is the bread and butter of console applications. The menu displays, the user chooses, the action executes, and the cycle repeats. REPEAT..UNTIL is the natural choice because the menu must display at least once.

Pattern 6: Running Maximum / Minimum

Finding the largest (or smallest) value in a sequence:

Max := Data[1];
for i := 2 to N do
  if Data[i] > Max then
    Max := Data[i];
WriteLn('Maximum value: ', Max);

Initialize with the first element, then compare against each subsequent element. If the current element is larger, it becomes the new maximum. After the loop, Max holds the largest value in the entire collection.

This works equally well for minimum (just reverse the comparison: if Data[i] < Min). You can also track the position of the max or min by saving i when a new extreme is found.

Pattern 7: Flag-Controlled Loop

Using a Boolean flag to control loop termination:

Done := False;
while not Done do
begin
  Write('Enter command: ');
  ReadLn(Command);
  case Command of
    'q', 'Q': Done := True;
    'h', 'H': ShowHelp;
    'p', 'P': PrintReport;
  else
    WriteLn('Unknown command. Type H for help.');
  end;
end;

This is similar to the menu pattern but uses a Boolean flag instead of checking a specific value in the UNTIL/WHILE condition. It can be cleaner when multiple different conditions might cause the loop to end. Rather than writing a complex multi-part condition in the while or until clause, you simply set Done := True wherever appropriate.

Pattern 8: Validation Loop

Combining input reading with validation:

repeat
  Write('Enter a month number (1-12): ');
  ReadLn(Month);
  if (Month < 1) or (Month > 12) then
    WriteLn('Invalid month. Please try again.');
until (Month >= 1) and (Month <= 12);

This pattern is so common that you will write it dozens of times in your programming career. The key elements are: prompt, read, validate, and repeat if invalid. Always provide a clear error message telling the user what went wrong and what you expect.


6.9 Debugging Loops

Loops are the most common source of bugs for beginning programmers — and, truthfully, for experienced programmers too. The logic of repetition introduces subtle possibilities for error that do not exist in sequential code. Here are the pitfalls to watch for and techniques to diagnose them.

Infinite Loops

An infinite loop runs forever because its termination condition is never met. If your program appears to "hang" or "freeze" — it is running but producing no output and accepting no input — an infinite loop is the most likely cause.

Common cause 1: Forgetting to update the loop variable.

i := 1;
while i <= 10 do
  WriteLn(i);
  { Oops — forgot i := i + 1; }
  { Actually, the i := i + 1 might be there but outside the loop
    due to missing begin..end }

Common cause 2: Updating the loop variable in the wrong direction.

i := 10;
while i >= 1 do
begin
  WriteLn(i);
  i := i + 1;  { Should be i := i - 1 }
end;

Here, i starts at 10 and goes to 11, 12, 13, ... It will never be less than 1.

Common cause 3: Using the wrong comparison operator.

i := 1;
while i <> 10 do  { Dangerous! }
begin
  WriteLn(i);
  i := i + 3;
end;

The variable i takes values 1, 4, 7, 10, 13, ... Wait — it does hit 10! So this particular example works. But change the step to 2, and i takes values 1, 3, 5, 7, 9, 11, 13, ... It will never equal 10, and the loop runs forever. Using i < 10 or i <= 9 instead of i <> 10 is much safer because inequality checks (<, <=) cannot be "jumped over."

Common cause 4: REPEAT..UNTIL condition that can never become True.

repeat
  Write('Enter yes or no: ');
  ReadLn(Answer);
until (Answer = 'yes') or (Answer = 'no');
{ User enters 'Yes' with capital Y — never matches! }

Fix: convert to lowercase before comparing, or check multiple forms.

Recovery: On most systems, pressing Ctrl+C will terminate a runaway program. In the Free Pascal IDE, you may need Ctrl+Break. On Windows, you can also use the Task Manager (Ctrl+Shift+Esc) to kill the process.

Off-by-One Errors

An off-by-one error (sometimes abbreviated OBOE) means your loop executes one time too many or one time too few. These are notoriously common, notoriously hard to spot, and responsible for more debugging time than perhaps any other class of bug.

Example: How many times does this loop execute?

for i := 1 to 10 do
  WriteLn(i);

Answer: 10 times. The range is inclusive on both ends: 1, 2, 3, ..., 10.

But what about this?

i := 1;
while i < 10 do
begin
  WriteLn(i);
  i := i + 1;
end;

Answer: 9 times (i = 1, 2, ..., 9). The loop exits when i reaches 10 because 10 < 10 is False. If you wanted 10 iterations, use i <= 10.

And this?

i := 0;
while i < 10 do
begin
  WriteLn(i);
  i := i + 1;
end;

Answer: 10 times (i = 0, 1, 2, ..., 9). Starting from 0 with < 10 gives you 10 iterations (0 through 9), just like starting from 1 with <= 10.

The fencepost problem: If you need to print 5 values separated by commas, you need 5 values but only 4 commas. Printing a comma after each value gives you a trailing comma. Printing a comma before each value gives you a leading comma. The solution is to handle the first (or last) element differently:

Write(Data[1]);
for i := 2 to 5 do
  Write(', ', Data[i]);
WriteLn;

Output: 10, 20, 30, 40, 50 (no trailing comma).

The name "fencepost problem" comes from this analogy: to build a 100-foot fence with posts every 10 feet, you need 11 posts, not 10. The number of posts is always one more than the number of gaps between them.

Tracing with WriteLn

When a loop misbehaves, the simplest and most effective debugging technique is to insert WriteLn statements that display the loop variable and key values at each iteration:

i := 1;
Total := 0;
while i <= N do
begin
  WriteLn('DEBUG: i=', i, ' Total=', Total);
  Total := Total + Data[i];
  i := i + 1;
end;
WriteLn('DEBUG: Final — i=', i, ' Total=', Total);

This trace output lets you see exactly what the loop is doing at each step. You can compare the actual behavior against your expectations and pinpoint where they diverge. Once the bug is fixed, remove or comment out the debug lines.

For infinite loops, the trace output will scroll rapidly, but you can still see the values changing (or not changing). If the variable is not changing, you have found your bug — the update is missing or unreachable.

Desk-Checking (Hand Tracing)

Before running your program, trace through the loop by hand with a small input. Write down the value of each variable after each iteration in a table. This is called desk-checking or hand tracing, and it is one of the most powerful debugging techniques ever invented.

Example: trace for i := 1 to 3 do Total := Total + i with Total starting at 0.

Iteration i Total (before) Total (after)
1 1 0 1
2 2 1 3
3 3 3 6

Final result: Total = 6. Does that match the expected sum of 1 + 2 + 3 = 6? Yes. The loop is correct.

This technique catches most errors before you even compile. It takes two minutes and can save twenty minutes of debugging. Get in the habit of desk-checking every loop you write, especially when the logic is non-trivial.


6.10 Introducing Crypts of Pascalia: The Game Loop

Every game, from Pong to the latest triple-A title, revolves around a game loop: an endless cycle of reading player input, updating the game state, and displaying the result. Our text adventure Crypts of Pascalia is no different.

The game loop is one of the oldest and most enduring patterns in programming. The earliest interactive programs on 1960s mainframes used it. Modern game engines like Unity and Unreal run sophisticated versions of it at 60 or 120 frames per second. But the core structure is identical to what we will build right here in Pascal.

Here is the skeleton:

program CryptsOfPascalia;
var
  Command: string;
  CurrentRoom: Integer;
  GameOver: Boolean;
begin
  WriteLn('=== CRYPTS OF PASCALIA ===');
  WriteLn('You stand at the entrance of an ancient crypt.');
  WriteLn('Type HELP for a list of commands.');
  WriteLn;

  CurrentRoom := 1;
  GameOver := False;

  repeat
    { 1. Display prompt }
    Write('> ');
    ReadLn(Command);

    { 2. Process command and update state }
    if (Command = 'quit') or (Command = 'QUIT') then
      GameOver := True
    else if (Command = 'help') or (Command = 'HELP') then
      WriteLn('Commands: LOOK, GO NORTH, GO SOUTH, QUIT')
    else if (Command = 'look') or (Command = 'LOOK') then
    begin
      case CurrentRoom of
        1: WriteLn('You are in a dusty entrance hall. A passage leads north.');
        2: WriteLn('You are in a torch-lit corridor. Passages lead north and south.');
        3: WriteLn('You are in a treasure room! Gold glitters in the torchlight.');
      end;
    end
    else if (Command = 'go north') or (Command = 'GO NORTH') then
    begin
      if CurrentRoom < 3 then
      begin
        CurrentRoom := CurrentRoom + 1;
        WriteLn('You walk north...');
      end
      else
        WriteLn('You cannot go that way.');
    end
    else if (Command = 'go south') or (Command = 'GO SOUTH') then
    begin
      if CurrentRoom > 1 then
      begin
        CurrentRoom := CurrentRoom - 1;
        WriteLn('You walk south...');
      end
      else
        WriteLn('You cannot go that way.');
    end
    else
      WriteLn('I do not understand "', Command, '". Type HELP for commands.');

  until GameOver;

  WriteLn('Thank you for playing Crypts of Pascalia!');
end.

Let us analyze the structure in detail:

  • REPEAT..UNTIL is the perfect construct here. The game must display at least one prompt before it can check whether the player wants to quit. You cannot quit before you have had a chance to play.
  • The loop body follows the classic game loop structure: (1) read input, (2) process the command and update state, (3) display results. Some games add a fourth step — render graphics — but for a text adventure, the output happens naturally as part of command processing.
  • The termination condition (GameOver) is a Boolean flag that is set when the player types QUIT. This is the flag-controlled loop pattern from Section 6.8.
  • The game state (just CurrentRoom for now) persists across iterations because it is declared outside the loop. Each iteration reads it, possibly modifies it, and leaves it for the next iteration.

This is a bare-bones skeleton with only three rooms and a handful of commands. In later chapters, we will transform it into a full game:

  • Chapter 8 (Procedures): Extract command handling into separate procedures for cleaner code.
  • Chapter 10 (Arrays): Store room descriptions and connections in arrays instead of hard-coding them.
  • Chapter 12 (Records): Define record types for rooms, items, and the player's inventory.
  • Chapter 14 (File I/O): Load room data from text files, enabling save games and user-created dungeons.

But the fundamental loop structure you see here — repeat / read input / process / display / until GameOver — will remain at the heart of the program throughout. Every enhancement builds on this foundation. That is the power of getting the core architecture right from the start.

The full, expanded version of this skeleton (with four rooms, an inventory system, and item interactions) is in code/example-05-game-loop.pas. Compile it, play through it, and study how the game loop manages state across iterations.

Theme 3 (Living Language): The game loop pattern is as old as interactive computing itself. From 1970s mainframe adventure games like Colossal Cave to modern game engines, the read-process-display cycle is universal. Pascal's clean syntax makes the pattern visible and easy to understand, which is exactly what Wirth intended when he designed the language.


6.11 Project Checkpoint: PennyWise Continuous Entry

In Chapter 5, our PennyWise expense tracker could record a single expense and categorize it. That is useful once but not very practical for daily life. You would have to run the program every time you wanted to record an expense. Now, with loops, we can transform PennyWise into a program that stays running, handles multiple expenses in a session, and displays summaries on demand.

New Features in This Checkpoint

  1. Main menu loop (REPEAT..UNTIL) — keeps the program running until the user chooses to exit. The user can add expenses, view summaries, or quit, and the program returns to the menu after each action.
  2. Continuous expense entry (REPEAT..UNTIL) — within the "Add expenses" option, the user can enter multiple expenses in one sitting, stopping by entering an amount of 0.
  3. Expense summary (FOR loop) — displays all recorded expenses, computes totals by category, and calculates percentages.

Design

We will use arrays (a preview of Chapter 10) to store up to 100 expenses. Each expense has an amount, a category code, and a description. Here is the high-level structure:

PennyWise Main Menu
1. Add expenses        <-- REPEAT..UNTIL loop (entry)
2. View summary        <-- FOR loop (computation and display)
3. View all expenses   <-- FOR loop (display)
4. Quit                <-- exits the REPEAT..UNTIL menu loop

The main menu uses REPEAT..UNTIL because the menu must display at least once — you cannot choose to quit before you have seen the options. The expense entry uses REPEAT..UNTIL because you must enter at least one expense (or at least see the prompt) before the program can check whether you are done. The summary uses FOR because we know exactly how many expenses have been recorded.

Key Code Excerpts

The main menu loop:

repeat
  WriteLn;
  WriteLn('--- Main Menu ---');
  WriteLn('  1. Add expenses');
  WriteLn('  2. View summary');
  WriteLn('  3. View all expenses');
  WriteLn('  4. Quit');
  Write('Choose an option (1-4): ');
  ReadLn(MenuChoice);

  case MenuChoice of
    1: { expense entry loop }
    2: { summary display }
    3: { list all expenses }
    4: { quit message and session summary }
  else
    WriteLn('Invalid option.');
  end;
until MenuChoice = 4;

The expense entry loop (inside option 1):

repeat
  Write('Amount: $');
  ReadLn(Amount);

  if Amount > 0 then
  begin
    { Get description and category }
    Write('Description: ');
    ReadLn(Desc);

    repeat
      Write('Category (F=Food, T=Transport, E=Entertainment, O=Other): ');
      ReadLn(CatCode);
    until CatCode in ['F','f','T','t','E','e','O','o'];

    { Store the expense }
    ExpenseCount := ExpenseCount + 1;
    Amounts[ExpenseCount] := Amount;
    Categories[ExpenseCount] := CatCode;
    Descriptions[ExpenseCount] := Desc;
    WriteLn('  Recorded: $', Amount:0:2, ' (', Desc, ')');
  end;
until Amount <= 0;

Notice the nested REPEAT..UNTIL for category validation: we keep asking until the user enters a valid category code. This is the input validation pattern from Section 6.2, nested inside the expense entry pattern from the same section.

The summary display (inside option 2) uses a FOR loop with the accumulator and counter patterns:

GrandTotal := 0;
FoodTotal := 0;
{ ... initialize other category totals ... }

for i := 1 to ExpenseCount do
begin
  GrandTotal := GrandTotal + Amounts[i];
  case Categories[i] of
    'F': begin FoodTotal := FoodTotal + Amounts[i]; FoodCount := FoodCount + 1; end;
    'T': begin TransportTotal := TransportTotal + Amounts[i]; TransportCount := TransportCount + 1; end;
    { ... other categories ... }
  end;
end;

{ Display the summary table }

Notice how naturally the three loop types fit their roles: - The outer menu uses REPEAT..UNTIL — we always show the menu at least once. - The expense entry uses REPEAT..UNTIL — we always prompt at least once. - The category validation uses REPEAT..UNTIL — we always ask at least once. - The summary display uses FOR — we know exactly how many expenses to process.

The full working program is in code/project-checkpoint.pas. Compile it with fpc project-checkpoint.pas, run it, and experiment. Try adding several expenses across different categories, then view the summary to see your spending breakdown.

What We Learned

This checkpoint demonstrates that loops are not just a theoretical concept — they transform programs from single-use tools into persistent, interactive applications. Without loops, PennyWise could record exactly one expense per program run. With loops, it becomes a genuine tool you could use to track spending throughout a day.

Theme 5 (A+DS=P): Even this simple program combines algorithms (the accumulator pattern in the summary, the counter pattern for tracking entries, the menu loop pattern for user interaction) with data structures (arrays to store expenses, categories, and descriptions). As our data structures become more sophisticated in later chapters, the algorithms we can build on top of them will grow correspondingly powerful.


6.12 Chapter Summary

This chapter introduced the three loop constructs in Pascal — the essential tools for iteration that, together with the decision constructs from Chapter 5, give you the ability to write programs of genuine complexity and utility.

REPEAT..UNTIL (post-test loop): - Executes the body at least once. - Terminates when the condition becomes True (stop when true). - Does not require begin..end for multiple statements — repeat and until are the delimiters. - Best for: input validation, menus, game loops — any situation where you must act before you can check.

WHILE..DO (pre-test loop): - May execute zero times if the condition is initially False. - Continues as long as the condition is True (continue when true). - Requires begin..end for multiple statements. - Best for: file processing, sentinel loops, any situation where zero iterations is a valid possibility.

FOR..TO / FOR..DOWNTO (counted loop): - Executes a predetermined number of times. - The loop variable is managed automatically — do not modify it inside the body. - The loop variable's value is undefined after the loop ends. - Requires begin..end for multiple statements. - Best for: counting, iterating over ranges, processing collections of known size.

Key patterns we learned: counter, accumulator, sentinel loop, search, menu loop, running max/min, flag-controlled loop, input validation. These patterns recur throughout all of programming — master them here, and you will recognize them in every language and every domain you encounter.

Debugging strategies: Watch for infinite loops (condition never met, variable not updated, wrong direction), off-by-one errors (wrong comparison operator, wrong boundary value, fencepost problems), and the begin..end trap (forgetting compound statement delimiters in WHILE and FOR loops). Desk-checking and WriteLn tracing are your primary diagnostic tools.

We built the Crypts of Pascalia game loop skeleton, demonstrating how REPEAT..UNTIL naturally models the read-process-display cycle at the heart of every interactive program. And we upgraded PennyWise to support continuous expense entry and summaries, combining all three loop types — REPEAT..UNTIL for the menu and entry loops, FOR for computation and display — in a single practical program.


Spaced Review

These questions revisit material from earlier chapters to strengthen your long-term retention.

From Chapter 4: What is the difference between Read and ReadLn?

Read reads the requested values but leaves the cursor at the end of the data on the current line; any remaining characters (including the newline) stay in the input buffer. ReadLn reads the values and then discards the rest of the line, including the newline character. In practice, ReadLn is safer for interactive input because it clears the buffer, preventing leftover characters from being picked up by the next read operation.

From Chapter 2: What command compiles a Pascal program from the command line?

fpc programname.pas — this invokes the Free Pascal Compiler, which compiles the source file and (if there are no errors) produces an executable. Common flags include -O2 for optimization and -g for debug information.


Looking Ahead

With decisions (Chapter 5) and loops (this chapter), you now have the two fundamental control flow mechanisms. Together, they give you the power to write programs that handle arbitrarily complex, dynamic behavior. You can branch, you can repeat, and you can combine branching and repetition in any way you need.

But you may have noticed that our PennyWise checkpoint used arrays without formally introducing them. We stored expenses in Amounts[1], Amounts[2], and so on — treating them as a numbered collection of variables. In Chapter 7, we will step back and examine data types in depth — the building blocks that determine what kinds of values our variables can hold and how they behave. Understanding types thoroughly is essential preparation for arrays (Chapter 10), records (Chapter 12), and everything that follows.

But first, test your understanding with the exercises and quiz that follow. The best way to learn loops is to write them — lots of them. The exercises in this chapter are intentionally numerous because fluency with loops comes from practice, not from reading. Work through the programming exercises (Part C) especially; each one reinforces a different pattern or skill. The more loops you write, the more naturally the right construct will come to mind when you face a new problem.