The Robot World Repo on GitHub
The Forth Repo on GitHub

#ElonaldDelendaEst! Continuing the implementation of CASE-ENDCASE. I think we need to do the case-sys, collect the branch points, and fill them in. I believe we have nearly-right mechanisms in OF-ENDOF, and we’ll use those and make them more right.

Here’s what we have now in case definition:

    def define_case_of_endof_endcase(self):
        def _get_c_stack_top(f):
            f.c_stack_top = f.compile_stack[-1]

        def _case(f):
            f.compile_stack.push(CompileInfo('CASE', f.word_list))

        def _endcase(f):
            f.compile_word('DROP')
            ci = f.compile_stack.pop()
            assert ci.name == 'CASE', f'expected CASE on compile stack, found {ci.name}'

        def _of(f):
            f.compile_word('OVER')
            f.compile_word('=')
            f.compile_word('0BR')
            f.compile_stack.push(CompileInfo('OF', f.word_list, len(f.word_list)))
            f.compile_word('BR_TARGET')
            f.compile_word('DROP')

        def _endof(f):
            f.compile_word('BR')
            f.compile_word('BR_TARGET')
            f.compile_stack.pop().patch('OF')

        def _0br(f):
            address = f.next_word()
            if f.stack.pop() == f.false:
                f.active_word.branch(address)

        def _br(f):
            f.active_word.branch(f.next_word())

        def _br_target(f):
            msg = f'branch not patched in {f.active_word}'
            raise NotImplementedError(msg)

        self.pw('OF', _of, immediate=True)
        self.pw('ENDOF', _endof, immediate=True)
        self.pw('CASE', _case, immediate=True)
        self.pw('ENDCASE', _endcase, immediate=True)
        self.pw('GET_C_STACK_TOP', _get_c_stack_top, immediate=True)
        self.pw('0BR', _0br)
        self.pw('BR', _br)
        self.pw('BR_TARGET', _br_target)

And let’s have a look at what we compile, so far, for our as yet incomplete full-boat test:

    def test_for_discussion(self):
        f = Forth()
        test = (': TEST'
                '  2 CASE'
                '    1 OF 111 ENDOF'
                '    2 OF 222 ENDOF'
                '  ENDCASE '
                ';')
        expected_stack = [222]
        f.process_line(test)
        w = f.find_word('TEST')
        expected = (
            ': TEST *# 2 *# 1 OVER = 0BR 10 '
            'DROP *# 111 BR BR_TARGET *# 2 OVER = 0BR 19 '
            'DROP *# 222 BR BR_TARGET DROP ;')
        assert str(w) == expected
: TEST
  2 CASE
    1 OF 111 ENDOF
    2 OF 222 ENDOF
  ENDCASE 
;

So far, that compiles to the following, with word numbers under each word for my counting convenience:

: TEST 
  *#2 
  0
    *#1 OVER = 0BR 10
    1   2    3 4   5
      DROP *#111 BR BR_TARGET 
      6    7     8  9
    *#2 OVER = 0BR 19
    10  11   12 13 14
      DROP *#222 BR BR_TARGET 
      15   16    17 18
    DROP 
    19
;

That is, by my best estimate, the code we want, except that the two remaining BR_TARGETs need to be patched to point to the word after the final DROP, which is there to deal with no case firing, because the rules are that the case input will be gone.

The two cells after 0BR, the 10 and the 19, are correct branches to the next case check in the case of the 10, and to the final DROP in the case of the 19. I believe that the two BR-TARGETs should branch to 20, past the final DROP.

I find this hard to think about. It would be really useful to have a display of the word, nicely formatted like the one above, with the word number under each word, and with an indication of which word compiled which parts. Maybe I should work on that. But not today.

The branches that we need to patch are produced in _end_of, and should be patched in _end_case:

        def _endof(f):
            f.compile_word('BR')
            f.compile_word('BR_TARGET')
            f.compile_stack.pop().patch('OF')

        def _endcase(f):
            f.compile_word('DROP')
            ci = f.compile_stack.pop()
            assert ci.name == 'CASE', f'expected CASE on compile stack, found {ci.name}'

We have an example of setting up the target and its patching, in _of:

        def _of(f):
            f.compile_word('OVER')
            f.compile_word('=')
            f.compile_word('0BR')
            f.compile_stack.push(CompileInfo('OF', f.word_list, len(f.word_list)))
            f.compile_word('BR_TARGET')
            f.compile_word('DROP')

I think it’s going to take a bit of fancy stack work to sort this out. Let’s create a new pattern test to check for the correct branches to the end. I think it should go like this:

    def test_pattern_exits(self):
        f = Forth()
        test = (': TEST'
                '  2 CASE'
                '    1 OF 111 ENDOF'
                '    2 OF 222 ENDOF'
                '  ENDCASE '
                ';')
        expected_stack = [222]
        f.process_line(test)
        w = f.find_word('TEST')
        first_exit_location = w.index('BR') + 1
        assert w.words[first_exit_location] == 20
        second_exit_location = w.index('BR', first_exit_location) + 1
        assert w.words[second_exit_location] == 20

This code finds the BR instructions, fetches the word after that, the patched word. It compares that to 20, for now. I think that’s the answer we want. The test should fail, getting BR_TARGET instead of 2. And it does:

Expected :20
Actual   :BR_TARGET

