The Robot World Repo on GitHub
The Forth Repo on GitHub

I did an evil thing last night. It is tempting me toward a hard right turn. Must think about this. After too long, I realize that I was distracted by the shiny thing and didn’t see its fangs.

The evil thing that I did was to ask an “AI” to write a Forth, not in Python, but in Linden Scripting Language, which is just about the least capable language I know. Evil, because the current “AI” systems are unquestionably tools of the capitalist oppressor and they consume resources, including water and electricity, at a prodigious rate. The thing about evil is that it is often fascinating, and last night’s experiment was an example.

The code it provided wouldn’t quite work. It hallucinated a void prefix on subroutines, which LSL does not provide, and it made an actual mistake in at least one operation, where it left a value on the stack that should have been popped. When I asked something like “Shouldn’t the code for IF pop the stack”, it allowed as how I was correct and produced the correct code.

But the approach it took was interesting, a pure interpreter, and I am quite sure that it would basically work. that is, I am quite sure that I could start with that code and make it work, and unless I’ve missed something, the changes would be simple local things. If there is a deep conceptual mistake in there, I do not see it.

I do not know what to think about these “AI” things. Right now, they are terrible resource hogs. Their answers are, well, “average”, almost literally. They read everything and produce answers that are statistically similar to what they have read. Since much of what they will read is wrong, they’ll often produce wrong answers. (In “fairness”, if answers they read tend toward being right, you can imagine that they might do better than average, in the same way that resampling statistics can produce better results than the original statistics.)

They will unquestionably be used to replace people. If an “AI” were providing tech support on some computerish product, at the same level of quality as I saw last night, it would be better than most of the phone support people I’ve talked with in the past year. One exception: the Apple support person I spoke with was fantastic, truly good. It was like talking with a colleague on my team who knew the stuff. But they will cost people their jobs.

We already have massive inequity in this country, much more in the world. It’s looking more and more like Hunger Games all the time. It would be quite possible for humanity to move to a post-scarcity world where the machines made things and the people lived happy and creative lives, pursuing art, or working with machines to build interesting new things, while robots tilled the soil and so on. We’re probably a long way from having a robot that could come to your house and fix the plumbing. Maybe less far from a roofing robot, perhaps guided by a human. But the point is, we could use the savings from automation to make people’s lives better, moving toward a situation where when you need something, or want something, you can get it, because the 3D printer in your basement or down the street will print it up for you.

That does not look to me to be where we are heading. Our leaders do not see a way to transition to post-scarcity without their losing their power and the power of their relative wealth. And the thing with having relatively more power and wealth is that in every transaction, the player with the larger resources tends to improve and the player with the lesser resources tends to lose. It’s very difficult to break the bank at Monte Carlo, not least because they’ll throw you out if you win too much.

I digress. But my point is, the current situation with the “AI” things is that they really are tools of the oppressor, and they really are consuming resources wildly. If we are very lucky, we’ll get through that phase to one where they don’t burn entire forests to answer a question and where anyone can print what they need, right in their own community or their own home. We’re a long way from that, we might not get there, but it’s possible.

Anyway, I’m not proud that I ran those queries last night, but I do have some hope for us, and I am unquestionably impressed with the code that the [Expletive Deleted] thing wrote for me.

Added in Post
You’ll see down at the bottom of the article that I’ve come to realize that the impressive “AI” code was much further from good enough than it appeared on the surface to be. I fell into its clever trap.

Maybe it’s a good thing. It got me thinking about how to make a better future, and also about something to do Real Soon Now.

What might you do Real Soon Now, Ron?

The Forth system that we have right now works similarly to a classical Forth, although ours relies on facilities of Python that classical Forths wouldn’t have, and ours has to do some things quite differently because Python lacks facilities that classical Forths did have, like the ability to branch anywhere they want to.

Our current Forth tokenizes its input into “tokens” separated by white space, looks up each word in its lexicon, getting an instance of Word, which is in essence a function. Some Words are primitive or primary, defined directly as code using Python lambda or def functions. The remaining Words are simple lists of Words. Once our Forth has converted the tokens into a list of Word instances, it either executes the list or tucks it away in a definition, depending on what is going on.

