Improving Loops
The Robot World Repo on GitHub
The Forth Repo on GitHub
#ElonaldDelendaEst! Applying 0BR, BR, and the new CompileInfo protocol in conditionals has resulted in simpler and more consistent code. Let’s take a look at loops. BEGIN-UNTIL goes nicely. Just one small misstep.
Right. Here’s BEGIN-UNTIL:
def _define_begin_until(self):
def _until(forth):
forth.compile_word('*UNTIL')
jump_loc = forth.compile_stack.pop()
forth.append_word(jump_loc - len(forth.word_list) - 1)
self.pw('BEGIN', lambda f: f.compile_stack.push(len(f.word_list)), immediate=True)
self.pw('UNTIL', _until, immediate=True)
Wow, that’s just about as heavy with simplicity as it can be, except that it is using a skip distance, not an address (index) in the word, and using *UNTIL
, which is defined like this:
def define_skippers(self, forth):
def _next_word(forth):
return forth.next_word()
def _zero_skip(forth):
branch_distance = _next_word(forth)
if forth.stack.pop() == 0:
forth.active_word.skip(branch_distance)
self.pw('*UNTIL', _zero_skip)
We see that BEGIN
pushes just the length of the word list onto the compile stack, as we did in the old days. If we create a suitable CompileInfo and push that, and unwind it for the until, we should just be able to use BR
instead of *UNTIL
.
We’ll begin by providing a function to edit rather than the lambda in BEGIN
. I do like the lambdas for their compactness, but they can only do one expression.
def _begin(forth):
forth.compile_stack.push(len(forth.word_list))
self.pw('BEGIN', _begin, immediate=True)
Unsurprisingly, we remain green. Commit: moving BEGIN-UNTIL toward using CompileInfo and BR.
Now we want to push a CompileInfo with a suitable name and the current location, onto the compile stack instead of that vanilla length. This will probably break things.
In times like these in the past (like yesterday) we called compile_branch
in Forth:
class Forth:
def compile_branch(self, branch_name, info_name):
self.compile_word(branch_name)
info = CompileInfo(info_name, self.word_list)
self.compile_stack.push(info)
self.update_branch()
self.compile_word('BR_TARGET')
def update_branch(self):
top = self.compile_stack.top()
top.add_current_location(top.name)
That won’t quite do, as we do not wish to compile any code just now. Let’s write out what we need to do longhand, and then see if we can reduce duplication. I think this is right:
def _begin(forth):
info = CompileInfo('BEGIN', forth.word_list)
forth.compile_stack.push(info)
info.add_current_location('BEGIN')
Three tests fail. I expected some failures. And we’re not yet even using the CompileInfo that we just stashed. Let’s verify what the tests are saying.
def _until(forth):
forth.compile_word('*UNTIL')
jump_loc = forth.compile_stack.pop()
> forth.append_word(jump_loc - len(forth.word_list) - 1)
E TypeError: unsupported operand type(s) for -: 'CompileInfo' and 'int'
Right, that looks consistent with what I’d expect: it sees a CompileInfo and has no clue. I think this function needs to be:
def _define_begin_until(self):
def _until(forth):
forth.compile_word('BR')
info = forth.compile_stack.pop()
forth.append_word(info.locations[0])
Curiously, one test now passes, but two still don’t. I am confused. Let’s see what they’re saying.
The failing tests both stack is full
. That makes me think that we jumped too far. And notice that -1 in the original _until
. I fiddle with the branch value to no avail. Let’s look at the test code and see what we can see. And maybe we can create a simpler example that will fail.
def test_begin_until_hard(self):
f = Forth()
f.process_line(': DOUBLE 2 * ;')
f.process_line(': DOUBLE_UNDER SWAP DOUBLE SWAP ;')
s = ': TEST 2 5 BEGIN DOUBLE_UNDER 1- DUP 0 >= UNTIL ;'
f.process_line(s)
f.process_line('TEST')
assert f.stack.pop() == 0
assert f.stack.pop() == 64
def test_power(self):
f = Forth()
f.process_line(': 2ROT ROT ROT ;')
pow_step = (': POW_STEP '
'(prod base -- prod*base base)'
'DUP ROT * SWAP ;')
f.process_line(pow_step)
f.process_line(': POWER'
'(base power -- base**power) '
'1 2ROT (1 base power)'
'BEGIN 2ROT (power 1 base)'
'POW_STEP (power product base)'
'ROT (product base power)'
'1- DUP 0 = UNTIL (product base power)'
'DROP DROP (product) ;')
f.process_line(' 3 4 POWER ')
assert f.stack.pop() == 81
Let’s decompile the power one and see what it’s doing.
: POWER *# 1 2ROT 2ROT POW_STEP ROT 1- DUP *# 0 = BR 2 DROP DROP ;
0 1 2 3 4 5 6 7 8 9 10 11 12 13
Oh! We need to compile 0BR, not BR! Duh!
def _define_begin_until(self):
def _until(forth):
forth.compile_word('0BR')
info = forth.compile_stack.pop()
forth.append_word(info.locations[0])
Green. Commit: BEGIN-UNTIL using 0BR and CompileInfo. Look for duplication and code sharing.
Reflection
So, setting aside my use of the unconditional branch, that went very well. However, the mistake did distract me, and raised my hackles, so a moment’s settling will be useful.
Let’s look for duplication and possibilities for code sharing now.
Our current changes:
def _define_begin_until(self):
def _until(forth):
forth.compile_word('0BR')
info = forth.compile_stack.pop()
forth.append_word(info.locations[0])
def _begin(forth):
info = CompileInfo('BEGIN', forth.word_list)
forth.compile_stack.push(info)
info.add_current_location('BEGIN')
self.pw('BEGIN', _begin, immediate=True)
self.pw('UNTIL', _until, immediate=True)
The Forth support for branching and CompileInfo:
def compile_branch(self, branch_name, info_name):
self.compile_word(branch_name)
info = CompileInfo(info_name, self.word_list)
self.compile_stack.push(info)
self.update_branch()
self.compile_word('BR_TARGET')
def update_branch(self):
top = self.compile_stack.top()
top.add_current_location(top.name)
We can use update_location
in _begin
. That might lead somewhere good.
def _begin(forth):
info = CompileInfo('BEGIN', forth.word_list)
forth.compile_stack.push(info)
forth.update_branch()
Ah. The two lines creating and pushing the info are mysteriously similar:
def _begin(forth):
'here'; info = CompileInfo('BEGIN', forth.word_list)
'here'; forth.compile_stack.push(info)
forth.update_branch()
def compile_branch(self, branch_name, info_name):
self.compile_word(branch_name)
'here'; info = CompileInfo(info_name, self.word_list)
'here'; self.compile_stack.push(info)
self.update_branch()
self.compile_word('BR_TARGET')
Extract method in Forth:
def compile_branch(self, branch_name, info_name):
self.compile_word(branch_name)
self.push_compile_info(info_name)
self.update_branch()
self.compile_word('BR_TARGET')
def push_compile_info(self, info_name):
info = CompileInfo(info_name, self.word_list)
self.compile_stack.push(info)
Call that:
def _begin(forth):
forth.push_compile_info('BEGIN')
forth.update_branch()
But wait, both those guys do an update! Move that line down into the push method:
class Forth:
def compile_branch(self, branch_name, info_name):
self.compile_word(branch_name)
self.push_compile_info(info_name)
self.compile_word('BR_TARGET')
def push_compile_info(self, info_name):
info = CompileInfo(info_name, self.word_list)
self.compile_stack.push(info)
self.update_branch()
def update_branch(self):
top = self.compile_stack.top()
top.add_current_location(top.name)
class Lexicon:
def _define_begin_until(self):
def _until(forth):
forth.compile_word('0BR')
info = forth.compile_stack.pop()
forth.append_word(info.locations[0])
def _begin(forth):
forth.push_compile_info('BEGIN')
self.pw('BEGIN', _begin, immediate=True)
self.pw('UNTIL', _until, immediate=True)
Commit: refining.
Convert begin back to lambda:
def _define_begin_until(self):
def _until(forth):
forth.compile_word('0BR')
info = forth.compile_stack.pop()
forth.append_word(info.locations[0])
self.pw('BEGIN', lambda f: f.push_compile_info('BEGIN'), immediate=True)
self.pw('UNTIL', _until, immediate=True)
Sweet. Commit.
Let’s reflect a bit. I want to share a thought that may be of value to you.
Reflection
You may be able to tell that I really don’t plan out or design the specific methods we wind up with in a sequence like this. I just look for duplication and exploit it. I look for code that is similar and try to make it the same and then exploit that duplication. And I try to give things meaningful names.
For me, this is very different from figuring out in my head what methods I need and how to get them just right. I fancy I could still do that if I tried, but I rarely try. Instead I just find or create duplication and then exploit the duplication by breaking out bits and calling them.
I get code that I like, and it’s almost as if I don’t have to think about it. It definitely is that I don’t have to think hard about it.
Just some thoughts that you might find useful.
Now what about the until? Is there commonality we can exploit there?
Generally, that is to say in the conditionals we have done over the past couple of days, we have compiled a branch and then a BR_TARGET and then used CompileInfo’s ability to patch. But that usage is the reverse of what we do here. Usually we patch the word list at the addresses the CompileInfo contains to point to the last word in the word list, which will be the branch target:
def patch(self, name):
assert self.name == name, f'expected {name}, found {self.name}'
if not self.locations:
print(f'{self.name} has no locations')
for location in self.locations:
self.word_list[location] = len(self.word_list)
In UNTIL
we do the reverse:
def _until(forth):
forth.compile_word('0BR')
info = forth.compile_stack.pop()
forth.append_word(info.locations[0])
We get the locations (there is only one) from the info and we put it into the last word in the word list (And we allocate that word. patch
does not allocate the word. In the case of patch
we are branching past all the existing code, to drop into whatever’s next.)
I’m not ready to defer this _until
operation over to CompileInfo, at least not yet. We’ll see whether more duplication arises as we go forward with DO-LOOP
. I think it will be quite similar to what we did today, but we’ll find out.
Summary
Enough for one morning. We’re 90 minutes in and at a perfect stopping point with another pair of words converted to our standard CompileInfo scheme, with a new reduction of code without loss of capability. If that’s not adding simplicity, I don’t know what is!
#ElonaldDelendaEst! See you next time!