DEV Community

Hercules Lemke Merscher
Hercules Lemke Merscher

Posted on • Originally published at bitmaybewise.substack.com

Tsonnet #43 - Object fields, now with an opt-out clause

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.

fork path

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),
}
Enter fullscreen mode Exit fullscreen mode

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
}
Enter fullscreen mode Exit fullscreen mode

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',
   ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^

Enter fullscreen mode Exit fullscreen mode

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"
}
Enter fullscreen mode Exit fullscreen mode
  • 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",
}
Enter fullscreen mode Exit fullscreen mode
  • 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.a
Enter fullscreen mode Exit fullscreen mode

A new object entry variant

diff --git a/lib/ast.ml b/lib/ast.ml
index 35f88a6..fdb5bbe 100644
--- a/lib/ast.ml
+++ b/lib/ast.ml
@@ -97,6 +97,7 @@ type expr =

 and object_entry =
   | ObjectField of string * expr
+  | ObjectConditionalField of expr * expr
   | ObjectExpr of expr
 and object_scope =
   | Self
@@ -236,6 +237,7 @@ let rec string_of_type = function

 and string_of_object_entry = function
   | ObjectField (field, expr) -> field ^ ": " ^ string_of_type expr
+  | ObjectConditionalField (field, expr) -> string_of_type field ^ ": " ^ string_of_type expr
   | ObjectExpr expr -> string_of_type expr
Enter fullscreen mode Exit fullscreen mode

The parser needs only one more rule for obj_field -- pretty straightforward:

diff --git a/lib/parser.mly b/lib/parser.mly
index 40858f4..4e7775e 100644
--- a/lib/parser.mly
+++ b/lib/parser.mly
@@ -113,6 +113,7 @@ obj_field:
         Closure (with_pos $startpos $endpos, { params = params; body = body })
       )
     }
+  | LEFT_SQR_BRACKET; k = conditional; RIGHT_SQR_BRACKET; COLON; e = assignable_expr { ObjectConditionalField (k, e) }
   | e = single_var { ObjectExpr e }
   ;
Enter fullscreen mode Exit fullscreen mode

The scope validation is also straightforward. First we check the field_expr, followed by the assignment expr:

diff --git a/lib/scope.ml b/lib/scope.ml
index df55eea..3f329fe 100644
--- a/lib/scope.ml
+++ b/lib/scope.ml
@@ -82,6 +82,7 @@ and validate_object_entries entries context =
       acc >>= fun _ ->
       match entry with
       | ObjectField (_, expr) -> _validate expr context
+      | ObjectConditionalField (field_expr, expr) -> _validate field_expr context >>= fun () -> _validate expr context
       | ObjectExpr expr -> _validate expr context
     )
     (ok ())
Enter fullscreen mode Exit fullscreen mode

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:

diff --git a/lib/type.ml b/lib/type.ml
index 88e5b3a..ce61163 100644
--- a/lib/type.ml
+++ b/lib/type.ml
@@ -9,7 +9,7 @@ type tsonnet_type =
   | Tnull
   | Tbool
   | Tnumber
-  | Tstring
+  | Tstring of string
   | Tany
   | Tarray of tsonnet_type
   | Tobject of t_object_entry list
@@ -47,7 +47,7 @@ let rec to_string = function
   | Tnull -> "Null"
   | Tbool -> "Bool"
   | Tnumber -> "Number"
-  | Tstring -> "String"
+  | Tstring _ -> "String"
   | Tany -> "Any"
   | Tarray ty -> "Array of " ^ to_string ty
   | Tobject fields ->
Enter fullscreen mode Exit fullscreen mode
let rec translate venv expr =
   match expr with
   | Unit -> ok (venv, Tunit)
   | Null _ -> ok (venv, Tnull)
   | Bool _ -> ok (venv, Tbool)
   | Number _ -> ok (venv, Tnumber)
-  | String _ -> ok (venv, Tstring)
+  | String (_, s) -> ok (venv, Tstring s)
   | Ident (pos, varname) -> translate_ident venv pos varname
   | Array (_pos, elems) -> translate_array venv elems
   | ParsedObject (pos, entries) -> translate_object venv pos entries
Enter fullscreen mode Exit fullscreen mode

