27.20Adding conditionals. 4tH 4tH Forth compiler
Oglas
Oglas
CHAPTER 27. MODIFYING 4TH
27.20
Adding conditionals
Basically, there are ten branch-instructions in 4tH:
538
1. BRANCH, which unconditionally branches to an address in the Code Segment.
2. 0BRANCH, which branches to an address if the Code Segment if the top of the
Data Stack is zero.
3. CALL, which unconditionally branches to an address in the Code Segment, throwing its origin on the Return Stack.
4. EXIT, which unconditionally branches to an address in the Code Segment, which is taken from the top of the Return Stack.
5. VECTOR, which unconditionally branches to an address in the Code Segment, which is taken from the contents of a variable, throwing its origin on the Return
Stack.
6. EXECUTE, which unconditionally branches to an address in the Code Segment, which is taken from the top of the Data Stack, throwing its origin on the Return
Stack.
7. CATCH, which unconditionally branches to an address in the Code Segment, which is taken from the top of the Data Stack, throwing the data stack pointer, the previous handler and its origin on the Return Stack.
8. LOOP, which branches to an address in the Code Segment if the top of the Return
Stack is less than value below it.
9. +LOOP, which branches to an address in the Code Segment if the top of the Return
Stack plus the top of the Data Stack is not equal to the value below the top of the
Return Stack.
10. ?DO, which branches to an address in the Code Segment if the top of the Return
Stack plus the top of the Data Stack is equal to the value below the top of the Return
Stack.
The first four are the most common and the most useful ones. Together, they control your entire program. But how do they know where to branch to? There is no instruction like
’BRANCH’. And where is ’DO’?
Well, ’DO’ doesn’t do any branching. It just puts the loop parameters on the Return stack.
And as for ’BRANCH’, this is how it works:
IF something ELSE other thing THEN
This is an expression we are very familiar with. We pronounce it as:
"If TOS is non-zero then something is executed."
This is not entirely true. In fact, it is:
"If TOS is zero then branch after ’ELSE’"
CHAPTER 27. MODIFYING 4TH 539
Which in effect results in the execution of "something". But when "something" has executed, it has to branch after the "other thing". Unconditionally, that is. ’THEN’ does nothing, except serve as a marker for the branch. It doesn’t have to compile to anything.
So this little piece to code will compile to:
[0]
[1]
[2]
[3]
[4]
0branch
...
branch
...
...
(2)
(3)
You see that ’IF’ compiles to a ’0BRANCH’ instruction, ’ELSE’ to a ’BRANCH’ instruction and ’THEN’ to nothing! If you have a closer look you might assume that 4tH will branch to instruction [2] and then branch directly to instruction [3]. This is not quite what was intended.
What a ’BRANCH’ instruction actually does is setting the instruction counter to a specific value. Then, like after every instruction, the instruction counter is incremented. Why make exceptions? That only slows the interpreter down. Let’s take a look at this piece of code:
10 dup if 1+ else . then cr
This will compile into:
[0]
[1]
[2]
[3]
[4]
[5]
[6] literal dup
0branch
1+ branch
.
cr
(10)
(0)
(4)
(0)
(5)
(0)
(0)
Now how does this execute. We will show you by giving the value of the instruction pointer before execution, after execution and after the automatic increment.
I
NSTRUCTION
# I
NSTRUCTION
B
EFORE
A
FTER
I
NCREMENT
[0]
[1]
[2]
[3]
[4]
[6] literal dup
0branch
1+ branch cr
[0]
[1]
[2]
[3]
[4]
[6]
[0]
[1]
[2]
[3]
[5]
[6]
[1]
[2]
[3]
[4]
[6]
[7]
Table 27.9: Example execution plan
You see that ’0BRANCH’ has no effect when the top of the Data Stack is non-zero. And while ’BRANCH’ sets the instruction pointer to "5", it will resume execution at location
"6".
If you compile this little piece of code by hand and start compiling from the beginning, you will also see that you can’t fill in the destination until it has been compiled. So how does
4tH do that?
4tH has a small stack (Control Stack) where it stores these addresses. So when it encounters an ’IF’ or ’ELSE’ or ’THEN’ instruction, it stores its current address there. What would have happened during the compile of the previous program:
CHAPTER 27. MODIFYING 4TH 540
• ’IF’ is encountered. It compiles a ’0BRANCH’ instruction and stores ’2’ on the
Control Stack.
• ’ELSE’ is encountered. It compiles a ’BRANCH’ instruction, takes ’2’ from the
Control Stack and changes the argument of the ’0BRANCH’ word to its own address, which is ’4’. It stores ’4’ on the Control Stack again.
• ’THEN’ is encountered. It takes ’4’ from the Control Stack and changes the argument of the ’BRANCH’ word to address of the last compiled word, which is ’5’.
But this example was correct. What would have happened if we had written something like:
5 0 : test begin 1 dup if ; while . then dup dup repeat
To prevent the compiler from accepting these kind of constructions, a reference is added.
This reference tells the compiler what conditional was put on the stack. If the reference isn’t correct, the compiler will throw an exception. There are five predefined references, but you may add your own.
There are three functions which handle conditionals:
F
UNCTION
D
ESCRIPTION
MarkLink
PairLink
MakeLink
Throws an address on the stack
Gets an address from the stack
Makes a "backlink"
Table 27.10: Branch resolving API
They all take a reference as argument. All address calculation and errorchecking is done by these functions. Let’s get to business and retrace our steps when we added ’BEGIN..
WHILE.. REPEAT’.
All conditionals are "immediate" words. So they have to be added to ImmedList[].
That also means, that each word has its own function. Let’s design the one for ’BEGIN’.
’BEGIN’ is just a marker, which means we have little more to do than to save the address on the stack:
#ifndef ARCHAIC static void DoBegin (void)
#else
#endif static void DoBegin ()
{
MarkLink (R_BEGIN);
}
Yes, that’s all. Just call MakeLink() with the proper reference! Just make sure, you’ve compiled everything you wanted to compile. Jumps resolved by MarkLink() will in effect always continue from the word that will be compiled next, although it seems they jump to the word compiled last.
Now we have to make ’WHILE’. ’WHILE’ executes a piece of code when the top of the
DataStack is non-zero. Which means it jumps to ’REPEAT’ when the top of the DataStack is zero. Sounds like a ’0BRANCH’ instruction to us. Note, that ’BEGIN’ doesn’t play any part whatsoever here!
CHAPTER 27. MODIFYING 4TH 541
Because the address it has to jump to isn’t known yet (we haven’t encountered ’REPEAT’, we can only compile the ’0BRANCH’ instruction with an arbitrary address. But ’REPEAT’ will have to know the address in order to make a backlink, so we have to throw it on the stack:
#ifndef ARCHAIC static void DoWhile (void)
#else
#endif static void DoWhile ()
{
}
CompileMark (BRANCH0, R_WHILE);
Note that CompileMark() is equivalent to:
CompileWord (BRANCH0, 0L);
MarkLink (R_WHILE);
Now we come to ’REPEAT’. It has a lot of things to do. First, it has to compile a
’BRANCH’ instruction in order to get back to ’BEGIN’. Second, it will have to resolve the backlink from ’WHILE’.
In a way, ’REPEAT’ has an advantage over ’WHILE’. It doesn’t have to compile an arbitrary address, since it is already on the control stack. It has been provided by ’BEGIN’. It can retrieve that address by calling PairLink() with the proper reference:
CompileWord (BRANCH, PairLink (R_BEGIN));
But there is a problem. MakeLink() should always make a link to the last compiled word. And we can’t compile the ’BRANCH’ instruction first, because of the "WHILE" reference on the top of the controlstack!
So we have to resolve the ’WHILE’ backlink first. For that purpose, MakeLink() has an extra argument. Usually, MakeLink() is called with "LASTW", which means it will jump to the word compiled last. Then the instruction counter will be incremented and the interpreter will continue from there.
In order to compile ’REPEAT’, we have to make a backlink that points to the word compiled next . So, this statement is inserted before CompileWord():
MakeLink (R_WHILE, NEXTW);
CompileWord (BRANCH, PairLink (R_BEGIN));
And what if we had made a "BEGIN..AGAIN" or a "BEGIN..UNTIL" loop? Well, in any case we would have to branch back to ’BEGIN’. ’UNTIL’ conditionally and ’AGAIN’ unconditionally. The address of ’BEGIN’ would have already been on the control stack, so a single statement could have taken care of it:
CompileWord (0BRANCH, PairLink (R_BEGIN));
CompileWord (BRANCH, PairLink (R_BEGIN));
Actually, since 4tH supports multiple ’WHILE’s the problem is a little more complex.
’REPEAT’ must resolve all ’WHILE’s on the control stack before it can even think of compiling a ’BRANCH’ instruction. We’ve already seen that the only difference between an
’AGAIN’ and an ’UNTIL’ is the branch instruction which is compiled. So in 4tH ’RE-
PEAT’, ’UNTIL’ and ’AGAIN’ are handled in a similar way:
CHAPTER 27. MODIFYING 4TH 542
#ifndef ARCHAIC static void CompileAgain (unit AgainToken)
#else static void CompileAgain (AgainToken) unit AgainToken;
#endif
{ while ((ToCS > 0) && (ControlStack [ToCS - 1].Mark == R_WHILE))
MakeLink (R_WHILE, NEXTW);
CompileWord (AgainToken, PairLink (R_BEGIN));
}
As long as the control stack is not empty and there is a ’WHILE’ reference on top of the control stack, backlinks are made. Finally, the branch instruction is compiled, which jumps back to ’BEGIN’. ’REPEAT’ can now be reduced to:
CompileAgain (BRANCH);
Note that a colon definition also uses the control stack. This reference is resolved by ’;’, which compiles ’EXIT’ and creates a backlink. The ’BRANCH’ instruction will prevent the interpreter from entering the definition. At the same time, ’:’ creates a symbol. We’ll go into that in the next section.
If you want to create your own branch instructions, you’ll have to define their behaviour in exec_4th()
. If the argument of the token contains the address you want to jump to in the end, you’ll have to define it like this:
JUMP (OPERAND);
That is pretty easy. This macro changes the Program Counter, which is part of the Hcode header:
Object->ErrLine
Of course, we’ve defined a macro for that:
PROGCOUNT
If the address you want to jump to is issued by the user, you probably want to check whether it is a valid execution token. Just use the macro XT():
DSIZE (1); a = DPOP;
XT (a);
JUMP (a);
Consequently, leaving the current Program Counter value on the return stack is pretty easy too:
RFREE (1);
RPUSH (PROGCOUNT);
I think that covers it all, don’t you?
Prenesi
Oglas