Description
We have encountered a subtle and relatively specific type-unification bug in the CEL compiler (specifically when compiling CEL policies).
When a list of strings is unwrapped from a list of optional strings (list(optional(string))) using standard comprehensions (filter/map), and then concatenated with an untyped empty list [] defined as a variable, the CEL type checker incorrectly propagates the list(optional(string)) constraint down the addition chain.
This causes the compiler to incorrectly infer the empty list variable as list(optional(string)) instead of list(string), resulting in a compilation failure:
found no matching overload for '_+_' applied to '(list(string), list(optional_type(string)))'
This bug is highly sensitive to variable boundaries and ONLY triggers when all steps are declared as separate variables in the policy. Inlining any of these steps acts as a workaround and allows the compiler to succeed.
To Reproduce
Check which components this affects:
Minimal Reproducible Example (MRE)
name: "mre"
rule:
variables:
# type: list(dyn) - Initially untyped empty list
- name: "empty_list"
expression: |
[]
# type: list(optional(string)) - Source list of optional strings
- name: "list_optional_string"
expression: |
[optional.of("foo")]
# type: list(string) - Unwrapped string list using standard comprehensions
- name: "string_list"
expression: |
variables.list_optional_string.filter(u, u.hasValue()).map(u, u.value())
# Expected type: list(string)
# ERROR TRIGGERS HERE:
# "found no matching overload for '_+_' applied to '(list(string), list(optional_type(string)))'"
- name: "concatenated_list"
expression: |
variables.string_list +
variables.empty_list
match:
- output: |
variables.concatenated_list
When compiling the above MRE, we get the following error message:
Compile(mre_policy.celpolicy) failed:
ERROR: mre_policy.celpolicy:24:31: found no matching overload for '_+_' applied to (list(string), list(optional_type(string)))
| variables.string_list +
| ..............................^
This bug seems strictly tied to how the compiler manages type constraints across variable boundaries. It only triggers when the compilation steps are split across separate variables:
- Empty list must be a variable:
The empty list [] must be defined as a variable (empty_list = []).
- Inlining it directly into the addition chain (e.g.,
variables.string_list + []) compiles successfully.
- Source optional list must be a variable:
The source optional list list_optional_string must be defined as a variable.
- Inlining it directly into the comprehension expression (e.g.,
[optional.of("foo")].filter(...)) compiles successfully.
- Unwrapped list must be a variable:
The mapped string list string_list must be defined as a variable.
- Inlining it directly into the final addition (e.g.,
variables.list_optional_string.filter(...).map(...) + variables.empty_list) compiles successfully.
If you inline any of these three variables, the type checker correctly unifies the types. Only when they are declared as separate variables does the compiler incorrectly "leak" the type of the original optional list (list_optional_string) to the empty list variable in the addition step, thinking we are trying to add list(string) and list(optional(string)).
Description
We have encountered a subtle and relatively specific type-unification bug in the CEL compiler (specifically when compiling CEL policies).
When a list of strings is unwrapped from a list of optional strings (
list(optional(string))) using standard comprehensions (filter/map), and then concatenated with an untyped empty list[]defined as a variable, the CEL type checker incorrectly propagates thelist(optional(string))constraint down the addition chain.This causes the compiler to incorrectly infer the empty list variable as
list(optional(string))instead oflist(string), resulting in a compilation failure:This bug is highly sensitive to variable boundaries and ONLY triggers when all steps are declared as separate variables in the policy. Inlining any of these steps acts as a workaround and allows the compiler to succeed.
To Reproduce
Check which components this affects:
Minimal Reproducible Example (MRE)
When compiling the above MRE, we get the following error message:
This bug seems strictly tied to how the compiler manages type constraints across variable boundaries. It only triggers when the compilation steps are split across separate variables:
The empty list
[]must be defined as a variable (empty_list = []).variables.string_list + []) compiles successfully.The source optional list
list_optional_stringmust be defined as a variable.[optional.of("foo")].filter(...)) compiles successfully.The mapped string list
string_listmust be defined as a variable.variables.list_optional_string.filter(...).map(...) + variables.empty_list) compiles successfully.If you inline any of these three variables, the type checker correctly unifies the types. Only when they are declared as separate variables does the compiler incorrectly "leak" the type of the original optional list (
list_optional_string) to the empty list variable in the addition step, thinking we are trying to addlist(string)andlist(optional(string)).