This, of course, led to many changes down the line. The semantics didn't change — I only had to fix the pattern-matching. Example:

@@ -466,7 +528,7 @@ and translate_object_field_access venv pos scope chain_exprs =
       | Number (pos, _) ->
         (* Handle numeric indexing of strings and arrays *)
         (match prev_ty with
-        | Tstring -> ok (venv, Tstring)
+        | Tstring _ as ty -> ok (venv, ty)
         | Tarray elem_ty -> ok (venv, elem_ty)
         | _ -> Error.error_at pos (Error.Msg.type_non_indexable_type (to_string prev_ty))
         )
Enter fullscreen mode Exit fullscreen mode

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:

(* Semantic equality for types. *)
let semantic_type_equal ty_a ty_b =
  match ty_a, ty_b with
  (* Ignores representation details that do not affect type identity,
     such as the concrete value carried by Tstring.  *)
  | Tstring _, Tstring _ -> true
  (* Falls back to structural equality otherwise. *)
  | _ -> ty_a = ty_b
Enter fullscreen mode Exit fullscreen mode

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:

@@ -359,22 +381,49 @@ and translate_object venv pos entries =
         let* (venv', _) = translate venv expr in (ok venv')
       | ObjectField (attr, expr) ->
         ok (Env.add_obj_field attr (Lazy expr) obj_id venv)
+      | ObjectConditionalField (attr_expr, expr) ->
+        (match check_expr_for_cycles venv attr_expr [] with
+        | Ok () -> ()
+        | Error _ ->
+          let attr_pos = match attr_expr with If (p, _, _, _) -> p | _ -> pos in
+          Error.warn Error.Msg.type_cyclic_conditional_field_key attr_pos
+        );
+        (* It's past attr_expr, which means it has no cyclic refs.
+           Now we need to add the expr to the attr name it resolves to. *)
+        let* (_, ty) = translate venv attr_expr in
+        (match ty with
+        | Tstring attr -> ok (Env.add_obj_field attr (Lazy expr) obj_id venv)
+        | Tnull -> ok venv
+        | _ ->
+          let attr_pos = match attr_expr with If (p, _, _, _) -> p | _ -> pos in
+          Error.error_at attr_pos
+            (Error.Msg.invalid_conditional_field_key (to_string ty))
+        )
     )
     (ok venv)
     entries
   in
Enter fullscreen mode Exit fullscreen mode

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:

   (* Check for cyclical references among object fields
-    (warn, don't error when the reference is not part of
-    the evaluation tree)
-  *)
+    (warn, don't error when the reference is not part of the evaluation tree) *)
   List.iter
     (fun entry ->
       match entry with
       | ObjectField (attr, _) ->
         (match check_cyclic_refs venv (Env.uniq_field_ident obj_id attr) [] pos with
         | Ok () -> ()
-        | Error _ -> Error.warn (Error.Msg.type_cyclic_reference (Env.uniq_field_ident obj_id attr)) pos)
+        | Error _ -> Error.warn (Error.Msg.type_cyclic_reference (Env.uniq_field_ident obj_id attr)) pos
+        )
+      | ObjectConditionalField (attr_expr, _) ->
+        (* attr_expr must be translated first to discover its name, then we can check_cyclic_refs *)
+        (match translate venv attr_expr with
+        | Ok (_, Tstring attr) ->
+          (match check_cyclic_refs venv (Env.uniq_field_ident obj_id attr) [] pos with
+          | Ok () -> ()
+          | Error _ -> Error.warn (Error.Msg.type_cyclic_reference (Env.uniq_field_ident obj_id attr)) pos
+          )
+        | _ -> ()
+        )
       | _ -> ()
     )
     entries;
Enter fullscreen mode Exit fullscreen mode

And the last step in translate_object is to translate each field:

@@ -382,16 +431,23 @@ and translate_object venv pos entries =
   (* Translate object fields lazily: warn on errors, skip invalid fields *)
   let entry_types = List.fold_left
     (fun entries' entry ->
-      match entry with
-      | ObjectField (attr, _) ->
-        (match Env.get_obj_field attr obj_id venv
-          ~succ:translate_lazy
-          ~err:(Error.error_at pos)
-        with
+      (* Resolve the field from the env to reuse memoized lazy translations
+        instead of retyping the field expression. *)
+      let get_field_type_from_env attr =
+        let obj_field = Env.get_obj_field attr obj_id venv ~succ:translate_lazy ~err:(Error.error_at pos) in
+        (match obj_field with
         | Ok (_, entry_ty) -> entries' @ [TobjectField (attr, entry_ty)]
-        | Error _ -> entries')
-      | _ ->
-        entries'
+        | Error _ -> entries'
+        )
+      in
+      match entry with
+      | ObjectField (attr, _) -> get_field_type_from_env attr
+      | ObjectConditionalField (attr_expr, _) ->
+        (match translate venv attr_expr with
+        | Ok (_, Tstring attr) -> get_field_type_from_env attr
+        | _ -> entries'
+        )
+      | _ -> entries'
     )
     []
     entries
Enter fullscreen mode Exit fullscreen mode

The cycle check above requires new code to deal with ObjectConditionalField:

@@ -100,6 +109,7 @@ let rec collect_free_idents = function
   | ParsedObject (_, entries) ->
     List.concat_map (function
       | ObjectField (_, e) -> collect_free_idents e
+      | ObjectConditionalField (field, e) -> List.append (collect_free_idents field) (collect_free_idents e)
       | ObjectExpr e -> collect_free_idents e
     ) entries
   | ObjectFieldAccess (_, scope, exprs) ->
@@ -152,6 +162,7 @@ and check_expr_for_cycles venv expr seen =
   | BinOp (_, _, e1, e2) -> iter_for_cycles venv seen [e1; e2]
   | UnaryOp (_, _, e) -> check_expr_for_cycles venv e seen
   | Seq exprs -> iter_for_cycles venv seen exprs
+  | If (_, cond_expr, then_expr, else_expr_opt) -> check_conditional_for_cycles venv (cond_expr, then_expr, else_expr_opt) seen
   | _ -> ok ()
 and iter_for_cycles venv seen exprs =
   List.fold_left
Enter fullscreen mode Exit fullscreen mode

Where we check each expr for cycles:

and check_conditional_for_cycles venv (cond_expr, then_expr, else_expr_opt) seen =
  let* () = check_expr_for_cycles venv cond_expr seen in
  let* () = check_expr_for_cycles venv then_expr seen in
  (match else_expr_opt with
  | Some else_expr -> check_expr_for_cycles venv else_expr seen
  | None -> ok ()
  )
Enter fullscreen mode Exit fullscreen mode
@@ -163,6 +174,9 @@ and check_object_for_cycles venv entries seen =
     (fun ok entry -> ok >>= fun _ ->
       match entry with
       | ObjectField (field, expr) -> check_expr_for_cycles venv expr (field :: seen)
+      | ObjectConditionalField (field, expr) ->
+        check_expr_for_cycles venv field seen >>= fun () ->
+        check_expr_for_cycles venv expr seen
       | ObjectExpr expr -> check_expr_for_cycles venv expr seen
     )
     (ok ())
Enter fullscreen mode Exit fullscreen mode

And to finish it off the type checking phase, we check if the two branch types are semantically equal:

@@ -714,13 +785,13 @@ and translate_conditional venv (pos, cond_expr, then_expr, else_expr_opt) =
     (match else_expr_opt with
     | Some else_expr ->
       let* (_, else_type) = translate venv else_expr in
-      if then_type = else_type
+      if semantic_type_equal then_type else_type
       then ok (venv, then_type)
-      else Error.error_at pos
-        (Error.Msg.type_conditional_branches_mismatch
-          ~then_type:(to_string then_type)
-          ~else_type:(to_string else_type)
-        )
+        else Error.error_at pos
+          (Error.Msg.type_conditional_branches_mismatch
+            ~then_type:(to_string then_type)
+            ~else_type:(to_string else_type)
+          )
     | None ->
       ok (venv, then_type)
     )
Enter fullscreen mode Exit fullscreen mode

And with that, we also add more error keys to the error module:

diff --git a/lib/error.ml b/lib/error.ml
index b1ae567..422456a 100644
--- a/lib/error.ml
+++ b/lib/error.ml
@@ -14,6 +14,8 @@ module Msg = struct
   let invalid_binary_op = "Invalid binary operation"
   let invalid_unary_op = "Invalid unary operation"
   let must_be_object = "Must be an object"
+  let invalid_conditional_field_key ty =
+    Printf.sprintf "Conditional field key must be String or Null, got %s" ty

   (* Parser messages *)
   let parse_error = "Parsing error. Invalid syntax:"
@@ -21,6 +23,7 @@ module Msg = struct

   (* Type checker messages *)
   let type_cyclic_reference varname = "Cyclic reference found for " ^ varname
+  let type_cyclic_conditional_field_key = "Cyclic reference found in conditional field key"
   let type_unused_variable varname = "Unused variable " ^ varname
   let type_non_indexable_value ty = ty ^ " is a non indexable value"
   let type_expected_integer_index ty = "Expected Integer index, got " ^ ty
diff --git a/lib/error.mli b/lib/error.mli
index 28cd496..ec9d177 100644
--- a/lib/error.mli
+++ b/lib/error.mli
@@ -11,6 +11,7 @@ module Msg : sig
   val invalid_binary_op : string
   val invalid_unary_op : string
   val must_be_object : string
+  val invalid_conditional_field_key : string -> string

   (* Parser messages *)
   val parse_error : string
@@ -18,6 +19,7 @@ module Msg : sig

   (* Type checker messages *)
   val type_cyclic_reference : string -> string
+  val type_cyclic_conditional_field_key : string
   val type_unused_variable : string -> string
   val type_non_indexable_value : string -> string
   val type_expected_integer_index : string -> string
Enter fullscreen mode Exit fullscreen mode

Evaluating

The evaluation part is straightforward, after the type checker did the heavy lifting:

diff --git a/lib/interpreter.ml b/lib/interpreter.ml
index 235b90e..40b9eea 100644
--- a/lib/interpreter.ml
+++ b/lib/interpreter.ml
@@ -137,16 +137,29 @@ and interpret_object env (pos, entries) =
   let* (obj_env, fields) = List.fold_left
     (fun result entry ->
       let* (env', fields) = result in
+      let add_field name expr obj_id env =
+        (* Object fields are kept lazy -- they will be evaluated only when accessed.
+           This prevents infinite loops from circular references. *)
+        let env' = Env.add_obj_field name expr obj_id env in
+        ok (env', ObjectFields.add name fields)
+      in
       match entry with
       | ObjectExpr expr ->
         (* ObjectExpr holds a single local. Interpreting
           it will add the expr to the environment *)
         let* (env', _) = interpret env' expr in ok (env', fields)
       | ObjectField (name, expr) ->
-        (* Object fields are kept lazy -- they will be evaluated only when accessed.
-           This prevents infinite loops from circular references. *)
-        let env' = Env.add_obj_field name expr obj_id env' in
-        ok (env', ObjectFields.add name fields)
+        add_field name expr obj_id env'
+      | ObjectConditionalField (field_expr, expr) ->
+        let* (_, ident) = interpret env field_expr in
+        (match ident with
+        | Null _ -> ok (env', fields) (* null attribute names are ignored *)
+        | String (_, name) -> add_field name expr obj_id env'
+        | _ ->
+          let field_pos = match field_expr with If (p, _, _, _) -> p | _ -> pos in
+          Error.error_at field_pos
+            (Error.Msg.invalid_conditional_field_key (string_of_type ident))
+        )
     )
     (ok (obj_env, ObjectFields.empty))
     entries
Enter fullscreen mode Exit fullscreen mode

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",

Enter fullscreen mode Exit fullscreen mode

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,
   ^^^^^^^^^^^^^^
Enter fullscreen mode Exit fullscreen mode

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 manifestation
Enter fullscreen mode Exit fullscreen mode

Tsonnet 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,
   ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^

Enter fullscreen mode Exit fullscreen mode

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
1
Enter fullscreen mode Exit fullscreen mode

But 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,
               ^^
---
1
Enter fullscreen mode Exit fullscreen mode

This 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 }
+      ]
+    }
+  }
Enter fullscreen mode Exit fullscreen mode

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.


Thanks for reading Bit Maybe Wise! Subscribe — and the condition for getting the next post is, conveniently, always true.

Photo by Jens Lelie on Unsplash

Top comments (0)