Skip to content

Conversation

@johnbartholomew
Copy link
Collaborator

The Go Jsonnet implementation already implements objectRemoveKey as a builtin and retains hidden fields, this brings the C++ Jsonnet into alignment. This implementation is a little tricky because lazy evaluation means that objects are not simple structures but form an inheritance tree. However, it's not too bad as the code is generally happy to strictly enumerate the object's field list without necessarily evaluating field values, and for std.objectRemoveKey all we need is the field list. Arguably we could even do without that by just storing the set of fields to be removed, but I'd prefer to keep things 'simpler' with a flat field list.

See google/go-jsonnet#830

@johnbartholomew johnbartholomew marked this pull request as draft January 29, 2026 14:12
@johnbartholomew
Copy link
Collaborator Author

Ok, this isn't right yet. Needs more deliberate design.

With this implementation (084d795) we get odd behaviour like this:

$ ./jsonnet -e '{a:1}+std.objectRemoveKey({b:super.a},"a")'
RUNTIME ERROR: field does not exist: a
	<cmdline>:1:30-35	object <anonymous>
	During manifestation

The Go Jsonnet implementation already implements objectRemoveKey as
a builtin and retains hidden fields, this brings the C++ Jsonnet into
alignment.

See google/go-jsonnet#830
@johnbartholomew
Copy link
Collaborator Author

The bug was caused by incorrect counting when traversing object nodes, and failing to treat the new HeapRestrictedObject as a 'leaf' object type so it wasn't properly skipped when evaluating super in the inner object.

In the expression { a: 1 } + std.objectRemoveKey({ b: super.a }, "a") the object graph looks like this:

---
config:
  look: handDrawn
  theme: neutral
---
graph TD;
  A["{ a: 1 }"]
  B["{ b: super.a }"]
  C["remove-key 'a'"]
  D["+"]
  B --> C
  A --> D
  C --> D
Loading

When placed in order, the nodes are: {a:1}, {b:super.a}, remove-key 'a' (the + node is not a leaf and doesn't participate in counting). b:super.a is defined in node B so when evaluating it it can look at any node "before it" (above it, in the inheritance hierarchy), which in this case is only node A. Node A does define a field a so that evaluation succeeds.

The final output object includes both keys a and b.

In the expression std.objectRemoveKey({ a: 1 } + { b: super.a }, "a") the object graph looks like this:

---
config:
  look: handDrawn
  theme: neutral
---
graph TD;
  A["{ a: 1 }"]
  B["{ b: super.a }"]
  C["+"]
  D["remove-key 'a'"]
  A --> C
  B --> C
  C --> D
Loading

In order, the nodes are still {a:1}, {b:super.a}, remove-key 'a' so this still evaluates the same way. But the final output object includes only key b, because a is removed in the outermost node.

@johnbartholomew johnbartholomew marked this pull request as ready for review January 29, 2026 17:33
@johnbartholomew
Copy link
Collaborator Author

You can compare this to a function that just hides a particular key rather than removing it:

local removeA(o) = o + { a:: "HIDE" };
removeA({ a:1 } + { b:super.a })

Evaluates to { b: 1 }. The inner super.a still evaluates to 1 because it looks "up" the inheritance hierarchy, not down to the { a:: "HIDE" } node. But the final output does not include the field a because it is hidden in the right-most node.

local removeA(o) = o + { a:: "HIDE" };
removeA({ a:1 } + { b:super.a })

Also evaluates to { b: 1 }.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant