Forth: IF-THEN??
Our next story is IF-THEN. How might we do this pair of Words? Triple, if you count ELSE.
As we talked about a couple of articles back, real Forth implementations treat IF as an “immediate” word during compilation. They compile in a word *IF, which has room for an integer, the number of words to skip if the if is not true. They save the current index in the lexicon on the stack. When they later encounter THEN or ELSE, those are also immediate, and they pop the stack, subtract that from their current location in the lexicon, and patch it into the *IF’s parameter location.
I think that while we could put a spurious value right into the SecondaryWord’s list, not to be executed, that would be bad. So I think we should have a parameter field in our word definition for *IF, and the THEN and ELSE operations should set it.
It seems to me that *IF and the others are primaries. We’ll begin by giving all the primaries a new member variable. Wasteful, but we don’t have the same concerns over memory that we had back in 1970. If I recall correctly …
I see vaguely, how we might proceed. We can hand-roll an *IF and test it, so then we’ll know what it has to do. But we can see, even now, that what it will need to do is to communicate with the SecondaryWord we are running, to tell it to skip forward N words, so as to jump over the IF if it’s not to be executed.
As things stand, our Words have no access to the current SecondaryWord being executed, if there is one. We could provide it here:
class SecondaryWord:
def do(self, forth):
lexicon = forth.lexicon
for word_index in self.word_indices:
lexicon[word_index].do(forth) # <== HERE
Another possibility might be for each secondary to send a message to its forth
parameter, telling it who’s running. Then, since the words have access to forth, they could message forth and it could message the Secondary and tell it to do what is needed.
However … secondaries call secondaries, so the forth instance would have to have a stack of active secondaries. That seems nasty.
We see two ways. One of them requires a new parameter on every word’s code. The other requires us to maintain a stack of words in use, and to maintain it correctly. That said, the code involved is short and sweet. I think.
What it might be like is this:
class Forth:
def __init__(self):
self.stack = Stack()
self.lexicon = []
self.active_words = []
self.define_primaries()
def begin(self, secondary):
self.active_words.append(secondary)
def end(self):
self.active_words.pop()
def active_word(self):
return self.active_words[-1]
And then in SecondaryWord, maybe:
def do(self, forth):
forth.begin(self)
lexicon = forth.lexicon
for word_index in self.word_indices:
lexicon[word_index].do(forth)
forth.end()
I have no tests for that, I just typed it in to see what it might be like. I can enhance a test.
def test_compiler_hyp(self):
f = Forth()
f.compile(': SQUARE DUP * ;')
f.compile(': HYPSQ SQUARE SWAP SQUARE + ;')
f.compile(': HYP HYPSQ SQRT ;')
f.stack.extend([3, 4])
f.find_word('HYP').do(f)
assert f.stack.pop() == 5
assert f.active_words == []
Green. Of course it would also be green if we just defined the active words and never touched it.
I think we’ll follow this thread a bit and see whether along the way we become confident that this new stack is implemented correctly. (Actually, I’m confident that it works now. I would like to be more confident that if we break it in any of the various ways we might, tests will break also.)
Let’s create a new Primary for *IF and test it. This will take a few iterations, because I really cannot see all the connections, though I am confident that we’ll see them by the end, and it will be good. If it’s not good, it’s not the end.
I decide that I need to divert and allow the compilation of numbers. No, never mind, I won’t divert.
def test_star_if(self):
f = Forth()
s = ': TEST *IF DUP + ;'
f.compile(s)
f.stack.extend([2, 0])
f.find_word('TEST').do(f)
assert f.stack.pop() == 2
If this worked as intended, the *IF pops and sees zero and skips the next two words. So the dup plus does not happen and the stack still contains 2.
Of course just now:
> raise ValueError(f'cannot find word "{word}"')
E ValueError: cannot find word "*IF"
Let’s define a new PW, named *IF, and have it do nothing.
class Forth:
def define_primaries(self):
lex = self.lexicon
lex.append(PrimaryWord('*IF', lambda f: f.star_if()))
...
def star_if(self):
pass
At this point, I expect the test to fail with 4 not 2, but no, it fails with 0 because *IF does nothing, not even consuming its input. We fix that:
def star_if(self):
self.stack.pop()
And we get what we expect:
tests/test_compile.py:69 (TestCompile.test_star_if)
4 != 2
Let’s proceed by Wishful Thinking. We’ll fetch the current word and tell it to skip 2:
def star_if(self):
self.stack.pop()
self.active_word().skip(2)
This fails, of course, because SecondaryWord does not know skip
:
def star_if(self):
self.stack.pop()
> self.active_word().skip(2)
E AttributeError: 'SecondaryWord' object has no attribute 'skip'
So far, so good, it continues not working in ways we expect. For SecondaryWord to be able to skip (I’m supposing backward skips will be allowed, by the way, not that it matters now) it will have to keep a separate index and increment it:
class SecondaryWord:
# why don't we just store the word in the lexicon? it's no larger than the index.
def __init__(self, name, word_indices):
self.name = name
self.word_indices = word_indices
self.word_index = 0
def do(self, forth):
forth.begin(self)
lexicon = forth.lexicon
self.word_index = 0
while self.word_index < len(lexicon):
lexicon[self.word_index].do(forth)
self.word_index += 1
forth.end()
I rather expected that to leave our one test failing but another has failed.
That’s just wrong and I back up and do again.
def do(self, forth):
forth.begin(self)
lexicon = forth.lexicon
self.ix = 0
while self.ix < len(self.word_indices):
word_index = self.word_indices[self.ix]
lexicon[word_index].do(forth)
self.ix += 1
forth.end()
Missed a level of indirection. Now down to just our one test failing and I think if we were to implement skip
good things might happen.
def skip(self, n):
self.ix += n
Green. Commit: initial *IF working.
Time for a break, let’s sum up.
Summary
One lesson: Often when we type in a few lines that “should” work and they do not, the right move is just to erase them and do over, rather than try to debug them.
I think we have a legitimate proof of concept / initial implementation of *IF here. We’ll “just” need to give the PrimaryWord a parameter field, set it during compilation via some horrendous tricks we can see vaguely, adjust *IF to actually check its parameter … no wait, let’s do that now. It’s easy, we’ll get it out of the way. Extend the test:
def test_star_if(self):
f = Forth()
s = ': TEST *IF DUP + ;'
f.compile(s)
f.stack.extend([2, 0])
f.find_word('TEST').do(f)
assert f.stack.pop() == 2
f.stack.extend([2, 1])
f.find_word('TEST').do(f)
assert f.stack.pop() == 4
And …
def star_if(self):
flag = self.stack.pop()
if flag != 0:
self.active_word().skip(2)
That does not work! Why not? I should have stopped while I was ahead.
Another lesson: When tired, stop. (Special case of “When you find yourself in a hole, stop digging”?)
OK, what I didn’t notice is that it’s the FIRST assertion that has failed. That’s because 0 is false and we want to skip on false, tiny fool!
def star_if(self):
flag = self.stack.pop()
if flag == 0:
self.active_word().skip(2)
Green. Commit, starIF works.
Now as I was saying, we have *IF almost certainly working. It remains to give it its skip distance as a parameter and to deal with the compiler side of things. I am optimistic about that but I am tired and when tired it is wise to stop before we type !=
when we should type ==
.
This is going nicely, silly mistakes notwithstanding. All my mistakes this morning have been trivial, and fixed essentially immediately. A good morning indeed!
See you next time!