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 refactored the AST to use named records, collapsed ClosureCall into FunctionCall, and cleaned up the grammar conflict that was keeping closures as second-class citizens:
Now that we support functions, just one more thing is missing on that front: methods!
The plan (such as it is)
// samples/functions/method.jsonnet
local object = {
my_method(x): x * x,
};
object.my_method(2)
Sneaking methods past the grammar
After implementing functions and pausing for a moment to reflect on how I'd implement methods, I realised that methods can be easily represented as closures that bind to an object field. So I did it:
diff --git a/lib/parser.mly b/lib/parser.mly
index e7b7771..0c5601d 100644
--- a/lib/parser.mly
+++ b/lib/parser.mly
@@ -101,6 +101,14 @@ obj_key:
obj_field:
| k = obj_key; COLON; e = assignable_expr { ObjectField (k, e) }
+ | k = obj_key;
+ LEFT_PAREN; params = separated_nonempty_list(COMMA, fundef_param); RIGHT_PAREN;
+ COLON; body = assignable_expr
+ { ObjectField (
+ k,
+ Closure (with_pos $startpos $endpos, { params = params; body = body })
+ )
+ }
| e = single_var { ObjectExpr e }
;
@@ -215,6 +223,9 @@ funcall:
| callee = scoped_expr;
LEFT_PAREN; params = separated_nonempty_list(COMMA, assignable_expr); RIGHT_PAREN
{ FunctionCall (with_pos $startpos $endpos, { callee = callee; args = params }) }
+ | callee = obj_field_access;
+ LEFT_PAREN; params = separated_nonempty_list(COMMA, assignable_expr); RIGHT_PAREN
+ { FunctionCall (with_pos $startpos $endpos, { callee = callee; args = params }) }
;
closure:
This approach avoids adding one more abstraction, and maps precisely onto the semantics.
Turns out, scope bugs don't fix themselves
diff --git a/lib/type.ml b/lib/type.ml
index ad4f333..4b438db 100644
--- a/lib/type.ml
+++ b/lib/type.ml
@@ -102,7 +102,12 @@ let rec collect_free_idents = function
| ObjectField (_, e) -> collect_free_idents e
| ObjectExpr e -> collect_free_idents e
) entries
- | ObjectFieldAccess (_, _, exprs) -> List.concat_map collect_free_idents exprs
+ | ObjectFieldAccess (_, scope, exprs) ->
+ let scope_idents = match scope with
+ | ObjVarRef name -> [name]
+ | Self | TopLevel -> []
+ in
+ scope_idents @ List.concat_map collect_free_idents exprs
| IndexedExpr (_, name, e) -> name :: collect_free_idents e
| Local (_, vars) -> List.concat_map (fun (_, e) -> collect_free_idents e) vars
| FunctionCall (_, call) ->
Cram tests corrected:
diff --git a/test/cram/objects.t b/test/cram/objects.t
index 1a402df..11cc9d1 100644
--- a/test/cram/objects.t
+++ b/test/cram/objects.t
@@ -23,32 +23,10 @@
{ "a": 1, "b": 3, "c": 4 }
$ tsonnet ../../samples/objects/untouched_field.jsonnet
- WARNING: ../../samples/objects/untouched_field.jsonnet:1:0 Unused variable result
-
- 1: local result = {
- ^^^^^^^^^^^^^^^^
- 2: a: 1,
- ^^^^^^^^^
- 3: b: 42,
- ^^^^^^^^^^
- ---
42
$ tsonnet ../../samples/objects/untouched_invalid_field.jsonnet
- WARNING: ../../samples/objects/untouched_invalid_field.jsonnet:1:0 Unused variable result
-
- 1: local result = {
- ^^^^^^^^^^^^^^^^
- 2: a: 1,
- ^^^^^^^^^
- 3: b: self.a,
- ^^^^^^^^^^^^^^
- 4: c: self.d,
- ^^^^^^^^^^^^^^
- 5: d: self.c
- ^^^^^^^^^^^^^
- ---
WARNING: ../../samples/objects/untouched_invalid_field.jsonnet:1:15 Cyclic reference found for 1->c
1: local result = {
diff --git a/test/cram/semantics.t b/test/cram/semantics.t
index c9a790e..3f26924 100644
--- a/test/cram/semantics.t
+++ b/test/cram/semantics.t
@@ -288,17 +288,6 @@
$ tsonnet ../../samples/semantics/valid_object_access_non_cyclic_field.jsonnet
- WARNING: ../../samples/semantics/valid_object_access_non_cyclic_field.jsonnet:1:0 Unused variable obj
-
- 1: local obj = {
- ^^^^^^^^^^^^^
- 2: a: 1,
- ^^^^^^^^^
- 3: b: self.c,
- ^^^^^^^^^^^^^^
- 4: c: self.b,
- ^^^^^^^^^^^^^^
- ---
WARNING: ../../samples/semantics/valid_object_access_non_cyclic_field.jsonnet:1:12 Cyclic reference found for 1->b
1: local obj = {
Does it work?
Yes, it does:
$ dune exec -- tsonnet samples/functions/method.jsonnet
4
diff --git a/test/cram/functions.t b/test/cram/functions.t
index 5c77837..9a9fe4d 100644
--- a/test/cram/functions.t
+++ b/test/cram/functions.t
@@ -9,3 +9,6 @@
$ tsonnet ../../samples/functions/closure.jsonnet
25
+
+ $ tsonnet ../../samples/functions/method.jsonnet
+ 4
Conclusion
Methods turned out to be less of a feature and more of a realisation: if you squint at them right, they're just closures sitting inside object fields, and the parser was already halfway there. Two small grammar rules and a scope bug fix later, object.my_method(2) just works. The scope fix was an interesting part — collect_free_idents was silently dropping the receiver when traversing ObjectFieldAccess, which meant variables used as method receivers could slip past the unused-variable analysis unnoticed. Worth catching.
Here is the entire diff.
Next up, time to wrap it up!
Thanks for reading Bit Maybe Wise! object.my_method(2) now returns 4. Your inbox could return something good too -- subscribe and square up.
Photo by Waldemar Brandt on Unsplash

Top comments (0)