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:
Tsonnet #33 - The arithmetic tutorial must go on
Hercules Lemke Merscher ・ Mar 13
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
Jsonnet behaves like this:
$ jsonnet samples/variables/untouched_variable.jsonnet
42
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
$ jsonnet samples/objects/untouched_field.jsonnet
42
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
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) ->
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
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
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
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
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
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
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 []
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
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
$ 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
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,
}
$ 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]
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 []
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
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
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)