DEV Community

Cover image for Tsonnet #34 - Dabbling with untouched bindings
Hercules Lemke Merscher
Hercules Lemke Merscher

Posted on • Originally published at bitmaybewise.substack.com

Tsonnet #34 - Dabbling with untouched bindings

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 wrapped up the arithmetic tutorial:

This time, before proceeding to cover the next tutorial, I'm going to revisit one laziness aspect: the evaluation (or no-evaluation) of untouched bindings.

Warnings, not errors

Up until now, Tsonnet would hard-error on cyclic references regardless of whether the offending variable was ever touched. That's a bit heavy-handed. Jsonnet is lazily evaluated -- if a variable has a cycle but is never used, the program should still run. We should warn, not panic. For example:

// samples/variables/untouched_variable.jsonnet
local a = 1, b = 42;
b
Enter fullscreen mode Exit fullscreen mode

Jsonnet behaves like this:

$ jsonnet samples/variables/untouched_variable.jsonnet
42
Enter fullscreen mode Exit fullscreen mode

Jsonnet does nothing to alert the programmer that something is unused. Maybe that belongs to a linter, but the compiler not even warning is a bad experience, IMHO.

The same logic applies to unused variables: they're suspicious, but they shouldn't crash anything.

// samples/objects/untouched_field.jsonnet
local result = {
    a: 1,
    b: 42,
};
result.b
Enter fullscreen mode Exit fullscreen mode
$ jsonnet samples/objects/untouched_field.jsonnet
42
Enter fullscreen mode Exit fullscreen mode

Let's work through each of these in order.

Warn on cyclic refs for unused variables

The first thing we need is a way to tell which variables are actually reachable from the body of a local expression. For that, I added collect_free_idents and reachable_bindings to the type checker:

let rec collect_free_idents = function
  | Unit | Null _ | Number _ | String _ | Bool _ -> []
  | Ident (_, name) -> [name]
  | Array (_, exprs) -> List.concat_map collect_free_idents exprs
  | BinOp (_, _, e1, e2) -> collect_free_idents e1 @ collect_free_idents e2
  | UnaryOp (_, _, e) -> collect_free_idents e
  | Seq exprs -> List.concat_map collect_free_idents exprs
  | ParsedObject (_, entries) ->
    List.concat_map (function
      | ObjectField (_, e) -> collect_free_idents e
      | ObjectExpr e -> collect_free_idents e
    ) entries
  | ObjectFieldAccess (_, _, exprs) -> 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
  | _ -> []

let reachable_bindings bindings initial_idents =
  let rec go visited = function
    | [] -> visited
    | name :: rest ->
      if List.mem name visited
      then go visited rest
      else
        let new_idents =
          match List.assoc_opt name bindings with
          | Some expr -> collect_free_idents expr
          | None -> []
        in
        go (name :: visited) (new_idents @ rest)
  in
  go [] initial_idents
Enter fullscreen mode Exit fullscreen mode

collect_free_idents walks an expression and collects every identifier referenced. reachable_bindings does a simple graph traversal starting from the identifiers used in the body, following variable references transitively. If a binding is never reachable from the body, it's unused.

I'm delegating part of what used to be Local-processing into the translate_seq function, where the logic actually runs, to keep things tidy:

diff --git a/lib/type.ml b/lib/type.ml
index fbe68b1..2f4bf41 100644
--- a/lib/type.ml
+++ b/lib/type.ml
@@ -154,23 +187,15 @@ let rec translate venv expr =
     )
   | ParsedObject (pos, entries) -> translate_object venv pos entries
   | ObjectFieldAccess (pos, scope, chain) -> translate_object_field_access venv pos scope chain
-  | Local (pos, vars) ->
+  | Local (_pos, vars) ->
     let venv' = List.fold_left
       (* Adds an expr to the env to be evaluated at a later point in time (when required) *)
       (fun venv (varname, var_expr) -> Env.add_local varname (Lazy var_expr) venv)
       venv
       vars