So the sequence is compile, then execute:

    def compile(self, text):
        new_text = re.sub(r'\(.*?\)', ' ', text)
        self.tokens = new_text.split()
        self.token_index = 0
        while self.token_index < len(self.tokens):
            self.compile_a_word()(self)

You can almost miss the execute. Look closely:

        self.compile_a_word()(self)

Let me extract a variable here just for clarity:

        while self.token_index < len(self.tokens):
            word = self.compile_a_word()
            word(self)

We call compile_a_word and then we call whatever comes back.

I think that’s more clear. Explaining Variable Name pattern? I’ll commit it.

Inside our current Forth, emulating the threaded versions of old, following in large part from Loeliger’s book, there are special words that patch up the code, filling in the number of words to skip over if an if is not true, or the number to skip back if a loop is to iterate again.

The Forth I looked at last night was more of a pure interpreter, without the little compilation phase, and without the patching. Imagine an IF-THEN:

0 > IF FOO BAR MUMBLE THEN

This code will execute FOO, BAR, and MUMBLE if the value on the stack is greater than zero. Last night’s Forth implemented IF by putting the interpreter in a state we could call if_active. Then it would keep reading tokens. If if_active is true, the token would be put into a separate list, maybe called if_words. When THEN is encountered, the code would pop and check the top of the stack to see if it was true or false. If false, it clears the if_active, empties the if_words and continues. Interpretation will proceed after the if.

On the other hand, if the popped value is true, the IF code would call execute, giving it the list if_words, which would duly be executed, performing the inside of the IF.

Only secondary words, i.e. those defined with COLON-SEMI, would be put in the dictionary / lexicon. If execute finds a word there, it recurs, calling execute on that list. If the word is not found in the lexicon, then execute knows that either it’s a mistake, or it’s a primary. It goes through its list of primaries, and if it finds the word, does the corresponding code (our lambda) and otherwise emits an error.

So the thing I saw last night is more of a pure interpreter than a compiler-interpreter such as classical Forth and such as we have in our Python Forth now.

And I, weak person that I am, am very tempted to make a hard right turn and implement Forth, in Python, as a pure interpreter. Why? Let me see if I have any good reasons:

  • It would probably be fun;
  • A simpler Forth might be better for embedding in Robot World;
  • Knowing two ways to do a thing is better than knowing one;
  • Refactoring from the current scheme down to a pure interpreter would be challenging;
  • I already have ideas on a couple of ways to do it, one more open, one more modular;

OK, they’re not very good reasons, though a couple of them have a bit of merit. But you know that I’m here to have fun and to share what I discover, so it’s a good bet that I’ll try it.

How about a compromise, at least for now. I won’t start a new PyCharm project, but I’ll do a spike in a test file in the current project. Just an experiment. No harm there, right?

OK, agreed, that’s what we’ll do. Let’s get started.

Interpreted Forth

class TestInterpreter:
    def test_hookup(self):
        assert False

Red bar. Change False to True. Green. Commit: initial interpreter test.

OK what would be a good first test? I suppose we could drive out the class. We’ll surely do this with a class.

class IForth:
    pass


class TestInterpreter:
    def test_exists(self):
        forth = IForth()

Right. Commit? Sure, why not. Now, what would be a good very first test, not too hard but enough to get something going? How about a tiny program and we check the stack?

    def test_stack(self):
        forth = IForth()
        assert forth.stack.is_empty()

No program at all. Make it simpler, don’t assume a smart stack instance.

    def test_stack(self):
        forth = IForth()
        assert forth.stack == []

class IForth:
    def __init__(self):
        self.stack = []

Green. Amazing! Commit: increment.

I think we’ll need a method to make tokens. Test that.

    def test_tokens(self):
        forth = IForth()
        tokens = forth.make_tokens('10 dup +')
        assert tokens == ['10', 'dup', '+']

