Skip to content

Commit

Permalink
Two-variable comprehension support (#1034)
Browse files Browse the repository at this point in the history
Two-variable comprehensions with support for transformMapEntry
  • Loading branch information
TristonianJones authored Oct 8, 2024
1 parent 0213a8e commit 3142950
Show file tree
Hide file tree
Showing 8 changed files with 983 additions and 26 deletions.
29 changes: 22 additions & 7 deletions checker/checker.go
Original file line number Diff line number Diff line change
Expand Up @@ -496,16 +496,32 @@ func (c *checker) checkComprehension(e ast.Expr) {
comp := e.AsComprehension()
c.check(comp.IterRange())
c.check(comp.AccuInit())
accuType := c.getType(comp.AccuInit())
rangeType := substitute(c.mappings, c.getType(comp.IterRange()), false)
var varType *types.Type

// Create a scope for the comprehension since it has a local accumulation variable.
// This scope will contain the accumulation variable used to compute the result.
accuType := c.getType(comp.AccuInit())
c.env = c.env.enterScope()
c.env.AddIdents(decls.NewVariable(comp.AccuVar(), accuType))

var varType, var2Type *types.Type
switch rangeType.Kind() {
case types.ListKind:
// varType represents the list element type for one-variable comprehensions.
varType = rangeType.Parameters()[0]
if comp.HasIterVar2() {
// varType represents the list index (int) for two-variable comprehensions,
// and var2Type represents the list element type.
var2Type = varType
varType = types.IntType
}
case types.MapKind:
// Ranges over the keys.
// varType represents the map entry key for all comprehension types.
varType = rangeType.Parameters()[0]
if comp.HasIterVar2() {
// var2Type represents the map entry value for two-variable comprehensions.
var2Type = rangeType.Parameters()[1]
}
case types.DynKind, types.ErrorKind, types.TypeParamKind:
// Set the range type to DYN to prevent assignment to a potentially incorrect type
// at a later point in type-checking. The isAssignable call will update the type
Expand All @@ -518,13 +534,12 @@ func (c *checker) checkComprehension(e ast.Expr) {
varType = types.ErrorType
}

// Create a scope for the comprehension since it has a local accumulation variable.
// This scope will contain the accumulation variable used to compute the result.
c.env = c.env.enterScope()
c.env.AddIdents(decls.NewVariable(comp.AccuVar(), accuType))
// Create a block scope for the loop.
c.env = c.env.enterScope()
c.env.AddIdents(decls.NewVariable(comp.IterVar(), varType))
if comp.HasIterVar2() {
c.env.AddIdents(decls.NewVariable(comp.IterVar2(), var2Type))
}
// Check the variable references in the condition and step.
c.check(comp.LoopCondition())
c.assertType(comp.LoopCondition(), types.BoolType)
Expand Down
139 changes: 135 additions & 4 deletions ext/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,12 @@
CEL extensions are a related set of constants, functions, macros, or other
features which may not be covered by the core CEL spec.

## Bindings
## Bindings

Returns a cel.EnvOption to configure support for local variable bindings
in expressions.

# Cel.Bind
### Cel.Bind

Binds a simple identifier to an initialization expression which may be used
in a subsequenct result expression. Bindings may also be nested within each
Expand All @@ -19,11 +19,11 @@ other.
Examples:

cel.bind(a, 'hello',
cel.bind(b, 'world', a + b + b + a)) // "helloworldworldhello"
cel.bind(b, 'world', a + b + b + a)) // "helloworldworldhello"

// Avoid a list allocation within the exists comprehension.
cel.bind(valid_values, [a, b, c],
[d, e, f].exists(elem, elem in valid_values))
[d, e, f].exists(elem, elem in valid_values))

Local bindings are not guaranteed to be evaluated before use.

Expand Down Expand Up @@ -684,3 +684,134 @@ Examples:

'gums'.reverse() // returns 'smug'
'John Smith'.reverse() // returns 'htimS nhoJ'

## TwoVarComprehensions

TwoVarComprehensions introduces support for two-variable comprehensions.

The two-variable form of comprehensions looks similar to the one-variable
counterparts. Where possible, the same macro names were used and additional
macro signatures added. The notable distinction for two-variable comprehensions
is the introduction of `transformList`, `transformMap`, and `transformMapEntry`
support for list and map types rather than the more traditional `map` and
`filter` macros.

### All

Comprehension which tests whether all elements in the list or map satisfy a
given predicate. The `all` macro evaluates in a manner consistent with logical
AND and will short-circuit when encountering a `false` value.

<list>.all(indexVar, valueVar, <predicate>) -> bool
<map>.all(keyVar, valueVar, <predicate>) -> bool

Examples:

[1, 2, 3].all(i, j, i < j) // returns true
{'hello': 'world', 'taco': 'taco'}.all(k, v, k != v) // returns false

// Combines two-variable comprehension with single variable
{'h': ['hello', 'hi'], 'j': ['joke', 'jog']}
.all(k, vals, vals.all(v, v.startsWith(k))) // returns true

### Exists

Comprehension which tests whether any element in a list or map exists which
satisfies a given predicate. The `exists` macro evaluates in a manner consistent
with logical OR and will short-circuit when encountering a `true` value.

<list>.exists(indexVar, valueVar, <predicate>) -> bool
<map>.exists(keyVar, valueVar, <predicate>) -> bool

Examples:

{'greeting': 'hello', 'farewell': 'goodbye'}
.exists(k, v, k.startsWith('good') || v.endsWith('bye')) // returns true
[1, 2, 4, 8, 16].exists(i, v, v == 1024 && i == 10) // returns false

### ExistsOne

Comprehension which tests whether exactly one element in a list or map exists
which satisfies a given predicate expression. This comprehension does not
short-circuit in keeping with the one-variable exists one macro semantics.

<list>.existsOne(indexVar, valueVar, <predicate>)
<map>.existsOne(keyVar, valueVar, <predicate>)

This macro may also be used with the `exists_one` function name, for
compatibility with the one-variable macro of the same name.

Examples:

[1, 2, 1, 3, 1, 4].existsOne(i, v, i == 1 || v == 1) // returns false
[1, 1, 2, 2, 3, 3].existsOne(i, v, i == 2 && v == 2) // returns true
{'i': 0, 'j': 1, 'k': 2}.existsOne(i, v, i == 'l' || v == 1) // returns true

### TransformList

Comprehension which converts a map or a list into a list value. The output
expression of the comprehension determines the contents of the output list.
Elements in the list may optionally be filtered according to a predicate
expression, where elements that satisfy the predicate are transformed.

<list>.transformList(indexVar, valueVar, <transform>)
<list>.transformList(indexVar, valueVar, <filter>, <transform>)
<map>.transformList(keyVar, valueVar, <transform>)
<map>.transformList(keyVar, valueVar, <filter>, <transform>)

Examples:

[1, 2, 3].transformList(indexVar, valueVar,
(indexVar * valueVar) + valueVar) // returns [1, 4, 9]
[1, 2, 3].transformList(indexVar, valueVar, indexVar % 2 == 0
(indexVar * valueVar) + valueVar) // returns [1, 9]
{'greeting': 'hello', 'farewell': 'goodbye'}
.transformList(k, _, k) // returns ['greeting', 'farewell']
{'greeting': 'hello', 'farewell': 'goodbye'}
.transformList(_, v, v) // returns ['hello', 'goodbye']

### TransformMap

Comprehension which converts a map or a list into a map value. The output
expression of the comprehension determines the value of the output map entry;
however, the key remains fixed. Elements in the map may optionally be filtered
according to a predicate expression, where elements that satisfy the predicate
are transformed.

<list>.transformMap(indexVar, valueVar, <transform>)
<list>.transformMap(indexVar, valueVar, <filter>, <transform>)
<map>.transformMap(keyVar, valueVar, <transform>)
<map>.transformMap(keyVar, valueVar, <filter>, <transform>)

Examples:

[1, 2, 3].transformMap(indexVar, valueVar,
(indexVar * valueVar) + valueVar) // returns {0: 1, 1: 4, 2: 9}
[1, 2, 3].transformMap(indexVar, valueVar, indexVar % 2 == 0
(indexVar * valueVar) + valueVar) // returns {0: 1, 2: 9}
{'greeting': 'hello'}.transformMap(k, v, v + '!') // returns {'greeting': 'hello!'}

### TransformMapEntry

Comprehension which converts a map or a list into a map value; however, this
transform expects the entry expression be a map literal. If the transform
produces an entry which duplicates a key in the target map, the comprehension
will error.

Elements in the map may optionally be filtered according to a predicate
expression, where elements that satisfy the predicate are transformed.

<list>.transformMap(indexVar, valueVar, <transform>)
<list>.transformMap(indexVar, valueVar, <filter>, <transform>)
<map>.transformMap(keyVar, valueVar, <transform>)
<map>.transformMap(keyVar, valueVar, <filter>, <transform>)

Examples:

// returns {'hello': 'greeting'}
{'greeting': 'hello'}.transformMapEntry(keyVar, valueVar, {valueVar: keyVar})
// reverse lookup, require all values in list be unique
[1, 2, 3].transformMapEntry(indexVar, valueVar, {valueVar: indexVar})

{'greeting': 'aloha', 'farewell': 'aloha'}
.transformMapEntry(keyVar, valueVar, {valueVar: keyVar}) // error, duplicate key
Loading

0 comments on commit 3142950

Please sign in to comment.