Tsonnet #31 - Taking back control of equality
Cleaning up a messy equality implementation by introducing a new AST variant.
Welcome to the Tsonnet series!
If you’re not following along, check out how it all started in the first post of the series.
In the previous post, I implemented the equality operator:
In a messy way, I should say. So, let’s take back control.
Encoding the compiler phases as phantom types
In the previous post I mentioned this, and it was my initial idea. The premise has its merit. We encode each compiler phase in the type system, as each phase will only deal with AST variants that matter in that stage. I still believe this should be the end state of the compiler, but I played with it, and decided not to pursue this path. At least, not yet.
I made a quick and dirty attempt, you can see the diff here, but in the end, I was not satisfied with how it turned out.
Why? Because it introduced too much noise, and made the code brittle, IMO. I don’t want to enforce too much strictness at this point while the compiler is still changing. Though I still believe this is going to be valuable eventually.
For now, I chose a different and simpler path.
A new variant
EvaluatedObject comes into play:
Here we’ll draw a line between the type variants that are terminal (can be representable as JSON), and those that are merely there for intermediate steps (used by the interpreter).
This gives us the power to simplify our workflow, starting with the semantic_equal function:
Intermediate object variants are not relevant in this function anymore.
We can remove some ugly code now:
By accepting only EvaluatedObject in the semantic_equal function, we invert the control of who evaluates the AST, leaving the responsibility for the interpreter.
The interpreter owns the evaluation
Before the evaluation changes, let’s make sure parsing equality has the right associativity:
EvaluatedObject will be returned as-is by interpret function, and the pattern match that used to return RuntimeObject as-is will call interpret_runtime_object from now on:
The recursive nature of interpret is extremely flexible and elegant to reduce AST variants. Don’t you think?
The Json module can be simplified a lot with this change too. Here’s a sneak peek of its usage from now on:
We’ll get back to it. Let’s get back to interpret_runtime_object:
And the equality operation becomes much more manageable:
Before handing over the interpretation phase to the serialization phase, now that we delineated the terminal variants, let’s deep evaluate the AST expression:
The deep_eval function guarantees that we have only terminal values before serializing.
The eval function now works with a 2 step interpretation. First the interpret that will evaluate the AST expressions lazily, and the deep_eval that will walk down the AST transforming the variants into terminal variants:
Do you know what we can do now? Simplify the JSON rendering!
JSON is finally free from the evaluation bad smell
Look how simple the Json module became -- simple like in the very early stages of Tsonnet:
It doesn’t need to care about how expr is interpreted anymore -- single responsibility:
Conclusion
The EvaluatedObject variant did a lot of heavy lifting here. By drawing a clear line between terminal and intermediate AST variants, we were able to push the responsibility for evaluation squarely back onto the interpreter, simplify semantic_equal into something I’m no longer embarrassed to look at, and let the Json module be blissfully ignorant of how things got evaluated. That TODO comment in the previous post aged well — sometimes the cleanest solutions come from just letting a problem sleep on it.
The entire diff can be seen here.














