Forth: 1 IF TODAY THEN
OK, after those nice improvements, let’s get started on IF-THEN. Really. We complete *IF with a few lines of code.
Cunning Plan
We’ll do the simplest IF-THEN I can think of, no else or any of that for the first run.
The way IF-THEN work in Forth is “simple”. The sequence
flag IF WORD_1 WORD_2 WORD_ETC THEN
Will execute the words inside the If_THEN pair if flag is true (non-zero). If the flag is zero, all those words will be skipped.
To make this work, we’ll compile in a two-word entry for if
*IF 0
At run time, the zero is expected to contain the integer number of following words to skip. In the case of the example above, it needs to contain 3. But we don’t know yet how many words have gone by. So we’ll just put zero, and we’ll stack the current position in the word list we’re building.
Then, when we see the THEN, we’ll pop the stack, do the subtract, and change the zero to the necessary value, in this case, 3. We will not need to compile any new code for the THEN. The result will be
*IF 3 WORD_1 WORD_2 WORD_ETC
Is this confusing? Threaded code is like that.
Forth solves this kind of thing with “immediate” words, words that are flagged in the lexicon so that they are executed instead of just stuffed into the output word list. So the IF word would be an immediate word that compiles the *IF and the zero.
We’ll get there, but I think we’ll start with ad hoc code.
I also think we have an ignored test or two that may be useful. Here’s one for the *IF:
@pytest.mark.skip(reason='not implemented')
def test_star_if(self):
f = Forth()
s = ': TEST *IF DUP + ;'
test_word = f.compile(s)
star_if = f.find_word('*IF')
star_if.parameter = 2
f.stack.extend([2, 0])
test_word.do(f)
assert f.stack.pop() == 2
f.stack.extend([2, 1])
test_word.do(f)
assert f.stack.pop() == 4
Now that test assumes that we’ll use the Word’s parameter
field. I now think that we’ll do it Forth style and put the value after the word. So this test needs to be changed to create the code we want. There is no way to put a zero all by itself into the list. This test will have to be very clever to build the list we need, unless we just build it by hand, like this:
def test_star_if_by_hand(self):
f = Forth()
words = []
words.append(f.find_word('*IF'))
words.append(2)
words.append(f.find_word('DUP'))
words.append(f.find_word('+'))
word = SecondaryWord('TEST', words)
f.stack.extend([5, 0])
word.do(f)
assert f.stack.pop() == 2
f.stack.extend([5, 1])
word.do(f)
assert f.stack.pop() == 10
I think that’s right. It’s close enough to fail. The hand-crafted code amounts to
5 0 *IF 2 DUP +
The flag is zero, the *IF should eat that, skip 2 and leave 5 on the stack. If the flag is non-zero we should drop thru, dup the 5, add, and get 10.
There is no lexicon entry for *IF, so we need one.
lex.append(PrimaryWord('*IF', lambda f: f.star_if()))
Wishful Thinking. And:
class Forth:
def star_if(self):
jump = self.next_word()
flag = self.stack.pop()
if not flag:
self.active_word.skip(jump)
The test runs. Since the tests run as I’m typing I saw some passes when I didn’t think it should. I want a bit more info, so I slip in some prints and trace the test’s execution. It’s doing the right thing.
Summary
I’m two hours in. Could press on but it is Christmas Eve Eve, and there are rumors of a mouse in the garage, and I do not want the mice eating the insides of our cars, so I think I’ll break here. I’d like to push on with the IF but I think it’s more prudent to have a bit of a break.
We have a new PrimaryWord, *IF, that expects a flag on the stack, and an integer N following it. It pops the flag and skips over the integer (by calling next_word
) and then if the flag is not true, skips N more words, handling the NOT case of the IF.
A point of interest—at least to me—is that the words we implement are all so nice and simple. That’s down to the inventor of Forth, Chuck Moore, and no particular credit to me as I follow in the footsteps of giants. But from the viewpoint of a long-term OO programmer, steeped in Smalltalk, these small methods feel to me to be the kind of code we like to see.
After IF is done, we’ll look around and see what isn’t so simple, and see what we can learn from that.
One more thing: my tentative plan for IF is to detect the word itself, in compile, handle it with code right there, and then refactor toward the notion of “immediate” words. I think that will let us take smaller steps.
Like this:
def compile_word_list(self, rest):
word_list = []
for word in rest:
if word == 'IF':
word_list.append(self.find_word('*IF'))
word_list.append(0)
elif (definition := self.find_word(word)) is not None:
word_list.append(definition)
elif (num := self.compile_number(word)) is not None:
definition = self.find_word('*#')
word_list.append(definition)
word_list.append(num)
else:
raise SyntaxError(f'Syntax error: "{word}" unrecognized')
return word_list
Hm, I accidentally typed that in. Let’s write a test:
def test_compile_if(self):
f = Forth()
s = ': TEST IF DUP + ;'
test_word = f.compile(s)
assert test_word.words[1] == 0 # the skip value
test_word.words[1] = 2 # hammer the skip value
f.stack.extend([5, 0])
test_word.do(f)
assert f.stack.pop() == 5
f.stack.extend([5, 1])
test_word.do(f)
assert f.stack.pop() == 10
That test passes. We’ve nearly implemented IF. We need to deal with the stacking of its location in the list, and with the THEN. We will do that next time. I apologize for being unable to stop, but telling you how it would work induced me to type it in.
So simple, so nice. See you next time!