Welcome to the Tsonnet series!
If you're just joining, you can check out how it all started in the first post of the series.
In the previous post, we added comments to Tsonnet:
Now, let's implement unary operations.
The simplest unary operation
Starting with a new sample Jsonnet file:
diff --git a/samples/unary_operations.jsonnet b/samples/unary_operations.jsonnet
new file mode 100644
index 0000000..f8b1d9c
--- /dev/null
+++ b/samples/unary_operations.jsonnet
@@ -0,0 +1 @@
+-666
Which interprets to -- well, itself:
$ jsonnet samples/unary_operations.jsonnet
-666
This is a boring example, but let's stay with it for now. Baby steps!
Let's add the cram test:
diff --git a/test/cram/unary_operations.t b/test/cram/unary_operations.t
new file mode 100644
index 0000000..09163a7
--- /dev/null
+++ b/test/cram/unary_operations.t
@@ -0,0 +1,2 @@
+ $ tsonnet ../../samples/unary_operations.jsonnet
+ -666
We need a new type to represent unary operations:
diff --git a/lib/ast.ml b/lib/ast.ml
index d34a152..ad32d95 100644
--- a/lib/ast.ml
+++ b/lib/ast.ml
@@ -4,6 +4,10 @@ type bin_op =
| Multiply
| Divide
+type unary_op =
+ | Plus
+ | Minus
+
type number =
| Int of int
| Float of float
@@ -17,3 +21,4 @@ type expr =
| Array of expr list
| Object of (string * expr) list
| BinOp of bin_op * expr * expr
+ | UnaryOp of unary_op * expr
When it comes to the lexer, I put the cart before the horses earlier, including the minus sign as part of number patterns. However, it can be simplified by applying the -
and +
unary operations:
diff --git a/lib/lexer.mll b/lib/lexer.mll
index 9245a14..22b7602 100644
--- a/lib/lexer.mll
+++ b/lib/lexer.mll
@@ -8,10 +8,10 @@
let white = [' ' '\t']+
let newline = '\r' | '\n' | "\r\n"
let digit = ['0'-'9']
-let int = '-'? digit+
+let int = digit+
let frac = '.' digit*
let exp = ['e' 'E']['-' '+']? digit+
-let float = '-'? digit* frac? exp?
+let float = digit* frac? exp?
let null = "null"
let bool = "true" | "false"
let letter = ['a'-'z' 'A'-'Z']
@@ -35,8 +35,8 @@ rule read =
| '}' { RIGHT_CURLY_BRACKET }
| ',' { COMMA }
| ':' { COLON }
- | '+' { ADD }
- | '-' { SUBTRACT }
+ | '+' { PLUS }
+ | '-' { MINUS }
| '*' { MULTIPLY }
| '/' { DIVIDE }
| id { ID (Lexing.lexeme lexbuf) }
I renamed the ADD and SUBTRACT tokens to PLUS and MINUS. The ADD and SUBTRACT tokens added semantic meaning, and since +
and -
are used for both unary and binary operations, it makes sense to reduce them to their names rather than their arithmetic semantic meanings.
We don't need to drop the specific types for arithmetic operations, however. In the parser, we can disambiguate them -- the more specific, the better:
diff --git a/lib/parser.mly b/lib/parser.mly
index a224ea3..5567c95 100644
--- a/lib/parser.mly
+++ b/lib/parser.mly
@@ -13,8 +13,8 @@
%token LEFT_CURLY_BRACKET
%token RIGHT_CURLY_BRACKET
%token COLON
-%token ADD SUBTRACT MULTIPLY DIVIDE
-%left ADD SUBTRACT
+%token PLUS MINUS MULTIPLY DIVIDE
+%left PLUS MINUS
%left MULTIPLY DIVIDE
%token <string> ID
%token EOF
@@ -36,10 +36,12 @@ expr:
| id = ID { Ident id }
| LEFT_SQR_BRACKET; values = list_fields; RIGHT_SQR_BRACKET { Array values }
| LEFT_CURLY_BRACKET; attrs = obj_fields; RIGHT_CURLY_BRACKET { Object attrs }
- | e1 = expr; ADD; e2 = expr { BinOp (Add, e1, e2) }
- | e1 = expr; SUBTRACT; e2 = expr { BinOp (Subtract, e1, e2) }
+ | e1 = expr; PLUS; e2 = expr { BinOp (Add, e1, e2) }
+ | e1 = expr; MINUS; e2 = expr { BinOp (Subtract, e1, e2) }
| e1 = expr; MULTIPLY; e2 = expr { BinOp (Multiply, e1, e2) }
| e1 = expr; DIVIDE; e2 = expr { BinOp (Divide, e1, e2) }
+ | PLUS; e = expr; { UnaryOp (Plus, e) }
+ | MINUS; e = expr; { UnaryOp (Minus, e) }
;
list_fields:
By having types for each specific operation, we can make our compiler semantics stronger.
The only thing left now is a pattern match on the new operation in the interpreter:
diff --git a/lib/tsonnet.ml b/lib/tsonnet.ml
index 94e185c..4ad4ab5 100644
--- a/lib/tsonnet.ml
+++ b/lib/tsonnet.ml
@@ -40,18 +40,29 @@ let interpret_concat_op (e1 : expr) (e2 : expr) : (expr, string) result =
ok (String (s1^s2))
| _ -> error "invalid string concatenation operation"
+let interpret_unary_op (op: unary_op) (n: number) =
+ match op, n with
+ | Plus, _ -> n
+ | Minus, (Int i) -> Int (-i)
+ | Minus, (Float f) -> Float (-. f)
+
(** [interpret expr] interprets and reduce the intermediate AST [expr] into a result AST. *)
let rec interpret (e: expr) : (expr, string) result =
match e with
| Null | Bool _ | String _ | Number _ | Array _ | Object _ | Ident _ -> ok e
| BinOp (op, e1, e2) ->
- let* e1' = interpret e1 in
+ (let* e1' = interpret e1 in
let* e2' = interpret e2 in
match op, e1', e2' with
| Add, (String _ as expr1), (_ as expr2) | Add, (_ as expr1), (String _ as expr2) ->
interpret_concat_op expr1 expr2
| _, Number v1, Number v2 -> ok (interpret_arith_op op v1 v2)
- | _ -> error "invalid binary operation"
+ | _ -> error "invalid binary operation")
+ | UnaryOp (op, expr) ->
+ (let* e' = interpret expr in
+ match e' with
+ | Number v -> ok (Number (interpret_unary_op op v))
+ | _ -> error "invalid unary operation")
let run (s: string) : (string, string) result =
parse s >>= interpret >>= Json.expr_to_string
And voilà:
$ dune exec -- tsonnet samples/unary_operations.jsonnet
-666
So far, Tsonnet is missing parentheses to delineate operation precedence. Let's add them.
Parentheses and precedence
For example:
diff --git a/samples/operation_precedence.jsonnet b/samples/operation_precedence.jsonnet
new file mode 100644
index 0000000..2d5b174
--- /dev/null
+++ b/samples/operation_precedence.jsonnet
@@ -0,0 +1 @@
+((1 + 2) * 3) / 4.0 - 6.0 / (5 + 41)
Updating the cram test to include the operation precedence test (and consolidate the operations test in a single file):
diff --git a/samples/unary_operations.jsonnet b/samples/unary_operations.jsonnet
index f8b1d9c..4120fa9 100644
--- a/samples/unary_operations.jsonnet
+++ b/samples/unary_operations.jsonnet
@@ -1 +1 @@
--666
+-624 + (-42)
diff --git a/test/cram/operations.t b/test/cram/operations.t
new file mode 100644
index 0000000..bb2dd83
--- /dev/null
+++ b/test/cram/operations.t
@@ -0,0 +1,8 @@
+ $ tsonnet ../../samples/binary_operations.jsonnet
+ 42.3
+
+ $ tsonnet ../../samples/unary_operations.jsonnet
+ -666
+
+ $ tsonnet ../../samples/operation_precedence.jsonnet
+ 2.119565217391304
diff --git a/test/cram/unary_operations.t b/test/cram/unary_operations.t
deleted file mode 100644
index 09163a7..0000000
--- a/test/cram/unary_operations.t
+++ /dev/null
@@ -1,2 +0,0 @@
- $ tsonnet ../../samples/unary_operations.jsonnet
- -666
The lexer will parse the symbols, and the parser will evaluate the expression wrapped by the symbols and throw away the parentheses:
diff --git a/lib/lexer.mll b/lib/lexer.mll
index 22b7602..b429abe 100644
--- a/lib/lexer.mll
+++ b/lib/lexer.mll
@@ -33,6 +33,8 @@ rule read =
| ']' { RIGHT_SQR_BRACKET }
| '{' { LEFT_CURLY_BRACKET }
| '}' { RIGHT_CURLY_BRACKET }
+ | '(' { LEFT_PAREN }
+ | ')' { RIGHT_PAREN }
| ',' { COMMA }
| ':' { COLON }
| '+' { PLUS }
diff --git a/lib/parser.mly b/lib/parser.mly
index 5567c95..8b5a0ce 100644
--- a/lib/parser.mly
+++ b/lib/parser.mly
@@ -9,6 +9,8 @@
%token <string> STRING
%token LEFT_SQR_BRACKET
%token RIGHT_SQR_BRACKET
+%token LEFT_PAREN
+%token RIGHT_PAREN
%token COMMA
%token LEFT_CURLY_BRACKET
%token RIGHT_CURLY_BRACKET
@@ -34,6 +36,7 @@ expr:
| b = BOOL { Bool b }
| s = STRING { String s }
| id = ID { Ident id }
+ | LEFT_PAREN; e = expr; RIGHT_PAREN { e }
| LEFT_SQR_BRACKET; values = list_fields; RIGHT_SQR_BRACKET { Array values }
| LEFT_CURLY_BRACKET; attrs = obj_fields; RIGHT_CURLY_BRACKET { Object attrs }
| e1 = expr; PLUS; e2 = expr { BinOp (Add, e1, e2) }
Result:
$ dune exec -- tsonnet samples/operation_precedence.jsonnet
2.119565217391304
So far, so good. But there's more.
To be or not to be
We need to support negation and the bitwise negation, too. It's pretty easy to add them now.
Let's add the sample files and tests:
diff --git a/samples/bitwise.jsonnet b/samples/bitwise.jsonnet
new file mode 100644
index 0000000..659aa5b
--- /dev/null
+++ b/samples/bitwise.jsonnet
@@ -0,0 +1 @@
+~1
diff --git a/samples/not_false.jsonnet b/samples/not_false.jsonnet
new file mode 100644
index 0000000..8c9104a
--- /dev/null
+++ b/samples/not_false.jsonnet
@@ -0,0 +1 @@
+!false
diff --git a/samples/not_true.jsonnet b/samples/not_true.jsonnet
new file mode 100644
index 0000000..b66ac98
--- /dev/null
+++ b/samples/not_true.jsonnet
@@ -0,0 +1 @@
+!true
diff --git a/test/cram/operations.t b/test/cram/operations.t
index bb2dd83..3e23509 100644
--- a/test/cram/operations.t
+++ b/test/cram/operations.t
@@ -6,3 +6,12 @@
$ tsonnet ../../samples/operation_precedence.jsonnet
2.119565217391304
+
+ $ tsonnet ../../samples/not_true.jsonnet
+ false
+
+ $ tsonnet ../../samples/not_false.jsonnet
+ true
+
+ $ tsonnet ../../samples/bitwise.jsonnet
+ -2
Now, we add the new unary types and translate the expressions:
diff --git a/lib/ast.ml b/lib/ast.ml
index ad32d95..00ead06 100644
--- a/lib/ast.ml
+++ b/lib/ast.ml
@@ -7,6 +7,8 @@ type bin_op =
type unary_op =
| Plus
| Minus
+ | Not
+ | BitwiseNot
type number =
| Int of int
diff --git a/lib/lexer.mll b/lib/lexer.mll
index b429abe..8dca713 100644
--- a/lib/lexer.mll
+++ b/lib/lexer.mll
@@ -41,6 +41,8 @@ rule read =
| '-' { MINUS }
| '*' { MULTIPLY }
| '/' { DIVIDE }
+ | '!' { NOT }
+ | '~' { BITWISE_NOT }
| id { ID (Lexing.lexeme lexbuf) }
| _ { raise (SyntaxError ("Unexpected char: " ^ Lexing.lexeme lexbuf)) }
| eof { EOF }
diff --git a/lib/parser.mly b/lib/parser.mly
index 8b5a0ce..0167984 100644
--- a/lib/parser.mly
+++ b/lib/parser.mly
@@ -19,6 +19,8 @@
%left PLUS MINUS
%left MULTIPLY DIVIDE
%token <string> ID
+%token NOT BITWISE_NOT
+%left NOT BITWISE_NOT
%token EOF
%start <Ast.expr> prog
@@ -45,6 +47,8 @@ expr:
| e1 = expr; DIVIDE; e2 = expr { BinOp (Divide, e1, e2) }
| PLUS; e = expr; { UnaryOp (Plus, e) }
| MINUS; e = expr; { UnaryOp (Minus, e) }
+ | NOT; e = expr; { UnaryOp (Not, e) }
+ | BITWISE_NOT; e = expr; { UnaryOp (BitwiseNot, e) }
;
list_fields:
diff --git a/lib/tsonnet.ml b/lib/tsonnet.ml
index 4ad4ab5..defc5f3 100644
--- a/lib/tsonnet.ml
+++ b/lib/tsonnet.ml
@@ -40,11 +40,14 @@ let interpret_concat_op (e1 : expr) (e2 : expr) : (expr, string) result =
ok (String (s1^s2))
| _ -> error "invalid string concatenation operation"
-let interpret_unary_op (op: unary_op) (n: number) =
- match op, n with
- | Plus, _ -> n
- | Minus, (Int i) -> Int (-i)
- | Minus, (Float f) -> Float (-. f)
+let interpret_unary_op (op: unary_op) (evaluated_expr: expr) =
+ match op, evaluated_expr with
+ | Plus, number -> ok number
+ | Minus, Number (Int i) -> ok (Number (Int (-i)))
+ | Minus, Number (Float f) -> ok (Number (Float (-. f)))
+ | Not, (Bool b) -> ok (Bool (not b))
+ | BitwiseNot, Number (Int i) -> ok (Number (Int (lnot i)))
+ | _ -> error "invalid unary operation"
(** [interpret expr] interprets and reduce the intermediate AST [expr] into a result AST. *)
let rec interpret (e: expr) : (expr, string) result =
@@ -58,11 +61,7 @@ let rec interpret (e: expr) : (expr, string) result =
interpret_concat_op expr1 expr2
| _, Number v1, Number v2 -> ok (interpret_arith_op op v1 v2)
| _ -> error "invalid binary operation")
- | UnaryOp (op, expr) ->
- (let* e' = interpret expr in
- match e' with
- | Number v -> ok (Number (interpret_unary_op op v))
- | _ -> error "invalid unary operation")
+ | UnaryOp (op, expr) -> interpret expr >>= interpret_unary_op op
let run (s: string) : (string, string) result =
parse s >>= interpret >>= Json.expr_to_string
Unlike the Plus
and Minus
operations, the Not
and BitwiseNot
operations work on booleans rather than numbers. This meant refactoring interpret_unary_op
to accept an expression, thus moving the error-handling there. But the pattern-matching on UnaryOp could be simplified to a one-liner. It's important to emphasize that we need to evaluate the expression before handing it over to interpret_unary_op
as this way we can deal with evaluated types, such as Number
or Bool
-- calling interpret
from interpret_unary_op
wouldn't compile since the OCaml compiler requires that definitions should be declared from top to bottom.
Conclusion
With unary operations now fully implemented in Tsonnet, our language has taken another step toward feature completeness. We've added support for plus (+), minus (-), logical not (!), and bitwise not (~) operators, along with proper parentheses for expression grouping.
In the next post, we'll explore adding even more language features to make Tsonnet even more powerful and expressive.
Enjoyed this Tsonnet adventure? Subscribe to Bit Maybe Wise for more programming language explorations and not miss a development update!
Photo by Jojo Yuen (sharemyfoodd) on Unsplash
Top comments (0)