Crafting Interpreters - Progress 2018-11-02
Last time, I mentioned I wasn't sure how best to get through Chapter 7 of Crafting Interpreters.
Yesterday, I talked myself through it and got it into a mostly-working state. This morning, I ironed out the really obvious bugs, took too long of a shower, and severely winded myself getting to the train.
Here's what the deal with the first part of that was. Some of working on this is a blur, but it's a fact that, before I got started really laying the details of this stuff out for myself, I hadn't been sure what kind of exceptions I wanted to be passing around. I think the source of my mental fuzziness was that I hadn't worked out all of the things that should be throwing errors usable by the interpreter. Once I added validation to the majority of the functions, things began to fall into place. Writing validators suggested shortcut functions, shortcut functions supported refactoring, and it became really pleasant after that. I ended up something that could do math. What I neglected to notice that night was that it didn't handle errors properly.
As I realized this morning, that was because I was changing a return value in the wrong part of the stack, so where I was trying to call the result with a bunch of handlers, I was instead calling the (boolean) status, so my error recovery code errored out. This was a little annoying to fix, but doable.
I later took on the challenges, and decided that addition should not coerce its arguments (to be honest, I kind of want to split out addition and concatenation so it works like in Lua, but without coercion with addition; coercion on concatenation is probably fine though), that it's fine to compare strings, but not values of unlike type (I might add support for comparing booleans later), and that, rather than making zero division raise an error, I'll take Pony's approach just to see what happens.
So, that was chapter 7. Earlier today, I ported up through the end of 8.1, before realizing that I should read over the rest of chapter 8 before I did anything. So I did. The basic revelation I got from doing so was that I intend to undo some of my changes, and make deliberate fundamental design changes relative to the Java version. Not just "this is a table instead of a switch statement, so things are a little weird", but "I want to rewrite the Lua code I have, so that it corresponds to some fundamentally different concepts, that would look like very different Java code". I am, mysteriously, 100% sure this won't bite me later. Here's the deal. Crafting Interpreters adds an Environment object to the Interpreter object, to track the current execution scope. This means that it needs a finally block after nested scopes to reset the global environment. The equivalent of "finally" in Lua is undesirable to me for code that could be nested deep, because it means that TCO is guaranteed impossible. (Granted, I'd have to do some trampolining to get TCO to happen regularly with this code, but I don't want to make it impossible for me.) It seemed a little weird to me, insofar as, we know that the environment should be the same before and after, we're shuffling it around inside a particular object. Why not just pass the environment as a parameter, and define new ones as needed, to be passed in only to the functions that need them? (Answer: because when I implemented the visitor pattern in Lua, I gave it the ability to take varargs, and I forgot that's not normal.) Now, if the current environment is a function of dynamic state, and the global environment is consistent between runs, perhaps it should live in the lox table instead, and because I have the visitor object taking the lox table, perhaps the visitors should be converted from dynamic to static visitors, and passed the lox object as part of the varargs. This leads in my mind to separating the statement and expression visitors into distinct objects, and collapsing the public interface to the intepreter module into a function.
Other things to look into doing eventually:
- Rely more on stacks for processing the source and running the code; I should ideally be able to bound all recursion depth to a relatively small limit, and avoid the error and pcall functions.
- Convert environments to store variables array-style, with a separate table for mapping names to indices, and cache indices. Because variables in Lox are declared explicitly, it's possible to use static analysis to determine all variable names ahead of time, and assign them positions in an array, then redo the variable accesses to be by index. Not sure how this interacts with stacked environments, maybe it doesn't work, we'll see eventually.
Next time, let's see what happens when I try to do the rewrites I laid out above.