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, the equality operation was simplified:
Tsonnet #31 - Taking back control of equality
Hercules Lemke Merscher ・ Mar 2
Now, in order to complete the arithmetic tutorial, there's a myriad of operations still left to be implemented. They are quite simple to add actually, so let's do this ASAP!
Operations everywhere!
Here are all the operations we are going to add to the lexer:
diff --git a/lib/lexer.mll b/lib/lexer.mll
index 6f51b6f..a4c3745 100644
--- a/lib/lexer.mll
+++ b/lib/lexer.mll
@@ -46,6 +46,13 @@ rule read =
| "@\"" { read_single_line_verbatim_double_quoted_string (Buffer.create 16) lexbuf }
| "@'" { read_single_line_verbatim_single_quoted_string (Buffer.create 16) lexbuf }
| "==" { EQUALITY }
+ | "!=" { INEQUALITY }
+ | ">=" { GREATER_EQUAL }
+ | "<=" { LESS_EQUAL }
+ | "<<" { SHIFT_LEFT }
+ | ">>" { SHIFT_RIGHT }
+ | '>' { GREATER }
+ | '<' { LESS }
| '[' { LEFT_SQR_BRACKET }
| ']' { RIGHT_SQR_BRACKET }
| '{' { LEFT_CURLY_BRACKET }
@@ -58,14 +65,21 @@ rule read =
| '-' { MINUS }
| '*' { MULTIPLY }
| '/' { DIVIDE }
+ | '%' { MODULO }
| '!' { NOT }
| '~' { BITWISE_NOT }
+ | "||" { LOGICAL_OR }
+ | '|' { BITWISE_OR }
+ | "&&" { LOGICAL_AND }
+ | "&" { BITWISE_AND }
+ | '^' { BITWISE_XOR }
| '=' { ASSIGN }
| ';' { SEMICOLON }
| '.' { DOT }
| "local" { LOCAL }
| "self" { SELF }
| "$" { TOP_LEVEL_OBJ }
+ | "in" { IN }
| id { ID (Lexing.lexeme lexbuf) }
| _ { raise (SyntaxError ("Unexpected char: " ^ Lexing.lexeme lexbuf)) }
| eof { EOF }
The parser changes are quite straightforward too:
diff --git a/lib/parser.mly b/lib/parser.mly
index 456e33e..7adb1ee 100644
--- a/lib/parser.mly
+++ b/lib/parser.mly
@@ -20,17 +20,19 @@
%token COLON
%token DOT
%token SELF TOP_LEVEL_OBJ
-%token PLUS MINUS MULTIPLY DIVIDE
+%token PLUS MINUS MULTIPLY DIVIDE MODULO
%left PLUS MINUS
-%left MULTIPLY DIVIDE
+%left MULTIPLY DIVIDE MODULO
%token <string> ID
-%token NOT BITWISE_NOT
-%left NOT BITWISE_NOT
+%token NOT BITWISE_NOT BITWISE_OR BITWISE_AND BITWISE_XOR LOGICAL_AND LOGICAL_OR
+%left NOT BITWISE_NOT BITWISE_OR BITWISE_AND BITWISE_XOR LOGICAL_AND LOGICAL_OR
%token SEMICOLON
%token LOCAL
%token ASSIGN
-%token EQUALITY
-%left EQUALITY
+%token EQUALITY INEQUALITY GREATER GREATER_EQUAL LESS LESS_EQUAL IN
+%left EQUALITY INEQUALITY GREATER GREATER_EQUAL LESS LESS_EQUAL IN
+%token SHIFT_LEFT SHIFT_RIGHT
+%left SHIFT_LEFT SHIFT_RIGHT
%token EOF
%start <Ast.expr> prog
@@ -139,12 +141,26 @@ obj_field_access:
;
%inline bin_op:
- | PLUS { Add }
- | MINUS { Subtract }
- | MULTIPLY { Multiply }
- | DIVIDE { Divide }
- | EQUALITY { Equality }
- ;
+ | PLUS { Add }
+ | MINUS { Subtract }
+ | MULTIPLY { Multiply }
+ | DIVIDE { Divide }
+ | MODULO { Modulo }
+ | EQUALITY { Equality }
+ | INEQUALITY { Inequality }
+ | GREATER { GreaterThan }
+ | GREATER_EQUAL { GreaterThanOrEqual }
+ | LESS { LessThan }
+ | LESS_EQUAL { LessThanOrEqual }
+ | BITWISE_OR { BitwiseOr }
+ | BITWISE_AND { BitwiseAnd }
+ | BITWISE_XOR { BitwiseXor }
+ | LOGICAL_AND { LogicalAnd }
+ | LOGICAL_OR { LogicalOr }
+ | IN { In }
+ | SHIFT_LEFT { ShiftLeft }
+ | SHIFT_RIGHT { ShiftRight }
+ ;
%inline unary_op:
| PLUS { Plus }
I only had to introduce the new symbols we'd use for each operation.
The new AST variants:
diff --git a/lib/ast.ml b/lib/ast.ml
index 03f413e..5111806 100644
--- a/lib/ast.ml
+++ b/lib/ast.ml
@@ -5,7 +5,21 @@ type bin_op =
| Subtract
| Multiply
| Divide
+ | Modulo
| Equality
+ | Inequality
+ | GreaterThan
+ | GreaterThanOrEqual
+ | LessThan
+ | LessThanOrEqual
+ | BitwiseOr
+ | BitwiseAnd
+ | BitwiseXor
+ | LogicalAnd
+ | LogicalOr
+ | In
+ | ShiftLeft
+ | ShiftRight
[@@deriving qcheck, show]
type unary_op =
@@ -160,7 +174,21 @@ let rec string_of_type = function
| Subtract -> "-"
| Multiply -> "*"
| Divide -> "/"
+ | Modulo -> "%"
| Equality -> "=="
+ | Inequality -> "!="
+ | GreaterThan -> ">"
+ | GreaterThanOrEqual -> ">="
+ | LessThan -> "<"
+ | LessThanOrEqual -> "<="
+ | BitwiseOr -> "|"
+ | BitwiseAnd -> "&"
+ | BitwiseXor -> "^"
+ | LogicalAnd -> "&&"
+ | LogicalOr -> "||"
+ | In -> "in"
+ | ShiftLeft -> "<<"
+ | ShiftRight -> ">>"
in prefix ^ " " ^ bin_op
| UnaryOp (_, unary_op, _) ->
let prefix = "Unary Operation" in
The type checker changes are also straightforward:
diff --git a/lib/type.ml b/lib/type.ml
index 5de3c4c..d553f5c 100644
--- a/lib/type.ml
+++ b/lib/type.ml
@@ -320,10 +320,29 @@ and translate_bin_op venv pos op e1 e2 =
match op, e1', e2' with
| Add, _, Tstring | Add, Tstring, _ -> ok (venv'', Tstring)
| Add, Tnumber, Tnumber -> ok (venv'', Tnumber)
+ | Add, (Tarray _), (Tarray _) -> ok (venv'', Tarray Tany)
| Subtract, Tnumber, Tnumber -> ok (venv'', Tnumber)
| Multiply, Tnumber, Tnumber -> ok (venv'', Tnumber)
| Divide, Tnumber, Tnumber -> ok (venv'', Tnumber)
+ | Modulo, Tnumber, Tnumber -> ok (venv'', Tnumber)
+ | BitwiseOr, Tnumber, Tnumber -> ok (venv'', Tnumber)
+ | BitwiseAnd, Tnumber, Tnumber -> ok (venv'', Tnumber)
+ | BitwiseXor, Tnumber, Tnumber -> ok (venv'', Tnumber)
+ | ShiftLeft, Tnumber, Tnumber -> ok (venv'', Tnumber)
+ | ShiftRight, Tnumber, Tnumber -> ok (venv'', Tnumber)
+ | LogicalAnd, Tbool, Tbool -> ok (venv'', Tbool)
+ | LogicalOr, Tbool, Tbool -> ok (venv'', Tbool)
| Equality, _, _ -> ok (venv'', Tbool)
+ | Inequality, _, _ -> ok (venv'', Tbool)
+ | GreaterThan, Tnumber, Tnumber -> ok (venv'', Tbool)
+ | GreaterThanOrEqual, Tnumber, Tnumber -> ok (venv'', Tbool)
+ | LessThan, Tnumber, Tnumber -> ok (venv'', Tbool)
+ | LessThanOrEqual, Tnumber, Tnumber -> ok (venv'', Tbool)
+ | GreaterThan, Tstring, Tstring -> ok (venv'', Tbool)
+ | GreaterThanOrEqual, Tstring, Tstring -> ok (venv'', Tbool)
+ | LessThan, Tstring, Tstring -> ok (venv'', Tbool)
+ | LessThanOrEqual, Tstring, Tstring -> ok (venv'', Tbool)
+ | In, Tstring, (Tobject _ | Tany | TruntimeObject _ | TobjectPtr _) -> ok (venv'', Tbool)
| _ -> Error.trace Error.Msg.invalid_binary_op pos >>= error
let check (config : Config.t) expr =
What changes on evaluation?
The array concatenation was missing, so I added that:
diff --git a/lib/interpreter.ml b/lib/interpreter.ml
index 0b522f2..689e786 100644
--- a/lib/interpreter.ml
+++ b/lib/interpreter.ml
@@ -44,7 +44,7 @@ let rec interpret env expr =
)
~err:(Error.error_at pos)
-and interpret_concat_op env e1 e2 =
+and interpret_string_concat_op env e1 e2 =
match e1, e2 with
| String (_, s1), String (_, s2) ->
ok (env, String (dummy_pos, s1^s2))
@@ -59,6 +59,13 @@ and interpret_concat_op env e1 e2 =
| _ ->
error Error.Msg.interp_invalid_concat
+and interpret_array_concat_op env e1 e2 =
+ match e1, e2 with
+ | Array (pos, exprs1), Array (_, exprs2) ->
+ ok (env, Array (pos, List.append exprs1 exprs2))
+ | _ ->
+ error Error.Msg.interp_invalid_concat
+
and interpret_array env (pos, exprs) =
let* (env', evaluated_exprs) = List.fold_left
(fun result expr ->
@@ -220,16 +227,20 @@ and interpret_runtime_object_fields obj_env fields =
| _ -> ok []
and interpret_bin_op env (pos, op, e1, e2) =
- let* (env1, e1') = interpret env e1 in
- let* (env2, e2') = interpret env1 e2 in
- match op, e1', e2' with
- | Add, (String _ as v1), (_ as v2) | Add, (_ as v1), (String _ as v2) ->
- interpret_concat_op env2 v1 v2
- | _, v1, v2 ->
- interpret_arith_op env2 (pos, op, v1, v2)
+ let* (env1, e1') = interpret env e1 in
+ let* (env2, e2') = interpret env1 e2 in
+ match op, e1', e2' with
+ | Add, (String _ as v1), (_ as v2) | Add, (_ as v1), (String _ as v2) ->
+ interpret_string_concat_op env2 v1 v2
+ | Add, (Array _ as v1), (Array _ as v2) ->
+ interpret_array_concat_op env2 v1 v2
+ | In, (String _ | Ident _ as field), (EvaluatedObject _ | RuntimeObject (_, _, _) as obj) ->
+ interpret_in_op env2 pos field obj
+ | _, v1, v2 ->
+ interpret_arith_op env2 (pos, op, v1, v2)
The previous interpret_concat_op was renamed to be more specific: interpret_string_concat_op. A new function was added to handle the concatenation of arrays: interpret_array_concat_op.
We still don't have one for objects, but we'll get there eventually.
interpret_arith_op grew into a large matching function. It's a lot of lines, but it's easy to follow:
and interpret_arith_op env (pos, bin_op, n1, n2) =
match bin_op, n1, n2 with
| Add, Number (_, Int a), Number (_, Int b) ->
ok (env, Number (pos, Int (a + b)))
| Add, Number (_, Float a), Number (_, Int b) ->
ok (env, Number (pos, Float (a +. (float_of_int b))))
| Add, Number (_, Int a), Number (_, Float b) ->
ok (env, Number (pos, Float ((float_of_int a) +. b)))
| Add, Number (_, Float a), Number (_, Float b) ->
ok (env, Number (pos, Float (a +. b)))
| Subtract, Number (_, Int a), Number (_, Int b) ->
ok (env, Number (pos, Int (a - b)))
| Subtract, Number (_, Float a), Number (_, Int b) ->
ok (env, Number (pos, Float (a -. (float_of_int b))))
| Subtract, Number (_, Int a), Number (_, Float b) ->
ok (env, Number (pos, Float ((float_of_int a) -. b)))
| Subtract, Number (_, Float a), Number (_, Float b) ->
ok (env, Number (pos, Float (a -. b)))
| Multiply, Number (_, Int a), Number (_, Int b) ->
ok (env, Number (pos, Int (a * b)))
| Multiply, Number (_, Float a), Number (_, Int b) ->
ok (env, Number (pos, Float (a *. (float_of_int b))))
| Multiply, Number (_, Int a), Number (_, Float b) ->
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 (_, 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) ->
ok (env, Number (pos, Float (a /. (float_of_int b))))
| Divide, Number (_, Int a), Number (_, Float b) ->
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 (_, Int a), Number (_, Int b) ->
ok (env, Number (pos, Int (a mod b)))
| Modulo, Number (_, Float a), Number (_, Int b) ->
ok (env, Number (pos, Float (Float.rem a (float_of_int b))))
| Modulo, Number (_, Int a), Number (_, Float b) ->
ok (env, Number (pos, Float (Float.rem (float_of_int a) b)))
| Modulo, Number (_, Float a), Number (_, Float b) ->
ok (env, Number (pos, Float (Float.rem a b)))
| BitwiseOr, Number (_, Int a), Number (_, Int b) ->
ok (env, Number (pos, Int (a lor b)))
| BitwiseOr, Number (_, Float a), Number (_, Int b) ->
ok (env, Number (pos, Int (int_of_float a lor b)))
| BitwiseOr, Number (_, Int a), Number (_, Float b) ->
ok (env, Number (pos, Int (a lor int_of_float b)))
| BitwiseOr, Number (_, Float a), Number (_, Float b) ->
ok (env, Number (pos, Int (int_of_float a lor int_of_float b)))
| BitwiseAnd, Number (_, Int a), Number (_, Int b) ->
ok (env, Number (pos, Int (a land b)))
| BitwiseAnd, Number (_, Float a), Number (_, Int b) ->
ok (env, Number (pos, Int (int_of_float a land b)))
| BitwiseAnd, Number (_, Int a), Number (_, Float b) ->
ok (env, Number (pos, Int (a land int_of_float b)))
| BitwiseAnd, Number (_, Float a), Number (_, Float b) ->
ok (env, Number (pos, Int (int_of_float a land int_of_float b)))
| BitwiseXor, Number (_, Int a), Number (_, Int b) ->
ok (env, Number (pos, Int (a lxor b)))
| BitwiseXor, Number (_, Float a), Number (_, Int b) ->
ok (env, Number (pos, Int (int_of_float a lxor b)))
| BitwiseXor, Number (_, Int a), Number (_, Float b) ->
ok (env, Number (pos, Int (a lxor int_of_float b)))
| BitwiseXor, Number (_, Float a), Number (_, Float b) ->
ok (env, Number (pos, Int (int_of_float a lxor int_of_float b)))
| ShiftLeft, Number (_, Int a), Number (_, Int b) ->
ok (env, Number (pos, Int (a lsl b)))
| ShiftLeft, Number (_, Float a), Number (_, Int b) ->
ok (env, Number (pos, Int (int_of_float a lsl b)))
| ShiftLeft, Number (_, Int a), Number (_, Float b) ->
ok (env, Number (pos, Int (a lsl int_of_float b)))
| ShiftLeft, Number (_, Float a), Number (_, Float b) ->
ok (env, Number (pos, Int (int_of_float a lsl int_of_float b)))
| ShiftRight, Number (_, Int a), Number (_, Int b) ->
ok (env, Number (pos, Int (a lsr b)))
| ShiftRight, Number (_, Float a), Number (_, Int b) ->
ok (env, Number (pos, Int (int_of_float a lsr b)))
| ShiftRight, Number (_, Int a), Number (_, Float b) ->
ok (env, Number (pos, Int (a lsr int_of_float b)))
| ShiftRight, Number (_, Float a), Number (_, Float b) ->
ok (env, Number (pos, Int (int_of_float a lsr int_of_float b)))
| LogicalAnd, Bool (_, a), Bool (_, b) ->
ok (env, Bool (pos, a && b))
| LogicalOr, Bool (_, a), Bool (_, b) ->
ok (env, Bool (pos, a || b))
| Equality, Array (_, items1), Array (_, items2) ->
(* Early exit: skip evaluation if lengths differ for efficiency *)
if List.length items1 <> List.length items2 then
ok (env, Bool (pos, false))
else
let* (_, evaluated1) = interpret_array env (pos, items1) in
let* (_, evaluated2) = interpret_array env (pos, items2) in
ok (env, Bool (pos, evaluated1 =~ evaluated2))
| Equality, v1, v2 ->
let* (_, eval_expr1) = interpret env v1 in
let* (_, eval_expr2) = interpret env v2 in
ok (env, Bool (pos, eval_expr1 =~ eval_expr2))
| Inequality, v1, v2 ->
(* Inequality is, simply put, negation of equality *)
let* (_, expr) = interpret_arith_op env (pos, Equality, v1, v2) in
(match expr with
| Bool (_, value) -> ok (env, Bool (pos, not value))
| _ -> Error.trace Error.Msg.invalid_binary_op pos >>= error
)
| GreaterThan, v1, v2 ->
ok (env, Bool (pos, Compare.gt v1 v2))
| GreaterThanOrEqual, v1, v2 ->
ok (env, Bool (pos, Compare.gte v1 v2))
| LessThan, v1, v2 ->
ok (env, Bool (pos, Compare.lt v1 v2))
| LessThanOrEqual, v1, v2 ->
ok (env, Bool (pos, Compare.lte v1 v2))
| _ ->
Error.trace Error.Msg.invalid_binary_op pos >>= error
Most operations map directly to their OCaml equivalents. Equality we took care in the previous post. The exception is the comparison operators -- I had to abstract those under the Ast.Compare module:
module Compare = struct
let _cmp_num n1 n2 =
let to_float = function Int i -> float_of_int i | Float f -> f in
Float.compare (to_float n1) (to_float n2)
let _cmp_str s1 s2 = String.compare s1 s2
let _compare op v1 v2 =
match v1, v2 with
| Number (_, n1), Number (_, n2) -> op (_cmp_num n1 n2) 0
| String (_, s1), String (_, s2) -> op (_cmp_str s1 s2) 0
| _ ->
(* unreachable: the type checker rejects comparisons on
non-numeric/non-string types before evaluation *)
false
let gt v1 v2 = _compare (>) v1 v2
let gte v1 v2 = _compare (>=) v1 v2
let lt v1 v2 = _compare (<) v1 v2
let lte v1 v2 = _compare (<=) v1 v2
end
Number comparisons work as you'd expect. Strings are compared by unicode codepoint order, which OCaml's String.compare handles out of the box.
There's also a new interpret_in_op that checks if a field is present in an object:
// samples/operations/in.jsonnet
{
has_field: 'foo' in { foo: 1 },
has_no_field: 'xyz' in { foo: 1 },
local field_name = 'bar',
has_field_bar: field_name in { bar: 2 },
has_no_field_bar: field_name in { baz: 3 },
}
And the implementation:
and interpret_in_op env pos field obj =
match field, obj with
| String (_, field_str), EvaluatedObject (_, fields)
| Ident (_, field_str), EvaluatedObject (_, fields) ->
let field_exists = List.exists (fun (name, _) -> name = field_str) fields in
ok (env, Bool (pos, field_exists))
| String (_, field_str), RuntimeObject (_, _, fields)
| Ident (_, field_str), RuntimeObject (_, _, fields) ->
let field_exists = ObjectFields.exists (fun name -> name = field_str) fields in
ok (env, Bool (pos, field_exists))
| _ ->
Error.trace Error.Msg.invalid_binary_op pos >>= error
We must pattern match on both String and Ident -- these are the two variants we use to define field names in objects, e.g. { a: 42 } or { "a": 42 }.
Verifying
New sample files:
//samples/arrays/concat.jsonnet
[1, 2, 3] + [4]
// samples/operations/modulo.jsonnet
10 % 3
// samples/operations/shift_left.jsonnet
1 << 1
// samples/operations/shift_right.jsonnet
4 >> 1
// samples/operations/bitwise.jsonnet
{
or: 1.6 | 3,
xor: 1 ^ 3,
and: 5 & 3
}
// samples/operations/gt_gte_lt_lte.jsonnet
{
gt_true: 2 > 1,
gt_false: 1 > 2,
gte_true: 2 >= 2,
gte_false: 1 >= 2,
lt_true: 1 < 2,
lt_false: 2 < 1,
lte_true: 2 <= 2,
lte_false: 2 <= 1,
}
// samples/operations/in.jsonnet
{
has_field: 'foo' in { foo: 1 },
has_no_field: 'xyz' in { foo: 1 },
local field_name = 'bar',
has_field_bar: field_name in { bar: 2 },
has_no_field_bar: field_name in { baz: 3 },
}
// samples/operations/inequality.jsonnet
{
eq_number_number: 1 != 1,
dif_number_number: 1 != 2,
dif_number_str: "1" != 1,
eq_str_str: "42" != "42",
dif_str_str: "42" != "0",
eq_array_array: [1,2,3] != [1,2,3],
dif_array_array: [1,2,3] != [3,2,1],
eq_obj_obj: {x: 1, y: 2, z: 3} != {z: 3, x: 1, y: 2},
dif_obj_obj: {a: 1, b: 2} != {b: 2, c: 3},
eq_complex_obj_complex_obj: [{}, { x: 3 - 1 }] != [{}, { x: 2 }],
dif_complex_obj_complex_obj: [{ a: 1 }, { b: 2 }] != [{ b: 2 }, { a: 1 }],
}
// samples/operations/logical.jsonnet
{
and_true: true && true,
and_false: true && false,
or_true: false || true,
or_false: false || false
}
// samples/strings/comparison.jsonnet
{
// Two strings can be compared with `<` (unicode codepoint order).
gt_true: "é" > "e",
gte_true: "é" >= "e",
lt_true: "e" < "é",
lte_true: "e" <= "é",
gt_false: "é" < "e",
gte_false: "é" <= "e",
lt_false: "e" > "é",
lte_false: "e" >= "é",
ordered: std.sort(["b", "a", "é", "A"])
}
// samples/operations/in.jsonnet
{
has_field: 'foo' in { foo: 1 },
has_no_field: 'xyz' in { foo: 1 },
local field_name = 'bar',
has_field_bar: field_name in { bar: 2 },
has_no_field_bar: field_name in { baz: 3 },
}
Of course, I match each one with a cram test:
diff --git a/test/cram/arrays.t b/test/cram/arrays.t
new file mode 100644
index 0000000..b840584
--- /dev/null
+++ b/test/cram/arrays.t
@@ -0,0 +1,2 @@
+ $ tsonnet ../../samples/arrays/concat.jsonnet
+ [ 1, 2, 3, 4 ]
diff --git a/test/cram/operations.t b/test/cram/operations.t
index bb1c93a..503084f 100644
--- a/test/cram/operations.t
+++ b/test/cram/operations.t
@@ -1,22 +1,25 @@
- $ tsonnet ../../samples/equality.jsonnet
+ $ tsonnet ../../samples/operations/modulo.jsonnet
+ 1
+
+ $ tsonnet ../../samples/operations/equality.jsonnet
{
"dif_array_array": false,
"dif_complex_obj_complex_obj": false,
@@ -30,3 +33,23 @@
"eq_obj_obj": true,
"eq_str_str": true
}
+
+ $ tsonnet ../../samples/operations/bitwise.jsonnet
+ { "and": 1, "or": 3, "xor": 2 }
+
+ $ tsonnet ../../samples/operations/logical.jsonnet
+ { "and_false": false, "and_true": true, "or_false": false, "or_true": true }
+
+ $ tsonnet ../../samples/operations/in.jsonnet
+ {
+ "has_field": true,
+ "has_field_bar": true,
+ "has_no_field": false,
+ "has_no_field_bar": false
+ }
+
+ $ tsonnet ../../samples/operations/shift_left.jsonnet
+ 2
+
+ $ tsonnet ../../samples/operations/shift_right.jsonnet
+ 2
Conclusion
What looked like a mountain of work turned out to be quite mechanical -- once the scaffolding is in place, each new operator slotted in with minimal friction.
You can check the entire diff here.
Thanks for reading Bit Maybe Wise! If 'foo' in { foo: 1 } returns true for you too, subscribe.
Photo by Joachim Schnürle on Unsplash
Top comments (0)