Forth uses IMMEDIATE words to build control structures at compile time. The various opening and closing words communicate with each other by passing items on a control stack to ensure proper nesting.
What goes on the control stack
- BEGIN leaves dest to be consumed by UNTIL or AGAIN
- AHEAD and IF leave orig to be consumed by THEN
- (AHEAD compiles an unconditional forward jump)
- DO and ?DO leave do-sys to be consumed by LOOP or +LOOP
- CASE leaves case-sys to be consumed by ENDCASE
- OF leaves of-sys to be consumed by ENDOF
Sometimes, however, strict nesting is not what is needed, and the Standard provides the words CS-PICK and CS-ROLL to change the
order of items on the stack. (0 CS-ROLL is a no-op, 1 CS-ROLL is a swap; 0 CS-PICK is a dup, 1 CS-PICK is an over, and so on...). The details of the control stack and the size and format of items is implementation-dependant, but by far the most common choice is to use the data stack, with either one or two items for each item.
In the examples below I shall use CS-PICK and CS-ROLL explicitly, but they are intended to be used for the definition of new control structure words, as in the examples of ELSE and WHILE below:
: ELSE \ orig1 -- orig2
POSTPONE AHEAD \ orig1 orig2
1 CS-ROLL POSTPONE THEN ; \ consumes orig1
IMMEDIATE
: WHILE \ dest -- orig dest
POSTPONE IF 1 CS-ROLL ; IMMEDIATE
Using WHILE
BEGIN ... WHILE is normally resolved by REPEAT, which is another way of saying AGAIN THEN - but could also be closed with
... UNTIL
do this on normal exit only THEN ...
and you can use multiple WHILEs within the loop, each resolved by its own THEN outside
BEGIN ... WHILE ... WHILE ... UNTIL ... THEN ... THEN
But you’ll notice that WHILE does not modify the dest on the top of the control stack. In fact, unless your Forth is overly pedantic, any other item will do as well. For example, in:
IF ... WHILE ... THEN ... THEN
(I don't recommend this - each time I look at it I have to work out again what it actually does).
Generally it is best to strictly nest both forward branches and loops, but it is sometimes useful to branch out of a loop (as in the examples above) or into the middle of a loop:
IF BEGIN
maybe ignore first time [ 1 CS-ROLL ] THEN
the rest of the loop UNTIL
WHILE may equally well be used to exit a DO ... LOOP, but here you need to use UNLOOP to get rid of the redundant loop index.
DO ... WHILE ... LOOP
normal exit ELSE
early exit UNLOOP THEN
is very messy, even more so with multiple WHILEs. It would be better to place the ELSE clauses within the loop, changing the sense of the test:
DO ... IF
early exit UNLOOP ELSE [ 1 CS-ROLL ] ... LOOP
normal exit THEN ...
Of course. if the definition ends at THEN, you can simply do an early return
DO ... IF
early exit UNLOOP EXIT THEN ... LOOP
normal exit
Or if you just want to exit the loop and perform whatever code follows in all cases:
DO ... IF
early exit LEAVE THEN ... LOOP
all exits
Branching to a Common Start
This can be done using CS-PICK. The simplest example mimics the Java’s continue keyword:
BEGIN ... WHILE ... [ 0 CS-PICK ] REPEAT ... UNTIL
DO ... WHILE ...[ 0 CS-PICK ] REPEAT ... LOOP
may possibly compile too, but is not guaranteed to work, since do-sys may not be identical to dest. In this case, since the loop counter is decremented by LOOP rather than DO, each time the program hits REPEAT it effectively repeats the same iteration.
The same trick can be used within a CASE statement. Suppose you want the same action for a range of values, with one exception:
CASE
exception OF
do-exception ENDOF
DUP
range WITHIN IF DROP
do-range ELSE [ 1 CS-ROLL ]
...
ENDCASE
THEN
On some (perhaps most) Forths this is unnecessary, since OF is the strict equivalent of OVER IF DROP and so can be paired with THEN, or IF paired with ENDOF.
Of course, if you find this uselful, it's better to define and document your own control words:
: DONE \ branch out of the current loop (resolved by implicit or explicit THEN
POSTPONE ELSE 1 CS-ROLL ; IMMEDIATE
: REPRISE \ return control to nearest BEGIN
0 CS-PICK POSTPONE REPEAT ; IMMEDIATE