I’m inching forward in understanding how to do this. Here’s where we should do the patching:

        def _endcase(f):
            f.compile_word('DROP')
            f.compile_stack.pop().patch('CASE')

The checking that used to be there is also done in CompileInfo:

class CompileInfo:
    def patch(self, name):
        assert self.name == name, f'expected {name}, found {self.name}'
        for location in self.locations:
            self.word_list[location] = len(self.word_list)

I want a little reassurance, so let me stuff a temporary print in there:

    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)

Now my failing test should remark to that effect:

CASE has no locations

tests/test_case.py:63 (TestCase.test_pattern_exits)
BR_TARGET != 20

Expected :20
Actual   :BR_TARGET

Perfect. I feel better now.

Now we need to define the locations needing patching, in the code that produces the BR BR_TARGET:

        def _endof(f):
            f.compile_word('BR')
            f.compile_word('BR_TARGET')
            f.compile_stack.pop().patch('OF')

The rule is that we define the location before we compile the target, as shown here:

        def _of(f):
            f.compile_word('OVER')
            f.compile_word('=')
            f.compile_word('0BR')
            f.compile_stack.push(CompileInfo('OF', f.word_list, len(f.word_list)))
            f.compile_word('BR_TARGET')
            f.compile_word('DROP')

So it needs to go here:

        def _endof(f):
            f.compile_word('BR')
            # add the current location to the case info's list
            f.compile_word('BR_TARGET')
            f.compile_stack.pop().patch('OF')

However, right now the OF is on top of the compile stack, not the CASE. How about this, then:

        def _endof(f):
            f.compile_word('BR')
            f.compile_stack.swap()
            # add this location to compile stack top (CASE)
            f.compile_stack.swap()
            f.compile_word('BR_TARGET')
            f.compile_stack.pop().patch('OF')

We swap the OF down, presumably bringing the CASE up, and give it the current location. Does CompileInfo know how to do that? It does:

class CompileInfo:
    def __init__(self, name, word_list, location=None):
        self.name = name
        self.word_list = word_list
        self.locations = []
        if location:
            self.locations.append(location)

    def add_target(self, location: int):
        self.locations.append(location)

I think the CompileInfo already knows the location we have in mind, namely len(self.word_list). Wishful thinking here:

        def _endof(f):
            f.compile_word('BR')
            f.compile_stack.swap()
            f.compile_stack.peek().add_current_location('CASE')
            f.compile_stack.swap()
            f.compile_word('BR_TARGET')
            f.compile_stack.pop().patch('OF')

We don’t have peek, nor add_current_location.

class CompileInfo:
    def add_current_location(self, expected_name):
        assert self.name == expected_name, f'expected {expected_name}, found {self.name}'
        self.add_target(len(self.word_list))

class Stack:
    def peek(self):
        return self.stack[-1]

Yes, I see that instead of swap peek swap we could do peek_under or whatever. And we will.

I am surprised, actually, to find that the test is not now passing. But no! It is the other test, for discussion! Check this out:

(TestCase.test_for_discussion)
(': TEST *# 2 *# 1 OVER = 0BR 10 DROP *# 111 BR 20 *# 2 OVER = 0BR 19 DROP *# '
 '222 BR 20 DROP ;') != 
 (': TEST *# 2 *# 1 OVER = 0BR 10 DROP *# 111 BR BR_TARGET *# 2 OVER = 0BR 19 '
 'DROP *# 222 BR BR_TARGET DROP ;')

It’s failing because the 20s are patched in, and our new test is passing! Callooh! Callay! Fix that test.

expected = (
    ': TEST *# 2 *# 1 OVER = 0BR 10 '
    'DROP *# 111 BR 20 *# 2 OVER = 0BR 19 '
    'DROP *# 222 BR 20 DROP ;')

Green. Commit: Case correctly compiled according to hand calculations.

Let’s implement peek_under and use it:

class Stack:
    def peek_under(self):
        return self.stack[-2]

PyCharm actually produced that return line as soon as I typed the r. Amazing, and that’s with the AI turned off.

In use:

class Lexicon:
    def _endof(f):
        f.compile_word('BR')
        f.compile_stack.peek_under().add_current_location('CASE')
        f.compile_word('BR_TARGET')
        f.compile_stack.pop().patch('OF')

Commit: tidying.

I’ve only been at this for 90 minutes, but this is a super place for a break. There is more refactoring to be done, but the code as it is is pretty decent and we have reached a major milestone.

Summary

I’d like to do some tests of CASE in action. It’s all well and good to inspect the compiled code but since I can write code that looks good to me but does not work, I’m pretty certain that I can write code that generates code that looks good to me but does not work. So we’ll do some operational tests.

I think we’ll want to adjust CompileInfo a bit, so that all its current uses look more similar than they now do, and we’ll want to use it in the conditionals and branching code as well. I think that CompileInfo is beginning to bear its own weight well, since it embodies recording the patch lists and patching them up all in one spot. It even checks to see that you have the expected type of CompileInfo, checking stack integrity.

So I foresee a few small refactoring steps, and some of those may lead us into places that I can’t foresee. That’s fine: I prefer to let the code participate in my design decisions, as Kent Beck said so many years ago.

A good morning. This went so smoothly! I am elated and will remain so until I check the news.

#ElonaldDelendaEst! See you next time!