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, we added a full set of binary operators:
Tsonnet #32 - != done, but getting there
Hercules Lemke Merscher ・ Mar 5
We were almost done with the arithmetic tutorial. Two pieces were still missing: object merging, and division by zero error handling. Let's wrap those up.
Merging objects
The + operator is overloaded to act as an operator to merge two objects.
This sample file:
// samples/objects/merge.jsonnet
{ a: 1, b: 2 } + { b: 3, c: 4 }
Results in:
{ "a": 1, "b": 3, "c": 4 }
The right-hand side overrides the left-hand side fields.
The cram test looks like this:
diff --git a/test/cram/objects.t b/test/cram/objects.t
index 9c12260..bfb25bd 100644
--- a/test/cram/objects.t
+++ b/test/cram/objects.t
@@ -18,3 +18,6 @@
$ tsonnet ../../samples/objects/toplevel_field_lookup_chain.jsonnet
{ "answer": { "value": 42 }, "answer_to_the_ultimate_question": 42 }
+
+ $ tsonnet ../../samples/objects/merge.jsonnet
+ { "a": 1, "b": 3, "c": 4 }
I'm adding a helper function in the Ast module to merge two lists of fields:
module Object = struct
let merge_fields fields1 fields2 =
(* fields2 comes after and has preference over fields1 *)
let updated = List.map
(fun (k, v) ->
match List.assoc_opt k fields2 with
| Some v' -> (k, v')
| None -> (k, v)
)
fields1
in
(* (k,v) in fields2 not present in fields1 *)
let new_fields = List.filter
(fun (k, _) -> not (List.mem_assoc k fields1))
fields2
in
(* then we merge *)
updated @ new_fields
end
The type checker only needs to include the overloaded operation:
diff --git a/lib/type.ml b/lib/type.ml
index d553f5c..fbe68b1 100644
--- a/lib/type.ml
+++ b/lib/type.ml
@@ -321,6 +321,8 @@ and translate_bin_op venv pos op e1 e2 =
| Add, _, Tstring | Add, Tstring, _ -> ok (venv'', Tstring)
| Add, Tnumber, Tnumber -> ok (venv'', Tnumber)
| Add, (Tarray _), (Tarray _) -> ok (venv'', Tarray Tany)
+ | Add, (Tobject _ | TruntimeObject _ | TobjectPtr _), (Tobject _ | TruntimeObject _ | TobjectPtr _) ->
+ ok (venv'', Tany)
| Subtract, Tnumber, Tnumber -> ok (venv'', Tnumber)
| Multiply, Tnumber, Tnumber -> ok (venv'', Tnumber)
| Divide, Tnumber, Tnumber -> ok (venv'', Tnumber)
The interpreter implements the new interpret_object_merge_op function to deal with that, and we pattern-match in the interpret_bin_op function to add it:
diff --git a/lib/interpreter.ml b/lib/interpreter.ml
index ddc8041..24f0595 100644
--- a/lib/interpreter.ml
+++ b/lib/interpreter.ml
@@ -66,6 +66,19 @@ and interpret_array_concat_op env e1 e2 =
| _ ->
error Error.Msg.interp_invalid_concat
+and interpret_object_merge_op env pos e1 e2 =
+ let eval_object = function
+ | EvaluatedObject _ as obj -> ok (env, obj)
+ | RuntimeObject _ as obj -> interpret env obj
+ | _ -> error Error.Msg.invalid_binary_op
+ in
+ let* (_, e1') = eval_object e1 in
+ let* (_, e2') = eval_object e2 in
+ match e1', e2' with
+ | EvaluatedObject (_, fields1), EvaluatedObject (_, fields2) ->
+ ok (env, EvaluatedObject (pos, Object.merge_fields fields1 fields2))
+ | _ -> error Error.Msg.invalid_binary_op
+
and interpret_array env (pos, exprs) =
let* (env', evaluated_exprs) = List.fold_left
(fun result expr ->
@@ -234,6 +247,8 @@ and interpret_bin_op env (pos, op, e1, e2) =
| Add, (Array _ as v1), (Array _ as v2) ->
interpret_array_concat_op env2 v1 v2
+ | Add, (EvaluatedObject _ | RuntimeObject _ as v1), (EvaluatedObject _ | RuntimeObject _ as v2) ->
+ interpret_object_merge_op env2 pos v1 v2
| In, (String _ | Ident _ as field), (EvaluatedObject _ | RuntimeObject (_, _, _) as obj) ->
interpret_in_op env2 pos field obj
| _, v1, v2 ->
Done!
dune exec -- tsonnet samples/objects/merge.jsonnet
{ "a": 1, "b": 3, "c": 4 }
Division by zero
Next we should deal with the division by zero error.
Here's how Jsonnet handles it:
$ jsonnet samples/errors/divide_by_zero.jsonnet
RUNTIME ERROR: Division by zero.
samples/errors/divide_by_zero.jsonnet:1:1-6 $
During evaluation
And here are the sample files and how I want it to look in Tsonnet:
diff --git a/samples/errors/divide_by_zero.jsonnet b/samples/errors/divide_by_zero.jsonnet
new file mode 100644
index 0000000..59a5d52
--- /dev/null
+++ b/samples/errors/divide_by_zero.jsonnet
@@ -0,0 +1 @@
+5 / 0
diff --git a/samples/errors/divide_by_zero_float.jsonnet b/samples/errors/divide_by_zero_float.jsonnet
new file mode 100644
index 0000000..20f3764
--- /dev/null
+++ b/samples/errors/divide_by_zero_float.jsonnet
@@ -0,0 +1 @@
+5 / 0.0
diff --git a/samples/errors/modulo_by_zero.jsonnet b/samples/errors/modulo_by_zero.jsonnet
new file mode 100644
index 0000000..be4046b
--- /dev/null
+++ b/samples/errors/modulo_by_zero.jsonnet
@@ -0,0 +1 @@
+5 % 0
diff --git a/samples/errors/modulo_by_zero_float.jsonnet b/samples/errors/modulo_by_zero_float.jsonnet
new file mode 100644
index 0000000..8f666b8
--- /dev/null
+++ b/samples/errors/modulo_by_zero_float.jsonnet
@@ -0,0 +1 @@
+5 % 0.0
diff --git a/test/cram/errors.t b/test/cram/errors.t
index 7c117b8..dfcac7e 100644
--- a/test/cram/errors.t
+++ b/test/cram/errors.t
@@ -93,9 +105,43 @@
$ tsonnet ../../samples/errors/object_outer_most_ref_out_of_scope.jsonnet
../../samples/errors/object_outer_most_ref_out_of_scope.jsonnet:2:13 No top-level object found
2: local _two = $.one + 1;
^^^^^^^^^^^^^^^^^^^^^^^
[1]
+
+
+ $ tsonnet ../../samples/errors/divide_by_zero.jsonnet
+ ../../samples/errors/divide_by_zero.jsonnet:1:0 Division by zero
+
+ 1: 5 / 0
+ ^^^^^
+ [1]
+
+
+ $ tsonnet ../../samples/errors/divide_by_zero_float.jsonnet
+ ../../samples/errors/divide_by_zero_float.jsonnet:1:0 Division by zero
+
+ 1: 5 / 0.0
+ ^^^^^^^
+ [1]
+
+
+ $ tsonnet ../../samples/errors/modulo_by_zero.jsonnet
+ ../../samples/errors/modulo_by_zero.jsonnet:1:0 Division by zero
+
+ 1: 5 % 0
+ ^^^^^
+ [1]
+
+
+ $ tsonnet ../../samples/errors/modulo_by_zero_float.jsonnet
+ ../../samples/errors/modulo_by_zero_float.jsonnet:1:0 Division by zero
+
+ 1: 5 % 0.0
+ ^^^^^^^
+ [1]
+
Not a huge difference from the reference implementation, but I like seeing the faulty operation highlighted in the source — much easier to spot and fix.
Let's add the error message:
diff --git a/lib/error.ml b/lib/error.ml
index 8c6b53f..ac8ae48 100644
--- a/lib/error.ml
+++ b/lib/error.ml
@@ -29,6 +29,7 @@ module Msg = struct
let type_invalid_lookup_key expr = "Invalid object lookup key: " ^ expr
(* Interpreter messages *)
+ let interp_division_by_zero = "Division by zero"
let interp_invalid_concat = "Invalid concatenation operation"
let interp_invalid_lookup = "Invalid object lookup"
let interp_cannot_interpret expr = Printf.sprintf "Expression %s cannot be interpreted" expr
diff --git a/lib/error.mli b/lib/error.mli
index 58d7e8e..0bdd708 100644
--- a/lib/error.mli
+++ b/lib/error.mli
@@ -26,6 +26,7 @@ module Msg : sig
val type_invalid_lookup_key : string -> string
(* Interpreter messages *)
+ val interp_division_by_zero : string
val interp_invalid_concat : string
val interp_invalid_lookup : string
val interp_cannot_interpret : string -> string
The interpret_arith_op function only needs to pattern-match on Int and Float zero before the division — pretty simple. Have I mentioned how much I enjoy pattern matching?
diff --git a/lib/interpreter.ml b/lib/interpreter.ml
index 24f0595..fe248b9 100644
--- a/lib/interpreter.ml
+++ b/lib/interpreter.ml
@@ -280,6 +280,8 @@ and interpret_arith_op env (pos, bin_op, n1, n2) =
ok (env, Number (pos, Float ((float_of_int a) *. b)))
| Multiply, Number (_, Float a), Number (_, Float b) ->
ok (env, Number (pos, Float (a *. b)))
+ | Divide, _, Number (_, n) when n = Int 0 || n = Float 0.0 ->
+ Error.error_at pos Error.Msg.interp_division_by_zero
| Divide, Number (_, Int a), Number (_, Int b) ->
ok (env, Number (pos, Float ((float_of_int a) /. (float_of_int b))))
| Divide, Number (_, Float a), Number (_, Int b) ->
@@ -288,6 +290,8 @@ and interpret_arith_op env (pos, bin_op, n1, n2) =
ok (env, Number (pos, Float ((float_of_int a) /. b)))
| Divide, Number (_, Float a), Number (_, Float b) ->
ok (env, Number (pos, Float (a /. b)))
+ | Modulo, _, Number (_, n) when n = Int 0 || n = Float 0.0 ->
+ Error.error_at pos Error.Msg.interp_division_by_zero
| Modulo, Number (_, Int a), Number (_, Int b) ->
ok (env, Number (pos, Int (a mod b)))
| Modulo, Number (_, Float a), Number (_, Int b) ->
The guard clauses (when n = Int 0 || n = Float 0.0) catch both integer and float zero before the division cases get a chance to run.
Completing the tutorial
With those two additions in place, samples/tutorials/arith.jsonnet is now fully supported -- minus the string formatting bits I'm deliberately setting aside for later. Here's the file with those commented out:
{
concat_array: [1, 2, 3] + [4],
concat_string: '123' + 4,
equality1: 1 == '1',
equality2: [{}, { x: 3 - 1 }]
== [{}, { x: 2 }],
ex1: 1 + 2 * 3 / (4 + 5),
// Bitwise operations first cast to int.
ex2: self.ex1 | 3,
// Modulo operator.
ex3: self.ex1 % 2,
// Boolean logic
ex4: (4 > 3) && (1 <= 3) || false,
// Mixing objects together
obj: { a: 1, b: 2 } + { b: 3, c: 4 },
// Test if a field is in an object
obj_member: 'foo' in { foo: 1 },
// // String formatting
// str1: 'The value of self.ex2 is '
// + self.ex2 + '.',
// str2: 'The value of self.ex2 is %g.'
// % self.ex2,
// str3: 'ex1=%0.2f, ex2=%0.2f'
// % [self.ex1, self.ex2],
// // By passing self, we allow ex1 and ex2 to
// // be extracted internally.
// str4: 'ex1=%(ex1)0.2f, ex2=%(ex2)0.2f'
// % self,
// // Do textual templating of entire files:
// str5: |||
// ex1=%(ex1)0.2f
// ex2=%(ex2)0.2f
// ||| % self,
}
Running it:
dune exec -- tsonnet samples/tutorials/arith.jsonnet
{
"concat_array": [ 1, 2, 3, 4 ],
"concat_string": "1234",
"equality1": false,
"equality2": true,
"ex1": 1.6666666666666665,
"ex2": 3,
"ex3": 1.6666666666666665,
"ex4": true,
"obj": { "a": 1, "b": 3, "c": 4 },
"obj_member": true
}
String formatting is next on the list -- but there are more interesting features to tackle first, so I'm parking it for now.
Conclusion
Two small additions -- object merging and division by zero -- and the arithmetic tutorial is effectively done. The pattern-matching guard clauses made the zero-division check almost embarrassingly straightforward, and the merge_fields helper slotted in without touching anything else.
You can check the entire diff here.
If you, too, believe dividing by zero should be an error and not a NaN, subscribe and let's keep each other honest.
Photo by Lance Grandahl on Unsplash

Top comments (0)