Beginner

Control Flow in Forth

Learn conditionals, CASE branching, and counted and indefinite loops in Forth using stack flags and runnable Docker examples

Control flow decides which words run and how often. In most languages a conditional reads a boolean expression in parentheses; in Forth, control flow is just more words operating on the data stack. A comparison word leaves a flag on the stack, and a structuring word like IF consumes that flag to decide what happens next.

This is a direct consequence of Forth’s stack-based, concatenative paradigm. There are no parentheses, no condition expressions, and no statement terminators — only words separated by whitespace. The control-flow words (IF, ELSE, THEN, DO, LOOP, BEGIN, UNTIL, WHILE, REPEAT, CASE) are themselves part of the language and can only appear inside a definition (: ... ;), because they compile branching logic into the word being defined.

A flag in Forth is just a number: 0 is false, and any non-zero value is true. The standard comparison words leave the canonical true flag -1 (all bits set) or 0. Because flags are ordinary stack values, you can compute, store, and combine them like any other number.

In this tutorial you will learn how to branch with IF...ELSE...THEN, select among many cases with CASE, run counted loops with DO...LOOP, and write condition-driven loops with BEGIN...WHILE...REPEAT and BEGIN...UNTIL.

Conditionals: IF, ELSE, THEN

A conditional reads a flag from the stack. IF consumes the top of the stack: if it is non-zero (true), the words between IF and ELSE run; otherwise the words between ELSE and THEN run. THEN simply marks the end of the conditional — it is not a “then do this” keyword as in other languages. The ELSE clause is optional.

Comparison words like >, <, and = take two values and leave a flag. Note that IF requires a flag to already be on the stack, so the comparison must come before IF.

Create a file named conditionals.fth:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
\ Conditionals in Forth

: SHOW-SIGN ( n -- )
    DUP 0 > IF   ." positive" CR
    ELSE DUP 0 < IF   ." negative" CR
    ELSE   ." zero" CR
    THEN THEN DROP ;

: ?BIG ( n -- )
    100 > IF ." that is a big number" CR THEN ;

5 SHOW-SIGN
-3 SHOW-SIGN
0 SHOW-SIGN
250 ?BIG
50 ?BIG
bye

SHOW-SIGN shows nested conditionals. DUP copies the number so the comparison can consume one copy while keeping the original for the next test or the final DROP. Each IF needs a matching THEN, which is why two THENs appear at the end. ?BIG shows the single-branch form: with no ELSE, nothing happens when the flag is false.

Multi-way Branching: CASE

When you need to select among many specific values, nesting IFs becomes hard to read. Forth provides CASE...OF...ENDOF...ENDCASE for this. The value to test sits on the stack; each value OF ... ENDOF clause compares against it, and the first match runs its body. Any words after the last ENDOF (before ENDCASE) act as the default case. ENDCASE automatically drops the test value.

Create a file named multiway.fth:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
\ Multi-way branching with CASE

: WEEKDAY ( n -- )
    CASE
        1 OF ." Monday"    ENDOF
        2 OF ." Tuesday"   ENDOF
        3 OF ." Wednesday" ENDOF
        4 OF ." Thursday"  ENDOF
        5 OF ." Friday"    ENDOF
        ." Weekend or invalid"
    ENDCASE CR ;

1 WEEKDAY
5 WEEKDAY
7 WEEKDAY
bye

For 7 WEEKDAY, no OF clause matches, so the default text prints. The original value (7) is still on the stack when the default runs, and ENDCASE discards it for you.

Counted Loops: DO, LOOP, and friends

When you know how many iterations you need, use a definite loop. DO takes two values — a limit and a starting index — and runs the body for each index from the start up to (but not including) the limit. Inside the loop, the word I pushes the current index onto the stack.

  • ?DO is like DO but skips the body entirely if the limit equals the start (a DO loop with equal bounds would run a huge number of times).
  • +LOOP takes a step value, letting you count by amounts other than 1.
  • In nested loops, I is the inner index and J is the next-outer index.
  • LEAVE exits the innermost loop immediately.

Create a file named definite_loops.fth:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
\ Definite (counted) loops

: SQUARES ( n -- )
    1+ 1 DO   I DUP * .   LOOP CR ;

: EVENS ( -- )
    10 0 DO   I .   2 +LOOP CR ;