class IForth:
    @staticmethod
    def make_tokens(text):
        return text.split()

Green. Commit.

OK, I think I’d like to work on having a word that works, maybe dup.

    def test_dup(self):
        forth = IForth()
        forth.stack.append(10)
        forth.execute('dup')
        assert forth.stack == [10, 10]

I think we’ll take the approach that we always get a string to execute. I’m not quite sure when we’ll convert to tokens. Anyway:

class IForth:
    def execute(self, command):
        if command == 'dup':
            self.stack.append(self.stack[-1])

Green. Commit.

    def test_plus(self):
        forth = IForth()
        forth.stack.append(10)
        forth.stack.append(32)
        forth.execute('+')
        assert forth.stack == [42]

That should be easy enough:

    def execute(self, command):
        if command == 'dup':
            self.stack.append(self.stack[-1])
        elif command == "+":
            self.push(self.pop() + self.pop())

    def push(self, value):
        self.stack.append(value)

    def pop(self):
        return self.stack.pop()

I decided that I needed some convenience methods.

Now how about a command that is more than one word. How should we do that. It seems to me that our execute is aimed, right now, at executing a single word by name. So let’s have another method for executing a string with more than one command in it.

    def test_dup_plus(self):
        forth = IForth()
        forth.stack.append(21)
        forth.execute_line('dup +')
        assert forth.stack == [42]

And let’s try this:

    def execute_line(self, line):
        for token in self.make_tokens(line):
            self.execute(token)

How about that? Green. Commit.

This is almost scary. Let’s do numbers.

    def test_numbers(self):
        forth = IForth()
        forth.execute_line('11 31 +')
        assert forth.stack == [42]
    def execute(self, command):
        if command == 'dup':
            self.stack.append(self.stack[-1])
        elif command == "+":
            self.push(self.pop() + self.pop())
        elif self.was_number(command):
            pass

    def was_number(self, command):
        try:
            number = int(command)
            self.push(number)
            return True
        except ValueError:
            return False

Green. Commit: we have numbers, dup, and plus.

Let’s settle down here and reflect.

Reflection

Well. Clearly we can implement all the primitives we want. And it seems very likely that if we were to build a dictionary of word-name to string, or to list of words, we could define new words. We haven’t addressed loops or conditionals and we definitely have to do that. Let’s try IF-THEN. I expect that to be … interesting.

We’ll write a test:

    def test_if_then_true(self):
        forth = IForth()
        forth.execute_line('21 1 if dup + then')
        assert forth.stack == [42]

Now we’ll need to put the interpreter into an if state.

This didn’t take long:

    def execute(self, command):
        if self.if_active:
            if command == 'then':
                self.if_active = False
                result = self.pop()
                if result:
                    line = ' '.join(self.if_words)
                    self.if_words = []
                    self.execute_line(line)
                    return
            else:
                self.if_words.append(command)
                return

        if command == 'dup':
            self.stack.append(self.stack[-1])
        elif command == "+":
            self.push(self.pop() + self.pop())
        elif command == 'if':
            self.if_active = True
            self.if_words = []
        elif self.was_number(command):
            pass

And we are green. Let me explain:

if the command is ‘if’, we set if_active and clear the if_words. Thereafter, if if_active, and we don’t have a then, we take the current command and append it to the if_words. If we do have a then, we covert the list to a line and execute it, if the top of the stack is not zero. We should make that more explicit. I do.

Let’s test the false case:

    def test_if_then_false(self):
        forth = IForth()
        forth.execute_line('21 0 if dup + then')
        assert forth.stack == [21]

That passes. Commit: simple if-then working.

We could pull out that code into a separate method. Let’s do that.

PyCharm won’t do it for me: it can’t handle the if else in the code. My first attempt at doing it manually fails, and I roll back instantly. Note the difference with yesterday, where I didn’t roll back and instead figured out what was wrong. Here I think I must have made some mechanical mistake.

