yet_another_webdev(blog) 2024-02-16 yet_another_webdev(blog)
PROJECT
Starting FORTH
POST
Chapter 04 - DECISIONS, DECISIONS, ...
DESCRIPTION
This chapter introduces expressions that evaluate to TRUE or FALSE and
the IF...THEN words that allow you to execute code only if certain
conditions are met.
In this chapter we'll learn how to program the computer to make
"decisions". This is the moment when you turn your computer into
something more than an ordinary calculator.
The Conditional Phrase
Imagine we are programming a mechanical egg-carton packer. Some sort of
mechanical device has counted the eggs on the conveyor belt, and now we
have the number of eggs on the stack. The FORTH phrase:
12 = IF FILL-CARTON THEN
tests whether the number on the stack is equal to 12, and if it is, the
word FILL-CARTON is executed. If it's not, execution moves right along to
the words that follow THEN
PForth V2.0.1, LE/64, built May 12 2023 10:51:31 (static)
: ?FULL 12 = IF ." IT'S FULL" THEN ; ok
Stack<10>
11 ?FULL ok
Stack<10>
12 ?FULL IT'S FULL ok
Stack<10>
NOTICE:
an IF ... THEN statement must be contained within a colon definition.
You can't just enter these words in "calculator style".
pForth:
It seems that pForth does allow if-then statements in "calculator style".
$ cat > ifthen.fth <<EOF
1 1 = IF ." TRUE." CR THEN ." AFTER IF-THEN" CR
EOF
$ forth -q ifthen.fth
TRUE.
AFTER IF-THEN
The program above, while running in pForth, is not a valid FORTH program.
And is not a valid gForth program.
The words that follow IF are executed if the condition is true. The
words that follow THEN are always executed, as though you were tellinmg
the computer, "After you make the choice, then continue the rest of the
definition". (In this example, the only word after THEN is ; , which
ends the definition)
Let's look at another example. This definition checks wheter the
temperature of a laboratory boiler is too hot. It expects to find the
temperature on the stack:
: ?TOO-HOT 220 > IF ." DANGER -- REDUCE HEAT" THEN ;
If the temperature on the stack is greater than 220, the danger message
will be printed at the terminal.
PForth V2.0.1, LE/64, built May 12 2023 10:51:31 (static)
: ?TOO-HOT 220 > IF ." DANGER -- REDUCE HEAT" THEN ; ok
Stack<10>
290 ?TOO-HOT DANGER -- REDUCE HEAT ok
Stack<10>
130 ?TOO-HOT ok
Stack<10>
Remember that every IF needs a THEN to come home to. Both words must be
in the same definition.
Here is a partial list of comparison operators that you can use before an
IF ... THEN statement:
= pronounced "equal"
< pronounced "less-than"
> pronounced "greater-than"
=0 pronounced "zero equal"
0< pronounced "zero-less-than"
0> pronounced "zero-greater-than"
The words < and > expect the same stack order as the arithmetic
operators, that is:
Infix Postfix
2 < 10 is equivalent to 2 10 <
17 > -39 is equivalent to 17 -39 >
The words 0= , 0< , and 0> expect only one value on the stack. The
value is compared with zero.
Another word, NOT , doesn't test any value at all; it simply reverses
whatever condition has just been tested. For example, the phrase:
... = NOT IF ...
will execute the words after IF , if the two numbers on the stack are
not equal.
The Alternative Phrase
FORTH allows you to provide an alternative phrase in an IF statement,
with the word ELSE .
The following example is a definition which tests whether a given number
is a valid day of the month:
: ?DAY 32 < IF ." LOOKS GOOD" ELSE ." NO WAY" THEN ;
If the number on the stack is less then thirty-two, the message "LOOKS
GOOD" will be printed. Otherwise, "NO WAY" will be printed.
Imagine that IF pulls a railroad-track switch, depending on the coutcome
of the test. Execution then takes one of two routes, but either way, the
tracks rejoin at the word THEN .
By the way, in computer terminology, this whole business of rerouting the
path of execution is called "branching".
Here's a more useful example. You know that dividing any number by zero
is impossible, so if you try it on a computer, you'll get an incorrect
answer. We might define a word which only performs division if the
denominator is not zero. The following definition expects stack items in
this order:
(numerator denominator -- )
: /CHECK DUP 0= IF ." INVALID" DROP ELSE / THEN ;
Notice that we first have to DUP the denominator because the phrase
0= IF
will destroy it in the process.
Also notice that word DROP removes the denominator if division won't be
performed, so that wheter we devide or not, the stack effect will be the
same.
Nested IF...THEN Statements
It's possible to put an IF ... THEN (or IF ... ELSE ... THEN )
statement inside another IF ... THEN statement. In fact, you can get
as complicated as you like, so long as every IF has one THEN .
Consider the following definition, which determines the size of comercial
eggs (extra large, large, etc.), given their weight in ounces per dozen:
$ cat > nestedIf.fth <<EOF
: EGGSIZE DUP 18 < IF ." REJECT" ELSE
DUP 21 < IF ." SMALL" ELSE
DUP 24 < IF ." MEDIUM" ELSE
DUP 27 < IF ." LARGE" ELSE
DUP 30 < IF ." EXTRA LARGE" ELSE
." ERROR"
THEN THEN THEN THEN THEN DROP ;
17 DUP . EGGSIZE CR
18 DUP . EGGSIZE CR
19 DUP . EGGSIZE CR
20 DUP . EGGSIZE CR
21 DUP . EGGSIZE CR
22 DUP . EGGSIZE CR
23 DUP . EGGSIZE CR
24 DUP . EGGSIZE CR
25 DUP . EGGSIZE CR
26 DUP . EGGSIZE CR
27 DUP . EGGSIZE CR
28 DUP . EGGSIZE CR
29 DUP . EGGSIZE CR
30 DUP . EGGSIZE CR
31 DUP . EGGSIZE CR
EOF
$ forth -q nestedIf.fth
17 REJECT
18 SMALL
19 SMALL
20 SMALL
21 MEDIUM
22 MEDIUM
23 MEDIUM
24 LARGE
25 LARGE
26 LARGE
27 EXTRA LARGE
28 EXTRA LARGE
29 EXTRA LARGE
30 ERROR
31 ERROR
The entire definition is a series of "nested" IF ... THEN statements.
The word "nested" referst to the fact that the statements nest inside
one another, like a set of mixing bowls.
The five THEN at the bottom close off the five THEN in reverse order.
Also notice that a DROP is necessary at the end of the definition to get
rid of the original value.
Finally, notice that the definition is visually organized to be read
easily by humans. Most FORTH programmers would rather waste a little
space in a block than let things get any more confused than they have to
be.
A Closer Look at IF
How does the comparison operator ( = , < , > , or whichever) let IF
know whether the condition is true or false? By simply leaving a one or
a zero on the stack. A one means that the condition is true; a zero means
that the condition is false.
In computer jargon, when one piece of program leaves a value as a signal
for another piece of program, that value is called a "flag".
Try entering the following phrases at the terminal, letting . show you
what's on the stack as a flag.
5 4 > . 1 ok
5 4 < . 0 ok
TRUE/FALSE in pForth
In pForth the value of true is -1 and not +1, false is still zero.
PForth V2.0.1, LE/64, built May 12 2023 10:51:31 (static)
5 4 > . -1 ok
Stack<10>
5 4 < . 0 ok
Stack<10>
(It's okay to use comparison operators directly at your terminal like
this, but remember that an IF ... ELSE statement must be wholly
contained within a definition because it involves branching.)
IF will take a one (-1 in pForth) as a flag that means true and a zero
as a flag that means false. Now let's take a closer look at NOT , which
reverses the flag on the stack.
0 NOT . 1 ok
1 NOT . 0 ok
PForth V2.0.1, LE/64, built May 12 2023 10:51:31 (static)
0 NOT . -1 ok
Stack<10>
1 NOT . 0 ok
Stack<10>
-1 NOT . 0 ok
Stack<10>
Now we'll let you in on a little secret; IF will take any non-zero value
to mean true. The fact that an arithmetic zero is identical to a flag
that means "false" leads to some interesting results.
For one thinkg, if all you want to test is whether a number is zero, you
don't need a comparison operator at all. For example, a slightly simpler
version on /CHECK, which we saw earlier, could be:
: /CHECK DUP IF / ELSE ." INVALID" DROP THEN ;
Here's another interesting result. Say you want to test whether a number
is an even multiple of ten, such as 10, 20, 30, 40, etc. You know that
the phrase 10 MOD divides by ten and returns the remainder only. An even
multiple of ten would produce a zero remainder, so the phrase 10 MOD 0=
gives the appropriate "true" or "false" flag.
If you think about it, both 0= and NOT do exactly the same thing: they
change zeroes to ones and non-zeroes to zeroes. They have different names
because one makes more sense dealing with numbers, the other with flags.
Still another interesting result is that you can use - (minus) as a
comparison operator which tests whether two values are "not equal". When
you subtract two equal numbers, you get zero (false); when you subtract
two unequal numbers, you get a non-zero value (true).
And a final result is described in the next section.
A Little Logic
It's possible to take several flags from various tests and combine them
into a single flag for one IF statement. You might combine them as an
"either/or" decision, in which you make two comparison tests. If either
or both of the tests are true, then the computer will execute something.
If neither is true, it won't.
Here's a rather simple-minded example, just to show what we mean. Say you
want to print the name "ARTICHOKE" if an input number is either negative
or a multiple of ten. Consider the phrase:
DUP 0< SWAP 10 MOD 0= +
Here's what happens when the input number is, say, 30:
Operator Contents Operation
of stack
----------------------------------------------------------------
30
DUP 30 30 Duplicates it so we can test it twice.
0< 30 0 Is it negative? No (zero).
SWAP 0 30 Swap the flag with the number.
10 MOD 0= 0 1 Is it evenly divisible by 10? Yes (one).
+ 1 Adds the flags.
What happens when you add flags? The result is a true if either or both
conditions are ture. In this example, the result is one, which means
"true". If the input number had been -30, then both conditions would have
been true and the sum would have been two. Two is, of course, non-zero.
So as far as IF is concerned, two is as true as one.
Our simple-minded definition, then, would be:
: VEGETABLE DUP 0< SWAP 10 MOD 0= + IF ." ARTICHOKE" THEN ;
Here's an improved version of a previous example called ?DAY .
: ?DAY DUP 1 < SWAP 31 > + IF ." NO WAY" ELSE ." THANK YOU" THEN ;
The above two examples will always work because any "true" flags will
always be exactly 1. In some cases, however, a flag may be any non-zero
value, not just "1", in which case it's dangerous to add them with + .
For example, 1 -1 + . gives us a mathematically correct answer but not
the answer we want if 1 and -1 are flags.
For this reason, FORTH supplies a word called OR , which will return the
correct flag even in the case of 1 and -1. An "or decision" is a computer
term for the kind of flag combination we've been discussing. For example,
if either the front door or the back door is open (or both), flies will
come in.
Another kind of decision is called an "and" decision. In an "and"
decision, both conditions must be true for the result to be true. For
example, the front door and the back door must both be open for a breeze
to come through. If there are three or more conditions, they must all be
true.
How can we do this in FORTH? By using handy word AND . Here's what AND
would do with the four possible compbinations of flags we saw earlier:
flag1 flag2 AND
------------------
0 0 0
1 0 0
0 1 0
1 1 1
In other words, only the combination 1 1 AND produces a result of one.
AND in pForth
PForth V2.0.1, LE/64, built Jan 26 2024 21:06:14 (static)
0 -1 AND . 0 ok
Stack<10>
0 1 AND . 0 ok
Stack<10>
-1 -1 AND . -1 ok
Stack<10>
1 1 AND . 1 ok
Stack<10>
5 5 AND . 5 ok
Stack<10>
5 1 AND . 1 ok
Stack<10>
5 2 AND . 0 ok
Stack<10>
4 2 AND . 0 ok
Stack<10>
6 2 AND . 2 ok
Stack<10>
In the examples above the two cases 5 2 AND and 4 2 AND returned FALSE
while in both cases the input flags are non-zero, TRUE, values. The
reason for this is the pForth implementation of the AND word. It is
implemented using the bitwise AND C-language operator.
Back to the book...
Let's say we're looking for a cartboard box that's big enough to fit a
disk drive which measures: height 6", width 19" and length 22"
The height, width and length requirements all must be satisfied for the
box to be big enough. If we have the dimensions of a box on the stack,
then we can define:
: BOXTEST (length width heigth -- )
6 > ROT 22 > ROT 19 > AND AND
IF ." BIG ENOUGH" THEN ;
Notice that we've put a comment inside the definition, to remind us of
stack effects. This is particularly wise when the stack order is
potentionally confusing or hard to remember. You can test BOXTEST with
the phrase:
23 20 7 BOXTEST BIG ENOUGH ok
As your applications become more sophisticated, you will be able to write
statements in FORTH that look like postfix English and are very easy to
read. Just define the individual words within the definition to check
some condition somewhere, then leave a flag on the stack. An example:
: SNAPSHOT ?LIGHT ?FILM AND IF PHOTOGRAPH THEN ;
The example above checks that there is available light and that there is
film in the camera before taking the picture. Another example, which
might be used in a computer-dating application, is:
: MATCH HUMOROUS SENSITIVE AND
ART.LOVING MUSIC.LOVING OR AND SMOKING NOT AND
IF ." I HAVE SOMEONE YOU SHOULD MEET" THEN ;
Words like HUMOROUS and SENSITIVE have been defined to check a record in
a disk file that contains information on other applicants of the
appropriate sex.
Two Words with Built-in IFs
?DUP
The word ?DUP duplicates the top stack value only if it is non-zero.
This can eliminate a few surplus words. For example the definition
: /CHECK DUP IF / ELSE DROP THEN ;
can be shortened to:
: /CHECK ?DUP IF / THEN ;
ABORT"
It may happen that somewhere in a complex application an error might
occur (such as division by zero) way down in one of the low-level words.
When this happens you don't just want the computer to keep on going, and
you also dont't want it to leave anything on the stack.
If you think such an error might occur, you can use the word ABORT" .
ABORT" expects a flag on the stack: a "true" flag tells it to "abort",
which in turn clears the stack and returns execution on the terminal,
waiting for someone to type something. ABORT" also prints the name of
the last interpreted word, as well as whatever message you want.
Let's illustrate. We hope you're not sick of /CHECK by now, because here
is yet another version:
: /CHECK DUP 0= ABORT" ZERO DENOMINATOR" / ;
In this version, if the denominator is zero, any numbers that happen to
be on the stack will be dropped and the terminal will show:
8 0 /CHECK /CHECK ZERO DENOMINATOR
Testing the code above in pForth
PForth V2.0.1, LE/64, built May 12 2023 10:51:31 (static)
: /CHECK DUP 0= ABORT" ZERO DENOMINATOR" / ; ok
Stack<10>
8 0 /CHECK ZERO DENOMINATOR
Stack<10> 8 -2
THROW code = -2
ABORT
Code -2 is the code for abort", can be found in fth/system.fth .
Pressing ENTER results in:
PForth V2.0.1, LE/64, built May 12 2023 10:51:31 (static)
: /CHECK DUP 0= ABORT" ZERO DENOMINATOR" / ; ok
Stack<10>
8 0 /CHECK ZERO DENOMINATOR
Stack<10> 8 -2
THROW code = -2
ABORT
ok
Stack<10>
The terminal returns control to the user and the stack is empty.
Back to the book...
Just as an experiment, try putting /CHECK inside another definition:
: ENVELOPE /CHECK ." THE ANSWER IS" . :
and try:
8 4 ENVELOPE THE ANSWER IS 2 ok
8 0 ENVELOPE ENVELOPE ZERO DENOMINATOR
The point is that when /CHECK aborts, the rest of ENVELOPE is skipped.
Also notice that the name ENVELOPE, not /CHECK is printed.
A useful word to use in conjunction with ABORT" is ?STACK , which
checks for stack underflow and returns a true flag if it finds it. Thus
the phrase:
?STACK ABORT" STACK EMPTY"
aborts if the stack has underflowed.
FORTH uses the identical phrase, in fact. But is waits until all of your
definitions have stopped executing before it performs the ?STACK test,
because checking continuosly throughout execution would needlessly slow
down the computer. You're free to insert a ?STACK ABORT" phrase at any
critical or not-yet-tested portion of your application.
Footnote: For computer Philosophers
FORTH provides certain error checking automatically. But because the
FORTH operating system is so easy to modify, users can readily control
the amount of error checking their system will do. This flexibility lets
users make their own tradeoffs between convenience and execution speed.
Here's a list of the FORTH words we've covered in this chapter:
IF xxx IF: (flag -- ) If flag is true (non-zero) executes xxx;
ELSE yyy otherwise executes yyy; continues with zzz
THEN zzz regardless. The phrase ELSE yyy is optional.
= (n1 n2 -- flag) Returns true if n1 and n2 are equal.
- (n1 n2 -- n-diff) Returns true (i.e., the non-zero difference)
if n1 and n2 are not equal.
< (n1 n2 -- flag) Returns true if n1 is less than n2.
> (n1 n2 -- flag) Returns true if n1 is greater than n2.
0= (n -- flag) Returns true if n is zero (i.e., reverses
the truth value)
0< (n -- flag) Returns true if n is negative.
0> (n -- flag) Returns true if n is positive.
NOT (flag -- flag) Reverses the result of the previous test;
equivalent to 0=.
AND (n1 n2 -- and) Returns the logical AND.
OR (n1 n2 -- or) Returns the logical OR.
?DUP (n -- n n) Duplicates only if n is non-zero.
or (0 -- 0)
ABORT" xxx" (f -- ) If the flag is true, types out the last word
interpreted, followed by the text. Also
clears the user's stack and returns control
to the terminal. If false, takes no action.
?STACK ( -- flag) Returns true if a stack underflow
condition has occured.
Review of Terms
Abort
as a general computer term, to abruptly cease execution if a condition
occurs which the program is not designed to handle, in order to avoid
producing nonsense or possibly doing damage
"And" decision
two conditions that are combined such that if both of them are true, the
result is true.
Branching
breaking the normally straightforward flow execution, depending on
conditions if effect at the time of execution. Branching allows the
computer to respond differently to different conditions.
Comparison operator
in general, a command that compares one value with another (for example,
determines whether one is greater than the other) and sets a flag
accordingly, which normally will be checked by a conditional operator. In
FORTH, a comparison operator leaves the flag on the stack.
Conditional operator
a word, such as IF , which routes the flow of execution differently
depending on some condition (true or false).
Flag
as a general computer term, a value stored in memory which serves as a
signal as to whether some known condition is true or false. Once the
"flag is set", any number of routines in various parts of a program may
check (or reset) the flag, as necessary.
Logic
in computer terminology, the system of representing conditions in the
form of "logical variables", which can be either true or false, and
combining these variables using such "logical operators" as "and", "or",
and "not", to form statements which may be true or false.
Nesting
placing a branching structure within an outer branching structure.
"OR" decision
two conditions that are combined such that if either of them is true, the
result is true.
toolsV2 yet_another_webdev(blog)