Skip to content

Commit c0c9030

Browse files
l46kokcopybara-github
authored andcommitted
Update README.md to include aggregate semantics
PiperOrigin-RevId: 957326132
1 parent ba3a185 commit c0c9030

1 file changed

Lines changed: 113 additions & 37 deletions

File tree

README.md

Lines changed: 113 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -33,13 +33,19 @@ graph TD
3333
RootRule --> ID[id: string]
3434
RootRule --> Description[description: string]
3535
RootRule --> Variables[variables: List of CEL Expressions]
36-
RootRule --> Match[match: List of Matches]
36+
RootRule --> Match[match: List of First Match Choices]
37+
RootRule --> Aggregate[aggregate: List of Aggregate Choices]
3738
3839
Match --> MatchItem[Match Choice]
3940
MatchItem --> Condition[condition: CEL Bool Expression]
4041
MatchItem --> Explanation[explanation: CEL String Expression]
4142
MatchItem --> Output[output: CEL Expression]
4243
MatchItem --> SubRule[rule: RuleBlock]
44+
45+
Aggregate --> AggregateItem[Aggregate Choice]
46+
AggregateItem --> AggCondition[condition: CEL Bool Expression]
47+
AggregateItem --> Emit[emit: CEL Expression]
48+
AggregateItem --> AggSubRule[rule: RuleBlock]
4349
```
4450

4551
---
@@ -53,17 +59,17 @@ graph TD
5359
* **Lazy Scoped Variables**: Local variables are defined using CEL expressions
5460
and are evaluated on-demand (lazily) and memoized (cached) to prevent
5561
redundant computation.
56-
* **Flexible Evaluation Semantics**: Currently, rules are evaluated in a strict
57-
top-down `FIRST_MATCH` sequence (the first condition to evaluate to `true`
58-
determines the outcome, eliminating backtracking). The specification is
59-
designed to extend to other evaluation modes in the future, such as
60-
`LAST_MATCH`, `ALL_MATCH`, and `AGGREGATE` policies.
62+
* **Flexible Evaluation Semantics**: Rules support multiple evaluation
63+
strategies: top-down `FIRST_MATCH` sequence (`match`), where the first
64+
condition to evaluate to `true` determines the single outcome, and
65+
`AGGREGATE` sequence (`aggregate`), where all choices with matching
66+
conditions evaluate and accumulate their emitted values into a list.
6167
* **Strong Composition and Type Checking**: The policy compiler statically
6268
validates that all possible output paths evaluate to the **exact same type**,
6369
avoiding dynamic runtime type mismatches.
64-
* **Structured Defaults and Optionals**: If no match conditions are met,
65-
policies cleanly return `optional.none()`, allowing callers to seamlessly
66-
handle unmatched states.
70+
* **Structured Defaults and Optionals**: If no conditions are met in a `match`
71+
policy, it cleanly returns `optional.none()`, while an unmatched `aggregate`
72+
policy returns an empty list `[]`.
6773

6874
---
6975

@@ -77,8 +83,10 @@ combined and ordered according to the policy evaluation semantic.
7783

7884
A policy source document supports the following top-level keys:
7985

80-
- `name` *(string, required)*: A system-specific unique identifier for the policy.
81-
- `description` *(string, optional)*: A human-readable description of what the policy does.
86+
- `name` *(string, required)*: A system-specific unique identifier for the
87+
policy.
88+
- `description` *(string, optional)*: A human-readable description of what the
89+
policy does.
8290
- `imports` *(list[object], optional)*: A list of type name aliases to simplify
8391
object and protobuf references within the expressions.
8492
- `rule` *(object, required)*: The entry point for the policy execution.
@@ -98,7 +106,12 @@ A `rule` block supports the following fields:
98106
- `id` *(string, optional)*: A unique identifier for the rule.
99107
- `description` *(string, optional)*: A user-friendly description of the rule.
100108
- `variables` *(list, optional)*: Ordered local variable declarations.
101-
- `match` *(list, required)*: The sequential choices to evaluate.
109+
- **Evaluation Semantics**: A rule must specify exactly one evaluation mode:
110+
- `match` *(list)*: The default evaluation mode. Sequential choices
111+
evaluated using `FIRST_MATCH` semantics (evaluates top-down until a
112+
condition is met).
113+
- `aggregate` *(list)*: Choices evaluated using `AGGREGATE` semantics
114+
(evaluates all matching choices and collects emitted values into a list).
102115

103116
---
104117

@@ -132,8 +145,8 @@ within a CEL expression.
132145
### Match Choices (`match`)
133146

134147
A `match` block contains a sequence of conditional logic and outcomes evaluated
135-
in a top-down, first-match sequence. A match block must contain at least one
136-
output path.
148+
in a top-down, first-match sequence (`FIRST_MATCH`). A match block must contain
149+
at least one output path.
137150

138151
Each match item contains:
139152

@@ -148,15 +161,39 @@ Each match item contains:
148161

149162
---
150163

164+
### Aggregate Choices (`aggregate`)
165+
166+
An `aggregate` block contains a list of choices evaluated under `AGGREGATE`
167+
semantics, collecting all matched outcomes into a list. Unlike `match`,
168+
evaluation does not short-circuit at the first matching condition; all
169+
matching items evaluate and accumulate their results.
170+
171+
Each aggregate choice item contains:
172+
173+
- `condition` *(string, optional)*: A CEL expression evaluating to `bool`. If
174+
omitted, it defaults to `true`. Conditions must not evaluate to a static
175+
constant `false`.
176+
- **Outcome**: Each aggregate choice item must define exactly one of:
177+
* `emit` *(string)*: A CEL expression defining a value to append to the
178+
accumulated result list if matched.
179+
* `rule` *(object)*: A nested `rule` block (such as a nested `match` block)
180+
to evaluate further if matched.
181+
182+
> [!NOTE]
183+
> `aggregate` rules cannot be nested directly or indirectly inside another
184+
> `aggregate` rule. However, `aggregate` rules can be nested within `match`
185+
> rules, and `match` rules can be nested within `aggregate` choices.
186+
187+
---
188+
151189
### Conditions and Return Types
152190

153191
A `condition` expression must type-check to a `bool` return type. When a
154-
`condition` predicate evaluates to `true`, either the corresponding `output`
155-
expression is evaluated and returned, or the nested `rule` block is evaluated.
192+
`condition` predicate evaluates to `true`, the corresponding outcome
193+
(`output`, `emit`, or nested `rule`) is evaluated.
156194

157-
#### Optional Return Types
158-
The overall return type of a policy is dynamically determined by its evaluation
159-
completeness:
195+
#### Return Types for `match` Rules (Optional & Plain Types)
196+
For `match` rules, the return type is determined by evaluation completeness:
160197

161198
- **Exhaustive/Unconditional Return**: If the policy guarantees that a match
162199
path is always met (e.g., the final match has no `condition` or is
@@ -169,7 +206,32 @@ completeness:
169206
result in a matched output, `optional.none()` is returned as the overall
170207
policy result.
171208

172-
For more details on CEL optionals, refer to the [CEL optional proposal](https://github.com/google/cel-spec/wiki/proposal-246).
209+
For more details on CEL optionals, refer to the
210+
[CEL optional proposal](https://github.com/google/cel-spec/wiki/proposal-246).
211+
212+
#### Return Types for `aggregate` Rules (List Types)
213+
For `aggregate` rules, matching outcomes are collected into a list:
214+
215+
- **Aggregated Return**: If the emitted items in an `aggregate` rule evaluate to
216+
type `T`, the overall return type of the rule is `list(T)`
217+
(e.g., `list(string)`).
218+
- **Empty Result**: If no conditions within an `aggregate` block evaluate to
219+
`true`, the policy returns an empty list `[]`.
220+
- **Nested Optional Pruning**: If a nested sub-rule (such as a nested `match`
221+
block) under an `aggregate` choice yields `optional.none()` (because no
222+
match branch was met), that `optional.none()` is pruned (omitted) from the
223+
aggregated list.
224+
- **Nested List Values**: If an `emit` or nested `output` explicitly yields a
225+
list value `list(T)` (e.g., `emit: "['tag1', 'tag2']"`), each emitted list is
226+
appended as an element of the result list, yielding `list(list(T))`
227+
(e.g., `[['tag1', 'tag2']]`).
228+
229+
For conformance test examples, see:
230+
231+
<!-- disableFinding(LINE_OVER_80) -->
232+
- `aggregate` ([policy](conformance/testdata/aggregate/policy.yaml), [tests](conformance/testdata/aggregate/tests.yaml))
233+
- `aggregate_explicit_list_output` ([policy](conformance/testdata/aggregate_explicit_list_output/policy.yaml), [tests](conformance/testdata/aggregate_explicit_list_output/tests.yaml))
234+
- `aggregate_nested_explicit_list_double_wrapping` ([policy](conformance/testdata/aggregate_nested_explicit_list_double_wrapping/policy.yaml), [tests](conformance/testdata/aggregate_nested_explicit_list_double_wrapping/tests.yaml))
173235

174236
---
175237

@@ -288,40 +350,54 @@ repository houses a comprehensive conformance test suite.
288350
### Test Anatomy
289351
Each test category includes three key components:
290352

353+
<!-- disableFinding(LINE_OVER_80) -->
291354
| File Name | Format | Purpose |
292355
| :--- | :--- | :--- |
293-
| `config.yaml` / `config.textproto` | YAML or Protobuf | Configures the CEL environment, declares input variables (`variables`), specifies types, and registers stdlib extensions (like `strings`). See [context_pb/config.textproto](conformance/testdata/context_pb/config.textproto). |
294-
| `policy.yaml` | YAML | The actual CEL policy file being tested. See [nested_rule/policy.yaml](conformance/testdata/nested_rule/policy.yaml). |
295-
| `tests.yaml` / `tests.textproto` | YAML or Protobuf | Test cases containing input values (`input`) and the expected evaluation outcomes (`output`), or expected compilation error sets. See [nested_rule/tests.yaml](conformance/testdata/nested_rule/tests.yaml). |
356+
| `config.yaml` / `config.textproto` | YAML or Protobuf | Configures the CEL environment, declares input variables (`variables`), specifies types, and registers stdlib extensions (like `strings`). See [context_pb](conformance/testdata/context_pb/config.textproto). |
357+
| `policy.yaml` | YAML | The actual CEL policy file being tested. See [nested_rule](conformance/testdata/nested_rule/policy.yaml) and [aggregate](conformance/testdata/aggregate/policy.yaml). |
358+
| `tests.yaml` / `tests.textproto` | YAML or Protobuf | Test cases containing input values (`input`) and the expected evaluation outcomes (`output`), or expected compilation error sets. See [nested_rule](conformance/testdata/nested_rule/tests.yaml). |
296359

297360
---
298361

299362
## Static Analysis & Compilation Guarantees
300363

301364
Conforming CEL Policy compilers must implement strict compile-time static
302-
analysis. To verify this, the test suite in [compile_errors/](conformance/testdata/compile_errors) defines negative test cases that must fail compilation with appropriate error sets.
365+
analysis. To verify this, the test suite in
366+
[compile_errors/](conformance/testdata/compile_errors) defines negative test
367+
cases that must fail compilation with appropriate error sets.
303368

304369
The suite covers the following compile-time checks:
305370

306-
1. **Type Agreement (Incompatible Outputs)**: The compiler must statically
307-
verify that all possible match branches in a policy evaluate to the **exact
308-
same type**. Mixing outcome types (e.g., one branch returning `bool` and
309-
another returning `map`) is a compile-time error. See
310-
[compile_errors/compose_conflicting_output/policy.yaml](conformance/testdata/compose_conflicting_output/policy.yaml).
311-
2. **Unreachable Code (Dead Condition Detection)**: The compiler must detect and
312-
reject policies with dead-code branches. For example, if an unconditional
313-
match choice (one without a `condition` or where `condition: "true"`)
314-
precedes other choices in a block, subsequent choices are unreachable and
315-
will trigger a compilation failure. See
316-
[compile_errors/unreachable/policy.yaml](conformance/testdata/compile_errors/unreachable/policy.yaml).
371+
1. **Type Agreement (Incompatible Outputs & Emits)**: The compiler must
372+
statically verify that all possible outcome branches in a policy evaluate
373+
to the **exact same type**. Mixing outcome types in `match` outputs or
374+
`aggregate` emits (e.g., one branch emitting `string` and another emitting
375+
`int`) is a compile-time error. See
376+
[compose_conflicting_output](conformance/testdata/compile_errors/compose_conflicting_output/policy.yaml)
377+
and
378+
[aggregate_heterogeneous_outputs](conformance/testdata/compile_errors/aggregate_heterogeneous_outputs/policy.yaml).
379+
2. **Unreachable Code and Invalid Conditions**: The compiler must detect and
380+
reject policies with unreachable branches or conditions that are statically
381+
`false`.
382+
- In `match` rules, if an unconditional choice (where `condition` is omitted
383+
or `condition: "true"`) precedes other choices in a block, subsequent
384+
choices are unreachable. See
385+
[unreachable](conformance/testdata/compile_errors/unreachable/policy.yaml).
386+
- In `aggregate` rules, conditions that evaluate to a static constant
387+
`false` (e.g., `condition: "false"`) are rejected at compile time. See
388+
[aggregate_false_condition](conformance/testdata/compile_errors/aggregate_false_condition/policy.yaml).
317389
3. **Scope and Reference Validation**: The compiler must validate that all
318390
referenced variables, inputs, and imported Protobuf types are properly
319391
declared in the scope. It also ensures variable names are unique (no
320392
duplicates) and prevents forward or self-referential variable dependencies.
321393
See
322-
[compile_errors/undeclared_reference/policy.yaml](conformance/testdata/compile_errors/undeclared_reference/policy.yaml)
394+
[undeclared_reference](conformance/testdata/compile_errors/undeclared_reference/policy.yaml)
323395
and
324-
[compile_errors/duplicate_variable/policy.yaml](conformance/testdata/compile_errors/duplicate_variable/policy.yaml).
396+
[duplicate_variable](conformance/testdata/compile_errors/duplicate_variable/policy.yaml).
397+
4. **Semantics Nesting Restrictions**: `aggregate` rules cannot be nested
398+
inside another `aggregate` rule or its sub-rules
399+
(`nested aggregate rules are not allowed`). See
400+
[aggregate_nested_mixed_semantics](conformance/testdata/compile_errors/aggregate_nested_mixed_semantics/policy.yaml).
325401

326402
---
327403

0 commit comments

Comments
 (0)