Skip to content

Add a maps extension with maps.merge - #1409

Open
lopster568 wants to merge 1 commit into
cel-expr:masterfrom
lopster568:maps-merge-extension
Open

Add a maps extension with maps.merge#1409
lopster568 wants to merge 1 commit into
cel-expr:masterfrom
lopster568:maps-merge-extension

Conversation

@lopster568

Copy link
Copy Markdown
Contributor

Description

CEL has no way to combine two maps. The + operator concatenates strings, bytes, and lists, but has no map overload, and there is no maps extension.

This adds one, with a single function:

maps.merge(map(K, V), map(K, V)) -> map(K, V)

It returns a new map holding the entries of both arguments, with the second argument's values winning on conflicting keys. Neither input is modified. The merge is shallow, so a value that is itself a map is replaced rather than merged recursively.

maps.merge({}, {})                              // {}
maps.merge({'a': 1}, {'b': 2})                  // {'a': 1, 'b': 2}
maps.merge({'a': 1}, {'a': 2})                  // {'a': 2}
maps.merge({'a': {'x': 1}}, {'a': {'y': 2}})    // {'a': {'y': 2}}

Scope

This is deliberately only the replace step from #1240. @TristonianJones suggested starting there:

I think replace semantics is where I'd start -- last write wins, followed by set-if-absent as the deep merge is the trickiest case

So set-if-absent and recursive merge are left for follow-ups rather than guessed at here.

On the API shape, I went with a bare maps.merge(a, b) in a new maps namespace rather than a single entry point taking a strategy argument, because that is what the existing libraries do: sets.contains / sets.equivalent / sets.intersects, math.ceil / math.floor / math.abs. No function in ext takes a mode or strategy parameter today, so a later maps.mergeIfAbsent would fit that pattern without changing this signature. Happy to reshape it if you would rather have something else.

Implementation notes

  • Cost estimation and tracking scale with the combined size of both inputs plus map allocation, following ext/sets.go. Without them the checker would price this as a fixed cost of 1 despite the O(n+m) copy.
  • mapAllocCost is added to ext/costs.go alongside the existing listAllocCost.
  • Registered in ext/extension_option_factory.go (factory and alias) and in repl/evaluator.go.
  • ext/README.md gains a Maps section in the style of the surrounding entries.

Testing

ext/maps_test.go covers empty inputs, disjoint keys, conflicting keys, nested maps being replaced rather than merged, non-string keys, merging a variable, associativity over disjoint keys, compile-time rejection of non-map arguments, non-map arguments at runtime, and that the cost estimate scales with input size and brackets the tracked cost.

go build ./..., go test ./..., go vet ./ext/... and gofmt are clean.

CEL has no operator for combining two maps: + concatenates strings,
bytes, and lists but is not defined for maps, and there is no maps
extension.

Add maps.merge(a, b), returning a new map with the entries of both and
the second argument's values winning on conflicting keys. The merge is
shallow, so a value that is itself a map is replaced rather than merged.

This is the replace step of the merge semantics discussed in cel-expr#1240;
set-if-absent and recursive merge are left for follow-ups.

Includes cost estimation and tracking that scale with the combined size
of both inputs, registration in the extension option factory and the
repl, and documentation in ext/README.md.
@lopster568
lopster568 force-pushed the maps-merge-extension branch from 51aea9b to edbceda Compare August 8, 2026 03:44
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