-    in
-    let* _ = List.fold_left
-      (fun ok' (varname, _) -> ok' >>= fun _ -> check_cyclic_refs venv' varname [] pos)
-      (ok ())
-      vars
     in ok (venv', Tunit)
   | Seq exprs ->
-    List.fold_left
-      (fun acc expr -> acc >>= fun (venv, _) -> translate venv expr)
-      (ok (venv, Tunit))
-      exprs
+    translate_seq venv exprs
   | BinOp (pos, op, e1, e2) ->
     translate_bin_op venv pos op e1 e2
   | UnaryOp (pos, op, expr) ->
Enter fullscreen mode Exit fullscreen mode
and translate_seq venv exprs =
  let rec collect_locals = function
    | Local (pos, vars) :: rest ->
      let (all_vars, body) = collect_locals rest in
      (List.map (fun v -> (pos, v)) vars @ all_vars, body)
    | rest -> ([], rest)
  in
  let rec go venv = function
    | [] -> ok (venv, Tunit)
    | [expr] -> translate venv expr
    | (Local _ :: _) as exprs ->
      let (all_pos_vars, body) = collect_locals exprs in
      let all_vars = List.map snd all_pos_vars in
      let venv' = List.fold_left
        (fun venv (varname, var_expr) ->
          Env.add_local varname (Lazy var_expr) venv
        )
        venv
        all_vars
      in
      let body_idents = List.concat_map collect_free_idents body in
      let reachable = reachable_bindings all_vars body_idents in
      let* () = List.fold_left
        (fun acc (pos, (varname, _)) -> acc >>= fun () ->
          match check_cyclic_refs venv' varname [] pos with
          | Ok () -> ok ()
          | Error msg ->
            if List.mem varname reachable
            then error msg
            else (Error.warn (Error.Msg.type_cyclic_reference varname) pos; ok ())
        )
        (ok ())
        all_pos_vars
      in
      go venv' body
    | expr :: rest ->
      let* (venv', _) = translate venv expr in
      go venv' rest
  in
  go venv exprs
Enter fullscreen mode Exit fullscreen mode

The key part: if a cyclic reference involves a variable that's reachable from the body, we still error. If it's unreachable, we just warn and move on. Here's what it looks like in practice:

$ dune exec -- tsonnet samples/variables/untouched_invalid_variable.jsonnet
Warning: .../untouched_invalid_variable.jsonnet:1:31 Cyclic reference found for c

1: local a = 1, b = a, c = d, d = c;
   ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
Warning: .../untouched_invalid_variable.jsonnet:1:24 Cyclic reference found for d

1: local a = 1, b = a, c = d, d = c;
   ^^^^^^^^^^^^^^^^^^^^^^^^^^
1
Enter fullscreen mode Exit fullscreen mode

c and d have a cycle between them, but b is what the program actually evaluates. So we warn, and produce the result.

Warn on unused variables

With reachability analysis in place, unused variable warnings follow naturally. A variable is unused if it's not in the reachable set. Adding that to translate_seq:

diff --git a/lib/type.ml b/lib/type.ml
index 2f4bf41..0e7a680 100644
--- a/lib/type.ml
+++ b/lib/type.ml
@@ -247,6 +247,11 @@ and translate_seq venv exprs =
       (* Determine which vars are reachable from the body *)
       let body_idents = List.concat_map collect_free_idents body in
       let reachable = reachable_bindings all_vars body_idents in
+      (* Warn on unused variables *)
+      List.iter (fun (pos, (varname, _)) ->
+        if not (List.mem varname reachable)
+        then Error.warn (Error.Msg.type_unused_variable varname) pos
+      ) all_pos_vars;
       (* Check cycles: error for reachable, warn for unreachable *)
       let* () = List.fold_left
         (fun acc (pos, (varname, _)) -> acc >>= fun () ->
@@ -255,7 +260,7 @@ and translate_seq venv exprs =
           | Error msg ->
             if List.mem varname reachable
             then error msg
-            else (prerr_endline ("Warning: " ^ msg); ok ())
+            else (Error.warn (Error.Msg.type_cyclic_reference varname) pos; ok ())
         )
         (ok ())
         all_pos_vars
Enter fullscreen mode Exit fullscreen mode

I also refactored the warning infrastructure a bit. Instead of calling prerr_endline inline, there's now a proper Error.warn function:

diff --git a/lib/error.ml b/lib/error.ml
index ac8ae48..508ae0c 100644
--- a/lib/error.ml
+++ b/lib/error.ml
@@ -21,6 +21,7 @@ module Msg = struct

   (* Type checker messages *)
   let type_cyclic_reference varname = "Cyclic reference found for " ^ varname
+  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
   let type_invalid_expr expr = "Invalid type " ^ expr
@@ -104,3 +105,8 @@ let trace (err: string) (pos: position) : (string, string) result =
     (fun content -> ok (Printf.sprintf "%s\n%s" (trace_file_position err pos) content))

 let error_at pos = fun msg -> trace msg pos >>= error
+
+let warn msg pos =
+  match trace msg pos with
+  | Ok formatted -> prerr_endline ("Warning: " ^ formatted)
+  | Error _ -> prerr_endline ("Warning: " ^ msg)
diff --git a/lib/error.mli b/lib/error.mli
index 0bdd708..c09b930 100644
--- a/lib/error.mli
+++ b/lib/error.mli
@@ -18,6 +18,7 @@ module Msg : sig

   (* Type checker messages *)
   val type_cyclic_reference : string -> string
+  val type_unused_variable : string -> string
   val type_non_indexable_value : string -> string
   val type_expected_integer_index : string -> string
   val type_invalid_expr : string -> string
@@ -37,3 +38,4 @@ end

 val trace : string -> Ast.position -> (string, string) result
 val error_at : Ast.position -> string -> ('a, string) result
+val warn : string -> Ast.position -> unit
Enter fullscreen mode Exit fullscreen mode

Much cleaner. Let's see it in action:

$ dune exec -- tsonnet samples/variables/untouched_variable.jsonnet
Warning: .../untouched_variable.jsonnet:1:0 Unused variable a

1: local a = 1, b = 42;
   ^^^^^^^^^^^^^^^^^^^^
42
Enter fullscreen mode Exit fullscreen mode

a is defined but never used. The program still returns 42.

Warn on untouched object fields

Variables were the easy part. Objects are more involved because we're dealing with two separate phases -- the type checker and the interpreter -- and both need to handle cyclic field references gracefully.

The approach is the same: if a cyclic object field is never accessed during evaluation, warn instead of error. The tricky bit is detecting "accessed during evaluation" correctly.

Both modules now track which fields are actively being evaluated using a mutable ObjectFields set:

(* In interpreter.ml *)
let evaluating_fields = ref ObjectFields.empty

(* In type.ml *)
let translating_fields = ref ObjectFields.empty
Enter fullscreen mode Exit fullscreen mode

When we start evaluating a field, we add it to the set. When we're done (or on error), we remove it. If we try to evaluate a field already in the set -- cycle detected:

diff --git a/lib/interpreter.ml b/lib/interpreter.ml
index fe248b9..391c1e2 100644
--- a/lib/interpreter.ml
+++ b/lib/interpreter.ml
@@ -21,9 +23,17 @@ let rec interpret env expr =
   | ObjectPtr _ as obj_ptr -> ok (env, obj_ptr)
   | ObjectFieldAccess (pos, scope, chain) -> interpret_object_field_access env (pos, scope, chain)
   | Ident (pos, varname) ->
-    Env.find_var varname env
-      ~succ:(fun env' expr -> interpret env' expr)
-      ~err:(Error.error_at pos)
+    if ObjectFields.mem varname !evaluating_fields then
+      Error.error_at pos (Error.Msg.type_cyclic_reference varname)
+    else begin
+      evaluating_fields := ObjectFields.add varname !evaluating_fields;
+      let result = Env.find_var varname env
+        ~succ:(fun env' expr -> interpret env' expr)
+        ~err:(Error.error_at pos)
+      in
+      evaluating_fields := ObjectFields.remove varname !evaluating_fields;
+      result
+    end
   | BinOp (pos, op, e1, e2) -> interpret_bin_op env (pos, op, e1, e2)
   | UnaryOp (pos, op, expr) ->
     let* (env', expr') = interpret env expr in
@@ -201,9 +211,18 @@ and interpret_object_field_access env (pos, scope, chain_exprs) =
       match field_expr with
       | String (pos, field) | Ident (pos, field) ->
         let* (obj_id, field_env) = get_obj_id in
-        Env.get_obj_field field obj_id field_env
-          ~succ:(interpret)
-          ~err:(Error.error_at pos)
+        let key = Env.uniq_field_ident obj_id field in
+        if ObjectFields.mem key !evaluating_fields then
+          Error.error_at pos (Error.Msg.type_cyclic_reference key)
+        else begin
+          evaluating_fields := ObjectFields.add key !evaluating_fields;
+          let result = Env.get_obj_field field obj_id field_env
+            ~succ:(interpret)
+            ~err:(Error.error_at pos)
+          in
+          evaluating_fields := ObjectFields.remove key !evaluating_fields;
+          result
+        end
       | Number _ as index_expr ->
         (* Handle array/string indexing: prev_expr[number] *)
         Result.fold
@@ -223,19 +242,27 @@ and interpret_runtime_object env (pos, obj_env, fields) =
 and interpret_runtime_object_fields obj_env fields =
   match Env.Map.find_opt "self" obj_env with
   | Some (ObjectPtr (obj_id, _)) ->
-    let* field_list =
+    let field_list =
       ObjectFields.fold
         (fun field acc ->
-          let* evaluated_fields = acc in
           let key = Env.uniq_field_ident obj_id field in
-          match Env.Map.find_opt key obj_env with
-          | Some expr ->
-            let* (_, evaluated) = interpret obj_env expr in
-            ok ((field, evaluated) :: evaluated_fields)
-          | None -> acc
+          if ObjectFields.mem key !evaluating_fields then
+            acc (* Skip: cyclic reference detected *)
+          else
+            match Env.Map.find_opt key obj_env with
+            | Some expr ->
+              evaluating_fields := ObjectFields.add key !evaluating_fields;
+              let result =
+                match interpret obj_env expr with
+                | Ok (_, evaluated) -> (field, evaluated) :: acc
+                | Error _ -> acc
+              in
+              evaluating_fields := ObjectFields.remove key !evaluating_fields;
+              result
+            | None -> acc
         )
         fields
-        (ok [])
+        []
     in ok (List.rev field_list)
   | _ -> ok []
Enter fullscreen mode Exit fullscreen mode

For object fields that are never accessed, the type checker now warns instead of erroring. translate_object was changed to iterate over entries and emit warnings rather than propagate errors:

diff --git a/lib/type.ml b/lib/type.ml
index 0e7a680..8b41545 100644
--- a/lib/type.ml
+++ b/lib/type.ml
@@ -152,13 +154,21 @@ let rec translate venv expr =
   | Number _ -> ok (venv, Tnumber)
   | String _ -> ok (venv, Tstring)
   | Ident (pos, varname) ->
-    Env.find_var varname venv
-      ~succ:(fun venv ty ->
-        match ty with
-        | Lazy expr -> translate venv expr
-        | _ -> ok (venv, ty)
-      )
-      ~err:(Error.error_at pos)
+    if ObjectFields.mem varname !translating_fields then
+      Error.error_at pos (Error.Msg.type_cyclic_reference varname)
+    else begin
+      translating_fields := ObjectFields.add varname !translating_fields;
+      let result = Env.find_var varname venv
+        ~succ:(fun venv ty ->
+          match ty with
+          | Lazy expr -> translate venv expr
+          | _ -> ok (venv, ty)
+        )
+        ~err:(Error.error_at pos)
+      in
+      translating_fields := ObjectFields.remove varname !translating_fields;
+      result
+    end
   | Array (_pos, elems) ->
     (* As of now, we compare each element and if all have the same type,
       it is an array of this type, otherwise it will be an array of any.
@@ -298,32 +308,36 @@ and translate_object venv pos entries =
     entries
   in

-  (* Check for cyclical references among object fields *)
-  let* () = List.fold_left
-      (fun ok' entry -> ok' >>= fun _ ->
-        match entry with
-        | ObjectField (attr, _) ->
-          check_cyclic_refs venv (Env.uniq_field_ident obj_id attr) [] pos
-        | _ -> ok'
-      )
-      (ok ())
-      entries
-  in
+  (* Check for cyclical references among object fields
+    (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)
+      | _ -> ()
+    )
+    entries;

-  (* Then translate object fields *)
-  let* entry_types = List.fold_left
-    (fun result entry ->
-      let* entries' = result in
+  (* Translate object fields lazily: warn on errors, skip invalid fields *)
+  let entry_types = List.fold_left
+    (fun entries' entry ->
       match entry with
       | ObjectField (attr, _) ->
-        let* (_, entry_ty) = Env.get_obj_field attr obj_id venv
+        (match Env.get_obj_field attr obj_id venv
           ~succ:translate_lazy
           ~err:(Error.error_at pos)
-        in ok (entries' @ [TobjectField (attr, entry_ty)])
+        with
+        | Ok (_, entry_ty) -> entries' @ [TobjectField (attr, entry_ty)]
+        | Error _ -> entries')
       | _ ->
-        result
+        entries'
     )
-    (ok [])
+    []
     entries
   in
   (* Remove self and $ from the environment to prevent leaking *)
@@ -372,9 +386,18 @@ and translate_object_field_access venv pos scope chain_exprs =
       match field_expr with
       | String (_, field) | Ident (_, field) ->
         let* obj_id = get_obj_id in
-        Env.get_obj_field field obj_id venv
-          ~succ:translate_lazy
-          ~err:(Error.error_at pos)
+        let key = Env.uniq_field_ident obj_id field in
+        if ObjectFields.mem key !translating_fields then
+          Error.error_at pos (Error.Msg.type_cyclic_reference key)
+        else begin
+          translating_fields := ObjectFields.add key !translating_fields;
+          let result = Env.get_obj_field field obj_id venv
+            ~succ:translate_lazy
+            ~err:(Error.error_at pos)
+          in
+          translating_fields := ObjectFields.remove key !translating_fields;
+          result
+        end
       | Number (pos, _) ->
         (* Handle numeric indexing of strings and arrays *)
         (match prev_ty with
@@ -429,4 +452,5 @@ let check (config : Config.t) expr  =
   else
     let* _ = translate Env.empty expr in
     Env.Id.reset ();
+    translating_fields := ObjectFields.empty;
     ok expr
Enter fullscreen mode Exit fullscreen mode

Here's a sample that demonstrates the distinction. When we only access result.b, the cyclic c/d pair just produces warnings:

// samples/objects/untouched_invalid_field.jsonnet
local result = {
    a: 1,
    b: self.a,
    c: self.d,
    d: self.c
};
result.b
Enter fullscreen mode Exit fullscreen mode
$ dune exec -- tsonnet samples/objects/untouched_invalid_field.jsonnet
Warning: .../untouched_invalid_field.jsonnet:1:0 Unused variable result

...

Warning: .../untouched_invalid_field.jsonnet:1:15 Cyclic reference found for 1->c

...

Warning: .../untouched_invalid_field.jsonnet:1:15 Cyclic reference found for 1->d

...
1
Enter fullscreen mode Exit fullscreen mode

Meanwhile, if a cyclic field is actually accessed at runtime, it still errors hard:

// samples/semantics/invalid_object_with_cyclic_field.jsonnet
{
    a: 1,
    b: self.c,
    c: self.b,
}
Enter fullscreen mode Exit fullscreen mode
$ dune exec -- tsonnet samples/semantics/invalid_object_with_cyclic_field.jsonnet
Warning: .../invalid_object_with_cyclic_field.jsonnet:1:0 Cyclic reference found for 1->b
...
.../invalid_object_with_cyclic_field.jsonnet:3:12 Cyclic reference found for 1->c
...
[1]
Enter fullscreen mode Exit fullscreen mode

The type checker warns (because it doesn't know at type-check time which fields will be accessed), but the interpreter finds the cycle at runtime and errors properly.

Warn on cyclic refs for untouched object fields

The previous changes got the type checker side right, but the interpreter was still getting it wrong. When rendering an object, interpret_runtime_object_fields was folding into a plain list and silently skipping any field that errored -- including cyclic ones. So if you did access a cyclic field at runtime, you'd get an empty result instead of an error. Not great.

The fix is straightforward: go back to a monadic fold and let errors propagate normally. The cycle detection in interpret_object_field_access already handles the "is this field in a cycle?" question via evaluating_fields -- interpret_runtime_object_fields doesn't need to second-guess it.

diff --git a/lib/interpreter.ml b/lib/interpreter.ml
index 391c1e2..60f8a72 100644
--- a/lib/interpreter.ml
+++ b/lib/interpreter.ml
@@ -242,27 +242,19 @@ and interpret_runtime_object env (pos, obj_env, fields) =
 and interpret_runtime_object_fields obj_env fields =
   match Env.Map.find_opt "self" obj_env with
   | Some (ObjectPtr (obj_id, _)) ->
-    let field_list =
+    let* field_list =
       ObjectFields.fold
         (fun field acc ->
+          let* evaluated_fields = acc in
           let key = Env.uniq_field_ident obj_id field in
-          if ObjectFields.mem key !evaluating_fields then
-            acc (* Skip: cyclic reference detected *)
-          else
-            match Env.Map.find_opt key obj_env with
-            | Some expr ->
-              evaluating_fields := ObjectFields.add key !evaluating_fields;
-              let result =
-                match interpret obj_env expr with
-                | Ok (_, evaluated) -> (field, evaluated) :: acc
-                | Error _ -> acc
-              in
-              evaluating_fields := ObjectFields.remove key !evaluating_fields;
-              result
-            | None -> acc
+          match Env.Map.find_opt key obj_env with
+          | Some expr ->
+            let* (_, evaluated) = interpret obj_env expr in
+            ok ((field, evaluated) :: evaluated_fields)
+          | None -> acc
         )
         fields
-        []
+        (ok [])
     in ok (List.rev field_list)
   | _ -> ok []

Enter fullscreen mode Exit fullscreen mode

There's also a small fix in the type checker's translate_object_field_access. When chaining into a TruntimeObject, the field lookup was using the outer venv -- meaning self and $ weren't in scope. The fix builds a proper field_venv before looking up the field, the same way the interpreter does it:

diff --git a/lib/type.ml b/lib/type.ml
index 8b41545..200f533 100644
--- a/lib/type.ml
+++ b/lib/type.ml
@@ -376,22 +376,31 @@ and translate_object_field_access venv pos scope chain_exprs =
     (fun acc field_expr ->
       let* (venv, prev_ty) = acc in

-      let get_obj_id =
+      let get_obj_id_and_env =
         match prev_ty with
-        | TobjectPtr (obj_id, _) -> ok obj_id
-        | TruntimeObject (obj_id, _) -> ok obj_id
+        | TobjectPtr (obj_id, _) -> ok (obj_id, venv)
+        | TruntimeObject (obj_id, _) ->
+          (* TODO: we haven't included the environment in TruntimeObject yet.
+             It must be done such as Ast.RuntimeObject *)
+          let field_venv =
+            Env.add_local "self" (TobjectPtr (obj_id, TobjectSelf)) venv
+          in
+          let field_venv =
+            Env.add_local_when_not_present "$" (TobjectPtr (obj_id, TobjectTopLevel)) field_venv |> fst
+          in
+          ok (obj_id, field_venv)
         | _ -> Error.error_at pos Error.Msg.must_be_object
       in

       match field_expr with
       | String (_, field) | Ident (_, field) ->
-        let* obj_id = get_obj_id in
+        let* (obj_id, field_venv) = get_obj_id_and_env in
         let key = Env.uniq_field_ident obj_id field in
         if ObjectFields.mem key !translating_fields then
           Error.error_at pos (Error.Msg.type_cyclic_reference key)
         else begin
           translating_fields := ObjectFields.add key !translating_fields;
-          let result = Env.get_obj_field field obj_id venv
+          let result = Env.get_obj_field field obj_id field_venv
             ~succ:translate_lazy
             ~err:(Error.error_at pos)
           in
Enter fullscreen mode Exit fullscreen mode

I sprinkled a TODO here because I don't want to do this refactoring now. XD

Everything is captured in the cram tests:

diff --git a/samples/semantics/valid_object_with_cyclic_field.jsonnet b/samples/semantics/invalid_object_with_cyclic_field.jsonnet
similarity index 100%
rename from samples/semantics/valid_object_with_cyclic_field.jsonnet
rename to samples/semantics/invalid_object_with_cyclic_field.jsonnet
diff --git a/test/cram/objects.t b/test/cram/objects.t
index b200032..e7ae092 100644
--- a/test/cram/objects.t
+++ b/test/cram/objects.t
@@ -24,7 +24,7 @@

   $ tsonnet ../../samples/objects/untouched_field.jsonnet
   Warning: ../../samples/objects/untouched_field.jsonnet:1:0 Unused variable result
-  
+
   1: local result = {
      ^^^^^^^^^^^^^^^^
   2:     a: 1,
@@ -32,3 +32,39 @@
   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 = {
+     ^^^^^^^^^^^^^^^^
+  2:     a: 1,
+
+  3:     b: self.a,
+
+  4:     c: self.d,
+
+  Warning: ../../samples/objects/untouched_invalid_field.jsonnet:1:15 Cyclic reference found for 1->d
+
+  1: local result = {
+     ^^^^^^^^^^^^^^^^
+  2:     a: 1,
+
+  3:     b: self.a,
+
+  4:     c: self.d,
+
+  1
diff --git a/test/cram/semantics.t b/test/cram/semantics.t
index 4513915..c98851d 100644
--- a/test/cram/semantics.t
+++ b/test/cram/semantics.t
@@ -52,7 +52,11 @@
      ^^^^^^^^^^^^^^^^
   4:     local c = b,
      ^^^^^^^^^^^^^^^^
-  {}
+  ../../samples/semantics/invalid_binding_cycle_object_locals.jsonnet:4:14 Cyclic reference found for b
+  
+  4:     local c = b,
+     ^^^^^^^^^^^^^^^^
+  [1]

   $ tsonnet ../../samples/semantics/invalid_binding_cycle_binop.jsonnet
   ../../samples/semantics/invalid_binding_cycle_binop.jsonnet:2:10 Cyclic reference found for a
@@ -92,7 +96,11 @@
      ^^^^^^^^^^^^^^
   3:     b: self.a,
      ^^^^^^^^^^^^^^
-  {}
+  ../../samples/semantics/invalid_binding_cycle_object_fields.jsonnet:2:12 Cyclic reference found for 1->b
+  
+  2:     a: self.b,
+     ^^^^^^^^^^^^^^
+  [1]

   $ tsonnet ../../samples/semantics/invalid_binding_cycle_object_nested_field.jsonnet
   Warning: ../../samples/semantics/invalid_binding_cycle_object_nested_field.jsonnet:1:0 Cyclic reference found for 1->a
@@ -135,7 +143,11 @@
               ^^^^^^^^^
   4:     },

-  { "a": {} }
+  ../../samples/semantics/invalid_binding_cycle_object_nested_field.jsonnet:3:17 Cyclic reference found for 1->b
+  
+  3:         value: $.b
+     ^^^^^^^^^^^^^^^^^^
+  [1]

   $ tsonnet ../../samples/semantics/invalid_binding_cycle_outer_object_fields.jsonnet
   Warning: ../../samples/semantics/invalid_binding_cycle_outer_object_fields.jsonnet:1:0 Cyclic reference found for 1->a
@@ -154,7 +166,11 @@
      ^^^^^^^^^^^
   3:     b: $.a,
      ^^^^^^^^^^^
-  {}
+  ../../samples/semantics/invalid_binding_cycle_outer_object_fields.jsonnet:2:9 Cyclic reference found for 1->b
+  
+  2:     a: $.b,
+     ^^^^^^^^^^^
+  [1]

   $ tsonnet ../../samples/semantics/invalid_binding_cycle_object_field_and_local.jsonnet
   Warning: ../../samples/semantics/invalid_binding_cycle_object_field_and_local.jsonnet:1:0 Cyclic reference found for 1->b
@@ -165,7 +181,11 @@
      ^^^^^^^^^^^^^^^^^^^^^
   3:     b: a,
      ^^^^^^^^^
-  {}
+  ../../samples/semantics/invalid_binding_cycle_object_field_and_local.jsonnet:3:7 Cyclic reference found for a
+  
+  3:     b: a,
+     ^^^^^^^^^
+  [1]

   $ tsonnet ../../samples/semantics/invalid_binding_cycle_indexed_field.jsonnet
   Warning: ../../samples/semantics/invalid_binding_cycle_indexed_field.jsonnet:1:0 Cyclic reference found for 1->arr
@@ -184,10 +204,14 @@
      ^^^^^^^^^^^^^^^^^^^^^^
   3:     first: self.arr[0]
      ^^^^^^^^^^^^^^^^^^^^^^
-  {}
+  ../../samples/semantics/invalid_binding_cycle_indexed_field.jsonnet:2:15 Cyclic reference found for 1->first
+  
+  2:     arr: [self.first],
+     ^^^^^^^^^^^^^^^^^^^^^^
+  [1]

-  $ tsonnet ../../samples/semantics/valid_object_with_cyclic_field.jsonnet
-  Warning: ../../samples/semantics/valid_object_with_cyclic_field.jsonnet:1:0 Cyclic reference found for 1->b
+  $ tsonnet ../../samples/semantics/invalid_object_with_cyclic_field.jsonnet
+  Warning: ../../samples/semantics/invalid_object_with_cyclic_field.jsonnet:1:0 Cyclic reference found for 1->b

   1: {
      ^
@@ -197,7 +221,7 @@
      ^^^^^^^^^^^^^^
   4:     c: self.b,
      ^^^^^^^^^^^^^^
-  Warning: ../../samples/semantics/valid_object_with_cyclic_field.jsonnet:1:0 Cyclic reference found for 1->c
+  Warning: ../../samples/semantics/invalid_object_with_cyclic_field.jsonnet:1:0 Cyclic reference found for 1->c

   1: {
      ^
@@ -207,7 +231,11 @@
      ^^^^^^^^^^^^^^
   4:     c: self.b,
      ^^^^^^^^^^^^^^
-  { "a": 1 }
+  ../../samples/semantics/invalid_object_with_cyclic_field.jsonnet:3:12 Cyclic reference found for 1->c
+  
+  3:     b: self.c,
+     ^^^^^^^^^^^^^^
+  [1]

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

One thing I noticed while working through the test cases: the sample that used to be called valid_object_with_cyclic_field.jsonnet is not actually valid -- it errors when the cyclic fields are accessed. Renamed it to invalid_object_with_cyclic_field.jsonnet. These things happen when you're naming files before you've implemented the feature that would tell you whether they're valid or not.

Conclusion

The reachability analysis for variables and the evaluating_fields tracking for objects both push in the same direction -- lean on lazy evaluation instead of fighting it. This is the nature of Jsonnet, and Tsonnet should embrace it.

You can check the entire diff here.

I think, after that, we can start playing with much more fun things: functions!


Thanks for reading Bit Maybe Wise! If you too believe a cycle you never touch shouldn't ruin your day, subscribe and let's keep being reasonably lenient together.

Photo by Sebastian Unrau on Unsplash

Top comments (0)