You're describing the outer interpreter in interpretation state; Forth control flow words don't work properly in interpretation state, only in compile state.
They're
immediate words, so they execute at compile time instead of run time, so they can do arbitrary things to the code being compiled. Here's Mike Perry and Henry Laxen's implementation of the main control-flow words from F83, which is an indirect-threaded Forth:
\ Run Time Code for Control Structures 04OCT83HHL \ \ Run Time Code for Control Structures 05MAR83HHL
CODE BRANCH (S -- ) \ BRANCH Performs an unconditional branch. Notice that we
LABEL BRAN1 0 [IP] IP MOV NEXT END-CODE \ are using absolute addresses insead of relative ones. (fast)
CODE ?BRANCH (S f -- ) \ ?BRANCH Performs a conditional branch. If the top of the
AX POP AX AX OR BRAN1 JE IP INC IP INC NEXT END-CODE \ parameter stack in True, take the branch. If not, skip
\ over the branch address which is inline.
\ Extensible Layer Structures 03Apr84map \ \ Extensible Layer Structures 03Apr84map
: ?CONDITION (S f -- ) \ ?CONDITION
NOT ABORT" Conditionals Wrong" ; \ Simple compile time error checking. Usually adequate
: >MARK (S -- addr ) HERE 0 , ; \ >MARK Set up for a Forward Branch
: >RESOLVE (S addr -- ) HERE SWAP ! ; \ >RESOLVE Resolve a Forward Branch
: MARK (S -- f addr ) TRUE >MARK ; \ ?>MARK Set up a forward Branch with Error Checking
: ?>RESOLVE (S f addr -- ) SWAP ?CONDITION >RESOLVE ; \ ?>RESOLVE Resolve a forward Branch with Error Checking
: ?RESOLVE ; IMMEDIATE \ the Forth Conditional Structures. Each of them is immediate
: DO COMPILE (DO) ?>MARK ; IMMEDIATE \ and they must compile their runtime routines along with
: ?DO COMPILE (?DO) ?>MARK ; IMMEDIATE \ whatever addresses they need. A modest amount of error
: LOOP \ checking is done. If you want to rip out the error checking
COMPILE (LOOP) 2DUP 2+ ?RESOLVE ; IMMEDIATE \ change the ?> and ? and RESOLVE ; IMMEDIATE \ should stay the same.
: UNTIL COMPILE ?BRANCH ?MARK ; IMMEDIATE
: ELSE COMPILE BRANCH ?>MARK 2SWAP ?>RESOLVE ; IMMEDIATE
: WHILE [COMPILE] IF ; IMMEDIATE
When the interpreter is toodling along in compile state, compiling a colon definition by stowing pointers one after another (at the pointer
here) into the definition of some word you're compiling, and it encounters an
if, it sees that
if is
immediate, and so instead of stowing a pointer to
if it just runs it immediately. The definition of
if is
compile ?branch ?>mark.
compile is also an
immediate word [correction, no, it's not, see below comment, though the following is still correct];
compile ?branch stows a pointer to
?branch into the colon definition being compiled, and then
?>mark writes a 0 into the entry following the
?branch and pushes
true and the address of the 0 on the operand stack, at compile time, with the sequence
true here 0 ,. The interpreter toodles along compiling the body of the
if and eventually gets to, for example,
then, which is also
immediate, and is defined as
?>resolve, which overwrites the 0 into the address of the indirect-threaded code that will be compiled following the
then. It does this with
swap ?condition here swap !. The
swap ?condition part aborts with an error if there isn't an unresolved
if or similar on the stack to resolve, consuming the
true, leaving only the address of the 0 that
?>mark had pushed. So then
here swap ! overwrites that 0 with the current value of
here.
?branch is a word written in assembly which does a conditional jump in the inner interpreter (the one that interprets the indirect-threaded code); when it's executed, it pops a value off the stack and checks to see if it's zero, and if so, it changes the interpreter's execution pointer ip (which is defined elsewhere as the register si) to the number stored in the threaded code following the pointer to ?branch. If, on the other hand, the value it popped was nonzero, it increments ip twice to skip over that number. (Note that Laxen's comment on ?branch is incorrect in that it reverses the sense of the test.)
All the forward jumps work in pretty much the same way: when you begin a control structure you call ?>mark to write a zero placeholder and push its address, and later on you "resolve" that placeholder by popping its address off the stack and overwriting it with the correct address. leave (break) and ?leave (if (...) break) work slightly differently, but mostly the same.
Backward jumps work the other way around: when you begin a control structure, as in begin, you call ? to save the current address on the stack so that you can jump to it later, which ends up just being true here. Then, to actually compile the jump, for example in until or again, you call ?, which ends up just being swap ?condition ,—the , pops the jump target address off the stack and compiles it into the indirect threaded code, serving as an argument the ?branch or branch instruction compiled immediately before it.
begin ... while ... repeat is handled, as you can see, by treating the while ... repeat part as an if ... then with an unconditional jump back to the begin jammed in right before the then.
Hopefully this is helpful!
BTW, for the above, I reformatted the block files from the F83 distribution with http://canonical.org/~kragen/sw/dev3/blk2unix.py, which you may find useful if you want to do the same thing.