Conversation
all, any, none, one, count, sum, filter, map, find, findLast and reduce previously accepted only arrays, and min, max, mean and median rejected map arguments too. That made groupBy() a dead end: it returns a map, and nothing downstream could consume it, so a pipeline like events | groupBy(.Author) | filter(len(#) > 1) could not be written. These builtins now also accept a map with string keys (including a map declared with a loose key type that holds string keys at evaluation time, such as the ones groupBy produces), and iterate its entries in ascending key order, comparing keys as strings byte by byte. Inside a predicate, # holds the entry's value and #index holds its key; #index was previously only available in map and reduce, and now becomes available in all, any, none, one, count, sum, find and findLast too. For arrays #index keeps holding the element's position, so the two collection kinds stay symmetric. filter applied to a map returns a map holding the entries whose predicate holds, and map returns a map with the same keys and transformed values, so both compose and their results keep behaving like a map downstream (indexing, len, keys, sum, the in operator). The other builtins return the same shape they return for arrays: all, any, none and one report on the values, count and sum work as they do for arrays, find and findLast return the first and last matching value in key order, and reduce folds the values in key order. An empty or nil map behaves the way an empty array does. min, max, mean and median aggregate a map's values and mix freely with array and scalar arguments. findIndex, findLastIndex, groupBy and sortBy keep rejecting maps, since positions and reordering don't translate to map entries. Maps with non-string keys keep being rejected, at compile time when the environment types make that visible, otherwise at runtime. This works whether or not the expression is compiled against a typed environment; with one, the type checker accepts the new forms and carries sensible types through them (the key type for #index, the value type for #, and a map for filter/map results). Array behavior is unchanged. Adds test/issues/575/issue_test.go covering the new behavior end to end, plus a handful of cases folded into the existing builtin and checker test tables, and documents map support in the affected builtins. Closes expr-lang#575. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What
all,any,none,one,count,sum,filter,map,find,findLastandreduceused to accept only arrays, andmin,max,meanandmedianrejected map arguments too. That madegroupBy()a dead end: it returns a map, and nothing downstream could consume it, so a pipeline likecouldn't be written — it failed at compile time with a typed environment, or with a reflection error at runtime without one.
These builtins now also accept a map with string keys (including a map declared with a loose key type that holds string keys at evaluation time, such as the ones
groupBy()produces), and iterate its entries in ascending key order, comparing keys as strings byte by byte.Semantics
#holds the entry's value and#indexholds its key.#indexwas previously only available inmapandreduce; it's now available inall,any,none,one,count,sum,findandfindLasttoo. For arrays,#indexkeeps holding the element's position, so the two collection kinds stay symmetric.filterapplied to a map returns a map holding the entries whose predicate holds;mapreturns a map with the same keys and transformed values. Both compose, and the result keeps behaving like a map downstream — indexing,len,keys,sum, theinoperator.all/any/none/onereport on the values,count/sumwork as they do for arrays,find/findLastreturn the first/last matching value in key order, andreducefolds the values in key order.min,max,meanandmedianaggregate a map's values, and a map argument mixes freely with the array and scalar arguments they already take.findIndex,findLastIndex,groupByandsortBykeep rejecting maps — positions and reordering don't translate to map entries.#index, the value type for#, and a map type forfilter/mapresults.Examples
Implementation
vm: a loopScopecan now iterate a map's sorted keys instead of an array; three new opcodes (OpPointerIndex,OpMarkKey,OpCollect) replace the array-onlyOpGetIndex/OpArraypair somap/filtercan build either an array or a map result depending on what they're iterating.checker:isIterable/indexNaturehelpers let the predicate builtins accept arrays or string-keyed maps and give#indexthe right type;filter/mapcarry a map type through when the input is a map.builtin:min/max/mean/medianunwrap a map argument into its values before aggregating (a small reflection-based helper handles bothmap[string]anyand other map value types), rejecting non-string keys.Tests
test/issues/575/issue_test.go— end-to-end coverage: filter/map over maps, all/any/none/one/count/sum, find/findLast and reduce in key order,#indexas key vs. position, empty/nil maps, nested map+array predicates, composition, thegroupBypipeline from the issue, min/max/mean/median, behavior with and without a typed environment, and that non-string-keyed maps and the positional builtins (findIndex,findLastIndex,groupBy,sortBy) still reject maps.builtin.TestBuiltin(untyped) andchecker.TestCheck_types(typed, including a type-flow check that#really is typed as the map's value type and notany).go test ./...,go test -race .,go test -tags=expr_debug -run=TestDebugger -v ./vm,gofmt -l .andgo vet ./...all pass.Closes #575.
🤖 Generated with Claude Code