Second time it works:

    def execute(self, command):
        if self.if_active:
            self.handle_if(command)
            return

        if command == 'dup':
            self.stack.append(self.stack[-1])
        elif command == "+":
            self.push(self.pop() + self.pop())
        elif command == 'if':
            self.if_active = True
            self.if_words = []
        elif self.was_number(command):
            pass

    def handle_if(self, command):
        if command == 'then':
            self.if_active = False
            result = self.pop()
            if result != 0:
                line = ' '.join(self.if_words)
                self.if_words = []
                self.execute_line(line)
                return
        else:
            self.if_words.append(command)
            return

Commit: refactoring.

Time to reflect.

Reflection

The IF went very nicely. I made the mistake of saying self.if_words += command, which appended ‘d’, ‘u’, ‘p’ ‘+’ to the words, not quite what I meant. Other than that it just worked.

However, it seems to me to be clear that while ELSE can work similarly to IF, just starting to accumulate into an else-words list, nested IF-THEN is not going to work with the code we have now. If we had this Forth code:

5 1 if 4 > if 42 then 666 then

That code should wind up with stack [42, 666]. Just for fun, let’s see what we get.

    def test_nested_if(self):
        forth = IForth()
        forth.execute_line('5 1 if 4 > if 42 then 666 then')
        assert forth.stack == [42, 666]
Expected :[42, 666]
Actual   :[5, 42, 666]

I don’t want to try to explain why it does that. The point is, we’ll need to be stacking the if and then stuff in some way that I am not clever enough to see right now.

It might be as simple as a counter. We’ll have the same kind of issue with loops, and of course with the nesting of all these different constructs.

One possible answer is not to allow nesting. We can detect it and error. It is always possible to put the nesting in an inner word, I think. We’ll experiment with that in the sequel.

Hey, what about the “AI”?

Reflecting back on my being impressed with the “AI” version. I am quite sure that it did not cater to nesting of any of the IF and LOOP things it tried to implement. I’d have to review their code but my recollection is that it was essentially as simple as what we currently have, that is, too simplistic.

I want to flag two important issues:

First, I was probably more impressed than I should have been. The code it wrote was a few typos away from working about as well as our code here does. But it wasn’t as complete as I perceived it to be. I didn’t think of the nesting issue and neither did the “AI”.

Second, while an adequate programmer could probably find the typos and any simple bugs in the “AI” program, discovering the nesting issues would take some intensive testing, and fixing it would not be a small matter of changing a constant. We’ll see what it takes here, but I think that by the time we handle IF-ELSE-THEN, plus one or two different forms of looping, it’s going to be tricky.

And there we see the danger of these things. I’m a decent programmer and at first blush the thing impressed me, and at a deeper look I see issues that would be hard to fix. It kind of sucked me in with its glib description of what it was doing.

But what if I had asked it about something in which I am not expert? It would surely have sounded just as authoritative … and I would not be well-equipped to see the inevitable flaws in what it said.

In what application of this “AI” thing should we be comfortable with the results it produces. So far, it’s hard to see just where I would trust it. The “AI” summaries that Apple was putting on my email have often been wrong, one time even mistakenly telling me that my brother’s partner was dead. The code it produced last night was just good enough to pass an interested but not incredibly detailed inspection.

Do you want that thing advising you on medical matters? Do you want it flying your plane, driving your car? It’s almost always right after all …

Danger, Will Robinson, warning warning!

What about our little interpreter here?

Good question. It’s harder than I thought, possibly too hard. Possibly we can outlaw it, because it may well work if you push any nested things into a word inside the outer shell. But so far, it is mostly pretty simple.

I am inclined to push it a bit further, just to get a solid sense of the alternative. But given the one we have been working on, the advantages of this fully interpreted one are not all that clear.

Summary

Interesting. Cynical though I am about the world in general and “AI” in particular, I was still distracted by the shiny and almost failed to see that it wasn’t as good as it looked.

I owe it to myself to think more deeply about that aspect. And I do think we’ll play with the interpreted version a bit more, to get a fair sense of how it really compares.

See you next time! Watch out for the shiny!

Shiny Squirrel, most interesting possible object