Welcome to the Tsonnet series!
In the previous post, cram tests were introduced:
Ready to add some math to our language? Let's dive into binary operations!
Adding arithmetic operations
Let's implement binary operations. More specifically, arithmetic operations.
Here's the sample file and the test:
If we run Jsonnet against the sample, the output will be:
You might be wondering why we got that funky 44.700000000000003 result. Here's a fun fact about computers โ they store decimal numbers in binary floating-point representation (typically IEEE 754). Some decimal numbers cannot be represented exactly in binary floating-point, leading to small rounding errors.
This isn't unique to Jsonnet โ many programming languages will have the same underlying behavior, though some may hide it by defaulting to a different display format that rounds the number for display purposes.
The AST expr now needs to accommodate a binary operation. It's composed of a binary operation and the left and right expressions:
Having the left and right nodes as expr allows it to nest multiple operations easily.
The AST for the operations 1 + 2 * 3
will look like this:
The lexing of arithmetic tokens is straightforwardly simple:
The parsing is where the mathematical semantics come in:
In the parser, we should specify that all arithmetic operations are left associative, but also that multiplication and division take precedence over addition and subtraction. In Menhir, tokens declared later have higher precedence than those declared earlier. This matches arithmetic rules where 1 + 2 * 3 is interpreted as 1 + (2 * 3).
Now that we can parse our operations, let's make them actually do something!
When it comes to the interpreter, it is about to increase in complexity:
In OCaml, integers and floats are not interchangeable types and due to that we have different arithmetic operators for them. Integers use the regular operator symbols as we know, but float equivalents are: +.
, -.
, *.
, and /.
. This helps the compiler to optimize code and forces the programmers to always be aware of the numerical types they are manipulating. Even though it has its technical merits, it can become annoying quickly in a JS-like language, where the expectations are rather different.
To alleviate the annoyance, I'm cutting a corner and converting integers to float when doing arithmetic operations. Shouldn't be a big deal considering that, when doing division operations, we will invariably end up with a fractional number.
We can abstract integers and floats as numbers, to better represent the JSON abstraction. However, I'm postponing this to the next step. For now, the interpret_bin_op function is good enough.
So far, so good:
Wrapping up
Arithmetic operations are simple and straightforward binary operations to implement.
The interpret_bin_op function is ugly, I must admit. Wrapping Int and Float in a Number type may give us a better interface to parse Jsonnet code. Next time, we'll clean up that messy `interpret_bin_op` function by introducing a proper `Number` type. This won't just make our code cleaner -- it'll also set us up for the type checker Iโm building towards!
See you in the next post!