Tsonnet #43 - Object fields, now with an opt-out clause
Tsonnet gains computed field names, letting object fields exist conditionally.
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 got conditionals working:
Turns out conditionals had one more trick to pull: deciding whether a field exists at all.
The target
I need to cover the Computed Field Names tutorial as the next step for Tsonnet:
// samples/tutorials/computed-fields.jsonnet
local Margarita(salted) = {
ingredients: [
{ kind: 'Tequila Blanco', qty: 2 },
{ kind: 'Lime', qty: 1 },
{ kind: 'Cointreau', qty: 1 },
],
[if salted then 'garnish']: 'Salt',
};
{
Margarita: Margarita(true),
'Margarita Unsalted': Margarita(false),
}Let’s target only the part we are interested in, adding new attributes to samples/conditionals/conditionals.jsonnet:
// samples/conditionals/conditionals.jsonnet
{
// ...
[if true then 'conditional_attribute_then']: true,
[if false then '...']: false,
[if false then '...' else 'conditional_attribute_else']: false
}There’s one caveat though:
$ dune exec -- tsonnet samples/conditionals/conditionals.jsonnet
ERROR: samples/conditionals/conditionals.jsonnet:17:5 Parsing error. Invalid syntax:
17: [if true then 'conditional_attribute_then']: true,
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
$ dune exec -- tsonnet samples/tutorials/computed-fields.jsonnet
ERROR: samples/tutorials/computed-fields.jsonnet:7:3 Parsing error. Invalid syntax:
7: [if salted then 'garnish']: 'Salt',
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
There’s no object’s conditional attribute support yet.
Let’s add new testing samples
We need to cover a few cases other than the three conditional attributes shown above.
Non string attributes:
// samples/conditionals/conditional_attr_not_string.jsonnet
{
[if true then 42]: "value"
}Cyclic references in conditional fields:
// samples/semantics/invalid_conditional_field_cyclic_key.jsonnet
{
a: self.b,
b: self.a,
[if true then self.a else "x"]: "value",
}Ignored cyclic reference field:
// samples/semantics/valid_conditional_field_access_cyclic.jsonnet
local obj = {
a: 1,
[if true then "b"]: self.c,
c: self.b,
};
obj.aA new object entry variant
The parser needs only one more rule for obj_field -- pretty straightforward:
The scope validation is also straightforward. First we check the field_expr, followed by the assignment expr:
Type checking
The type checking phase is where most of the complexity for this feature lies. Let’s break it down.
I had to carry the string with the Tstring type variant to be able to do certain checks:
This, of course, led to many changes down the line. The semantics didn’t change — I only had to fix the pattern-matching. Example:
I’m intentionally dropping the other cases like that to keep this post cleaner, and more to the point. The entire thing can be seen in the complete diff shared in the conclusion.
We’ll need the semantic_type_equal function:
Everything other than Tstring can be compared structurally. Though this will likely need to be revisited soon.
When calling translate_object, we must pattern-match the new variant:
Since this field is a conditional, we must check the expr for cycles. After that, we can translate it, and extract the field name from Tstring to add the body expr to the environment. Nulls are ignored, just like Jsonnet.
The next step is the cyclic reference check for the attributes’ body expr, and conditional fields are not different:
And the last step in translate_object is to translate each field:
The cycle check above requires new code to deal with ObjectConditionalField:
Where we check each expr for cycles:
And to finish it off the type checking phase, we check if the two branch types are semantically equal:
And with that, we also add more error keys to the error module:
Evaluating
The evaluation part is straightforward, after the type checker did the heavy lifting:
Moment of truth
Jsonnet does not allow referencing the own object in conditional fields:
$ jsonnet samples/semantics/invalid_conditional_field_cyclic_key.jsonnet
samples/semantics/invalid_conditional_field_cyclic_key.jsonnet:4:19-23 Can't use self outside of an object.
[if true then self.a else "x"]: "value",
Tsonnet does, and will complain about the cyclic access:
$ dune exec -- tsonnet samples/semantics/invalid_conditional_field_cyclic_key.jsonnet
ERROR: samples/semantics/invalid_conditional_field_cyclic_key.jsonnet:3:7 Cyclic reference found for 1->a
3: b: self.a,
^^^^^^^^^^^^^^To be honest, I’m not certain which behavior is best to keep here. I believe allowing self references makes sense in a lazy language accessing references from its own scope. I will keep betting on this behavior until I hit a wall of unmanageable complexity. Until then, better checks for us!
In another scenario, Jsonnet goes on with the cyclic access:
$ jsonnet samples/semantics/invalid_conditional_field_cyclic_value.jsonnet
RUNTIME ERROR: max stack frames exceeded.
samples/semantics/invalid_conditional_field_cyclic_value.jsonnet:3:25-31 object <anonymous>
samples/semantics/invalid_conditional_field_cyclic_value.jsonnet:4:8-14 object <anonymous>
samples/semantics/invalid_conditional_field_cyclic_value.jsonnet:3:25-31 object <anonymous>
samples/semantics/invalid_conditional_field_cyclic_value.jsonnet:4:8-14 object <anonymous>
samples/semantics/invalid_conditional_field_cyclic_value.jsonnet:3:25-31 object <anonymous>
samples/semantics/invalid_conditional_field_cyclic_value.jsonnet:4:8-14 object <anonymous>
samples/semantics/invalid_conditional_field_cyclic_value.jsonnet:3:25-31 object <anonymous>
samples/semantics/invalid_conditional_field_cyclic_value.jsonnet:4:8-14 object <anonymous>
samples/semantics/invalid_conditional_field_cyclic_value.jsonnet:3:25-31 object <anonymous>
samples/semantics/invalid_conditional_field_cyclic_value.jsonnet:4:8-14 object <anonymous>
...
samples/semantics/invalid_conditional_field_cyclic_value.jsonnet:4:8-14 object <anonymous>
samples/semantics/invalid_conditional_field_cyclic_value.jsonnet:3:25-31 object <anonymous>
samples/semantics/invalid_conditional_field_cyclic_value.jsonnet:4:8-14 object <anonymous>
samples/semantics/invalid_conditional_field_cyclic_value.jsonnet:3:25-31 object <anonymous>
samples/semantics/invalid_conditional_field_cyclic_value.jsonnet:4:8-14 object <anonymous>
samples/semantics/invalid_conditional_field_cyclic_value.jsonnet:3:25-31 object <anonymous>
samples/semantics/invalid_conditional_field_cyclic_value.jsonnet:4:8-14 object <anonymous>
samples/semantics/invalid_conditional_field_cyclic_value.jsonnet:3:25-31 object <anonymous>
Field "b"
During manifestationTsonnet will report an error -- plus warnings:
dune exec -- tsonnet samples/semantics/invalid_conditional_field_cyclic_value.jsonnet
WARNING: samples/semantics/invalid_conditional_field_cyclic_value.jsonnet:1:0 Cyclic reference found for 1->b
1: {
^
2: a: 1,
^^^^^^^^^
3: [if true then "b"]: self.c,
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
4: c: self.b,
^^^^^^^^^^^^^^
---
WARNING: samples/semantics/invalid_conditional_field_cyclic_value.jsonnet:1:0 Cyclic reference found for 1->c
1: {
^
2: a: 1,
^^^^^^^^^
3: [if true then "b"]: self.c,
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
4: c: self.b,
^^^^^^^^^^^^^^
---
ERROR: samples/semantics/invalid_conditional_field_cyclic_value.jsonnet:3:29 Cyclic reference found for 1->c
3: [if true then "b"]: self.c,
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^For untouched object fields with cyclic references, Jsonnet ignores them, as expected of a lazy language:
$ jsonnet samples/semantics/valid_conditional_field_access_cyclic.jsonnet
1But we can do better, and Tsonnet warns about the cyclic references, without erroring in this case because the code is semantically valid:
$ dune exec -- tsonnet samples/semantics/valid_conditional_field_access_cyclic.jsonnet
WARNING: samples/semantics/valid_conditional_field_access_cyclic.jsonnet:1:12 Cyclic reference found for 1->b
1: local obj = {
^^^^^^^^^^^^^
2: a: 1,
3: [if true then "b"]: self.c,
^^^^^^^^^^^^^^^^^^^
4: c: self.b,
^^
---
WARNING: samples/semantics/valid_conditional_field_access_cyclic.jsonnet:1:12 Cyclic reference found for 1->c
1: local obj = {
^^^^^^^^^^^^^
2: a: 1,
3: [if true then "b"]: self.c,
^^^^^^^^^^^^^^^^^^^
4: c: self.b,
^^
---
1This is the kind of behavior that passes during development, but can be tightened to treat warnings as errors before pushing code to production. Eventually, I’ll add a flag to the compiler to enforce that.
Here are the cram tests to capture all the content above:
diff --git a/test/cram/conditionals.t b/test/cram/conditionals.t
new file mode 100644
index 0000000..a12933d
--- /dev/null
+++ b/test/cram/conditionals.t
@@ -0,0 +1,20 @@
+ $ tsonnet ../../samples/conditionals/conditionals.jsonnet
+ {
+ "cond_else_expr": 15,
+ "cond_else_false": "else branch",
+ "cond_else_true": "then branch",
+ "cond_nested_chain": 42,
+ "cond_nested_object": { "result": "this one" },
+ "cond_null": null,
+ "cond_true": "if true works!",
+ "conditional_attribute_else": false,
+ "conditional_attribute_then": true
+ }
+
+
+ $ tsonnet ../../samples/conditionals/conditional_attr_not_string.jsonnet
+ ERROR: ../../samples/conditionals/conditional_attr_not_string.jsonnet:2:3 Conditional field key must be String or Null, got Number
+
+ 2: [if true then 42]: "value"
+ ^^^^^^^^^^^^^^^^^^^^^
+ [1]
diff --git a/test/cram/semantics.t b/test/cram/semantics.t
index 14f5911..67bd204 100644
--- a/test/cram/semantics.t
+++ b/test/cram/semantics.t
@@ -332,3 +332,75 @@
^^^^^^^^^^^^^^^^^^^^^^^^^^
[1]
+
+ $ tsonnet ../../samples/semantics/invalid_conditional_field_cyclic_value.jsonnet
+ WARNING: ../../samples/semantics/invalid_conditional_field_cyclic_value.jsonnet:1:0 Cyclic reference found for 1->b
+
+ 1: {
+ ^
+ 2: a: 1,
+ ^^^^^^^^^
+ 3: [if true then "b"]: self.c,
+ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
+ 4: c: self.b,
+ ^^^^^^^^^^^^^^
+ ---
+ WARNING: ../../samples/semantics/invalid_conditional_field_cyclic_value.jsonnet:1:0 Cyclic reference found for 1->c
+
+ 1: {
+ ^
+ 2: a: 1,
+ ^^^^^^^^^
+ 3: [if true then "b"]: self.c,
+ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
+ 4: c: self.b,
+ ^^^^^^^^^^^^^^
+ ---
+ ERROR: ../../samples/semantics/invalid_conditional_field_cyclic_value.jsonnet:3:29 Cyclic reference found for 1->c
+
+ 3: [if true then "b"]: self.c,
+ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
+ [1]
+
+
+ $ tsonnet ../../samples/semantics/valid_conditional_field_access_cyclic.jsonnet
+ WARNING: ../../samples/semantics/valid_conditional_field_access_cyclic.jsonnet:1:12 Cyclic reference found for 1->b
+
+ 1: local obj = {
+ ^^^^^^^^^^^^^
+ 2: a: 1,
+
+ 3: [if true then "b"]: self.c,
+ ^^^^^^^^^^^^^^^^^^^
+ 4: c: self.b,
+ ^^
+ ---
+ WARNING: ../../samples/semantics/valid_conditional_field_access_cyclic.jsonnet:1:12 Cyclic reference found for 1->c
+
+ 1: local obj = {
+ ^^^^^^^^^^^^^
+ 2: a: 1,
+
+ 3: [if true then "b"]: self.c,
+ ^^^^^^^^^^^^^^^^^^^
+ 4: c: self.b,
+ ^^
+ ---
+ ERROR: ../../samples/semantics/valid_conditional_field_access_cyclic.jsonnet:4:7 Cyclic reference found for 1->b
+
+ 4: c: self.b,
+ ^^^^^^^^^^^^^^
+ [1]
+
+
+ $ tsonnet ../../samples/semantics/invalid_conditional_field_cyclic_key.jsonnet
+ WARNING: ../../samples/semantics/invalid_conditional_field_cyclic_key.jsonnet:4:5 Cyclic reference found in conditional field key
+
+ 4: [if true then self.a else "x"]: "value",
+ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
+ ---
+ ERROR: ../../samples/semantics/invalid_conditional_field_cyclic_key.jsonnet:3:7 Cyclic reference found for 1->a
+
+ 3: b: self.a,
+ ^^^^^^^^^^^^^^
+ [1]
diff --git a/test/cram/tutorials.t b/test/cram/tutorials.t
index b3c1bc9..0ba664c 100644
--- a/test/cram/tutorials.t
+++ b/test/cram/tutorials.t
@@ -175,3 +175,22 @@
"served": "Straight Up"
}
}
+
+ $ tsonnet ../../samples/tutorials/computed-fields.jsonnet
+ {
+ "Margarita": {
+ "garnish": "Salt",
+ "ingredients": [
+ { "kind": "Tequila Blanco", "qty": 2 },
+ { "kind": "Lime", "qty": 1 },
+ { "kind": "Cointreau", "qty": 1 }
+ ]
+ },
+ "Margarita Unsalted": {
+ "ingredients": [
+ { "kind": "Tequila Blanco", "qty": 2 },
+ { "kind": "Lime", "qty": 1 },
+ { "kind": "Cointreau", "qty": 1 }
+ ]
+ }
+ }
Conclusion
Computed field names done — and with them, the type checker took on most of the weight.
I’m still betting on letting self-references slide where Jsonnet won’t, trading strictness for warnings I can promote to errors later. Future me gets to decide if that was wisdom or hubris.
The entire diff can be seen here.
















