The Robot World Repo on GitHub
The Forth Repo on GitHub

#StopTheCoup! Let’s go after those skipped tests. I think they just need recasting. All goes pretty well!

I’ll start with just one and see what it takes to make it good:

    def test_destroy_world(self):
        f = Forth()
        f.process_line(': EXECUTE BEGIN 666 0 UNTIL ;')
        with pytest.raises(ValueError) as e:
            f.process_line(' EXECUTE ')
        assert str(e.value) == 'Stack is full'

This passes:

    def test_destroy_world(self):
        f = Forth()
        f.process_line(': EXECUTE BEGIN 666 0 UNTIL ;')
        f.process_line(' EXECUTE ')
        assert f.result == 'Stack is full ?'

I think it’d be better if we were to return the result all along, as well as set it.

    def test_destroy_world(self):
        f = Forth()
        f.process_line(': EXECUTE BEGIN 666 0 UNTIL ;')
        result = f.process_line(' EXECUTE ')
        assert result == 'Stack is full ?'

We adjust these two methods to return the result:

    def process_line(self, text):
        clean_line = re.sub(r'\(.*?\)', ' ', text)
        provider = StringProvider(clean_line)
        return self.main_loop(provider)

    def main_loop(self, provider):
        self.provider = provider
        while self.provider.has_tokens():
            try:
                self.process_token(self.provider.next_token())
            except Exception as e:
                self.result = f'{e} ?'
                self.abend()
            else:
                self.result = 'ok'
        if self.compilation_state:
            raise ValueError('Unexpected end of input')
        return self.result

I think the same treatment should apply to all the others, I’ll report back if that’s not the case.

I’m down to one failure, after this change:

    def main_loop(self, provider):
        self.provider = provider
        while self.provider.has_tokens():
            try:
                self.process_token(self.provider.next_token())
            except Exception as e:
                self.result = f'{e} ?'
                self.abend()
            else:
                self.result = 'ok'
        if self.compilation_state:
            return 'Unexpected end of input ?'
        return self.result

I return the string rather than raise the exception. This fails:

    def test_safe_compile_needs_more_input(self):
        f = Forth()
        result = f.compile(': FOO 42 ')  # no semicolon
        assert result == ''

This is getting ok back. And the fix is in compile. Now we are green and have:

    def compile(self, text):
        return self.process_line(text)

    def process_line(self, text):
        clean_line = re.sub(r'\(.*?\)', ' ', text)
        provider = StringProvider(clean_line)
        return self.main_loop(provider)

    def main_loop(self, provider):
        self.provider = provider
        while self.provider.has_tokens():
            try:
                self.process_token(self.provider.next_token())
            except Exception as e:
                self.result = f'{e} ?'
                self.abend()
            else:
                self.result = 'ok'
        if self.compilation_state:
            return 'Unexpected end of input ?'
        return self.result

Commit: skipped tests all recast, minor changes to return result from main loop.

We should inline process_line into compile. I think PyCharm will do the work for us.

PyCharm helps but I wind up doing a global replace to get it all done. Now everyone calls compile and the code is this:

    def compile(self, text):
        clean_line = re.sub(r'\(.*?\)', ' ', text)
        provider = StringProvider(clean_line)
        return self.main_loop(provider)

    def main_loop(self, provider):
        self.provider = provider
        while self.provider.has_tokens():
            try:
                self.process_token(self.provider.next_token())
            except Exception as e:
                self.result = f'{e} ?'
                self.abend()
            else:
                self.result = 'ok'
        if self.compilation_state:
            return 'Unexpected end of input ?'
        return self.result

    def process_token(self, token):
        definition = self.get_definition(token)
        if not self.compilation_state or definition.immediate:
            definition(self)
        else:
            self.append_word(definition)

Commit: combining process_line and compile into compile.

Now can’t we change main to use the returned result? I think we’ll find a need to deal with the … prompt.

ok
Forth>2 2 + .
4 ok
Forth>: c 32 -
ok
...

I don’t know about the ok before the ‘…’. Let’s check KeyboardProvider:

class KeyboardProvider:
    def next_token(self):
        if not self.provider.has_tokens():
            print(self.forth.result)
            prompt = 'Forth>'
            if self.forth.compilation_state:
                prompt = '...'
            self.set_line(input(prompt))
        return self.provider.next_token()

OK, try this:

    def next_token(self):
        if not self.provider.has_tokens():
            prompt = 'Forth>'
            if self.forth.compilation_state:
                prompt = '...'
            else:
                print(self.forth.result)
            self.set_line(input(prompt))
        return self.provider.next_token()

That gives us this result, losing the error on ‘whee’:

ok
Forth>2 2 + .
4 ok
Forth>: c 32 -
...5 * 9 / . ;
ok
Forth>32 c
0 ok
Forth>
ok
Forth>whee

Process finished with exit code 0

That won’t do. Is that working at all?

No, it wasn’t working before my change.

After some futzing, this is OK but not ideal:

if __name__ == '__main__':
    forth = Forth()
    provider = KeyboardProvider(forth)
    while True:
        result = forth.main_loop(provider)
        print(result)

class KeyboardProvider:
    def next_token(self):
        if not self.provider.has_tokens():
            prompt = 'Forth>'
            if self.forth.compilation_state:
                prompt = '...'
            self.set_line(input(prompt))
        return self.provider.next_token()

That gives this at the command prompt.

Forth>2 2 + .
4 Forth>whee
Syntax error: "WHEE" unrecognized ?
Forth>3 4 + .
7 Forth>: c 32
...- 5 * 9 / ;
Forth>32 c .
0 Forth>

I am not entirely satisfied with this but it is passing all the tests and looks decent at the prompt. Commit: adjusting KeyboardProvder and main.

Let’s sum up.

Summary

As I expected, fixing up the skipped tests was easy and involved no big surprises in the code. We are still kind of wandering around among exceptions and errors. The bye command still exits, though, because it is doing this:

    self.pw('BYE', lambda f: sys.exit())

So we’re not trapped. I do want to consider further how to manage errors and ‘ok’ from main, but aside from some odd-looking code, I think we’re in a good place. We’ll review further next time.

#StopTheCoup! See you next time!