: MULT-TABLE ( -- )
    4 1 DO
        4 1 DO   I J * .   LOOP
        CR
    LOOP ;

: FIND-FIVE ( n -- )
    0 ?DO
        I 5 = IF ." found 5 at index " I . CR LEAVE THEN
    LOOP ;

5 SQUARES
EVENS
MULT-TABLE
8 FIND-FIVE
bye

SQUARES prints the squares of 1 through n — note 1+ so the limit is one past the last value we want. EVENS steps by 2 with +LOOP. MULT-TABLE nests two loops and uses both I and J to build a small multiplication grid. FIND-FIVE uses ?DO to guard against an empty range and LEAVE to stop early once the target index is reached.

Indefinite Loops: BEGIN, WHILE, REPEAT, and UNTIL

When the number of iterations depends on a condition rather than a count, use an indefinite loop. Forth offers two shapes:

  • BEGIN ... condition WHILE ... REPEAT — tests at the top. The body runs only while the condition is true (this can run zero times).
  • BEGIN ... condition UNTIL — tests at the bottom. The body always runs at least once, and repeats until the condition becomes true.

Create a file named indefinite_loops.fth:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
\ Indefinite loops

: COUNTDOWN ( n -- )
    BEGIN DUP 0 > WHILE
        DUP . 1-
    REPEAT DROP CR ;

: COUNTUP ( limit -- )
    0
    BEGIN
        1+ DUP .
        2DUP =
    UNTIL 2DROP CR ;

5 COUNTDOWN
5 COUNTUP
bye

COUNTDOWN keeps a value on the stack, prints and decrements it with 1-, and loops while it stays above zero; when it reaches 0, WHILE fails and DROP cleans up the leftover. COUNTUP keeps a counter beneath the limit, increments and prints it, then uses 2DUP = to test whether the counter has reached the limit — UNTIL exits once it has, and 2DROP clears both values.

Running with Docker

Each file is a complete, self-contained Forth program. Use the official Gforth image to run them:

1
2
3
4
5
6
7
8
# Pull the Gforth image
docker pull forthy42/gforth:latest

# Run each example (the file already ends with `bye`)
docker run --rm -v $(pwd):/app -w /app forthy42/gforth:latest gforth conditionals.fth -e bye
docker run --rm -v $(pwd):/app -w /app forthy42/gforth:latest gforth multiway.fth -e bye
docker run --rm -v $(pwd):/app -w /app forthy42/gforth:latest gforth definite_loops.fth -e bye
docker run --rm -v $(pwd):/app -w /app forthy42/gforth:latest gforth indefinite_loops.fth -e bye

Expected Output

conditionals.fth:

positive
negative
zero
that is a big number

multiway.fth:

Monday
Friday
Weekend or invalid

definite_loops.fth:

1 4 9 16 25 
0 2 4 6 8 
1 2 3 
2 4 6 
3 6 9 
found 5 at index 5

indefinite_loops.fth:

5 4 3 2 1 
1 2 3 4 5

Key Concepts

  • Conditions are flags on the stack — comparison words like >, <, and = leave a flag (0 for false, non-zero for true) that IF, UNTIL, and WHILE consume.
  • THEN ends an IF, it does not begin a branch — read IF...ELSE...THEN as “if / else / end-if”. Every IF needs a matching THEN.
  • Control-flow words live inside definitionsIF, DO, BEGIN and friends compile branching into a word, so they belong between : and ;.
  • CASE replaces deep IF nesting — use value OF ... ENDOF clauses for clean multi-way selection; ENDCASE drops the test value automatically.
  • DO...LOOP is counted; I and J give you the indices — supply a limit and a start, use ?DO to guard empty ranges, +LOOP for custom steps, and LEAVE to break out early.
  • Choose your loop test positionBEGIN...WHILE...REPEAT tests at the top (may run zero times); BEGIN...UNTIL tests at the bottom (runs at least once).
  • Mind the stack inside loops — use DUP, DROP, 2DUP, and 2DROP to keep the right values available for the next iteration and to clean up when the loop ends.

Running Today

All examples can be run using Docker:

docker pull forthy42/gforth:latest
Last updated:

Comments

Loading comments...

Leave a Comment

2000 characters remaining