diff --git a/doc/langdef.md b/doc/langdef.md index b60ca34..efa677c 100644 --- a/doc/langdef.md +++ b/doc/langdef.md @@ -184,6 +184,21 @@ name which resolves in the current lexical scope is used. For example, if possible field selection, then `a.b.c` takes priority over the interpretation `(a.b).c`. +Note: Comprehensions (.exists, .all, etc) introduce new scopes that add +variables as simple identifiers. When resolving a name within a comprehension +body, the name is first compared against the variables declared by the +comprehension. If there is a match, the name resolves to that variable, taking +precedence over the package-based resolution rules above. If not, +resolution proceeds checking for variable matches in parent +comprehension scopes recursively. Finally the name follows the package +resolution rules above against declarations in the environment. A name with a +leading '.' always resolves in the root scope, bypassing local scopes from +comprehensions. + +For example, in `[1].exists(x, x == 1)`, `x` is a local variable which shadows +any identifier named 'x' in ancestor scopes or the package namespace. In +`[1].exists(x, .x == 1)`, x is the global variable `x`. + ## Values Values in CEL represent any of the following: diff --git a/tests/simple/testdata/bindings_ext.textproto b/tests/simple/testdata/bindings_ext.textproto index 3a3d93f..62bf65c 100644 --- a/tests/simple/testdata/bindings_ext.textproto +++ b/tests/simple/testdata/bindings_ext.textproto @@ -40,4 +40,63 @@ section: { bool_value: true } } + test: { + name: "shadowing" + expr: "cel.bind(x, 0, x == 0)" + type_env: { + name: "x" + ident: { + type: { primitive: INT64 } + } + } + bindings: { + key: "x" + value: { + value: { int64_value: 1 } + } + } + value: { + bool_value: true + } + } + test: { + name: "shadowing_namespace_resolution" + expr: "cel.bind(x, 0, x == 0)" + container: "com.example" + type_env: { + name: "com.example.x" + ident: { + type: { primitive: INT64 } + } + } + bindings: { + key: "com.example.x" + value: { + value: { int64_value: 1 } + } + } + value: { + bool_value: true + } + } + test: { + name: "shadowing_namespace_resolution_selector" + expr: "cel.bind(x, {'y': 0}, x.y == 0)" + container: "com.example" + type_env: { + name: "com.example.x.y" + ident: { + type: { primitive: INT64 } + } + } + bindings: { + key: "com.example.x.y" + value: { + value: { int64_value: 1 } + } + } + value: { + bool_value: true + } + } } diff --git a/tests/simple/testdata/namespace.textproto b/tests/simple/testdata/namespace.textproto index 15cc3ac..4cacd1a 100644 --- a/tests/simple/testdata/namespace.textproto +++ b/tests/simple/testdata/namespace.textproto @@ -11,7 +11,7 @@ section { expr: "x.y" value: { bool_value: true } type_env: { - name: "x.y", + name: "x.y" ident: { type: { primitive: BOOL } } } bindings: { @@ -28,11 +28,11 @@ section { expr: "y" container: "x" type_env: { - name: "x.y", + name: "x.y" ident: { type: { primitive: BOOL } } } type_env: { - name: "y", + name: "y" ident: { type: { primitive: STRING } } } bindings: { @@ -50,11 +50,11 @@ section { expr: "y" container: "x" type_env: { - name: "x.y", + name: "x.y" ident: { type: { primitive: BOOL } } } type_env: { - name: "y", + name: "y" ident: { type: { primitive: BOOL } } } bindings: { @@ -65,7 +65,198 @@ section { key: "y" value: { value: { bool_value: false } } } - disable_check: true ## ensure unchecked ASTs resolve the same as checked ASTs + disable_check: true ## ensure unchecked ASTs resolve the same as checked ASTs + value: { bool_value: true } + } +} +section { + name: "namespace_shadowing" + description: "Variable shadowing in comprehensions" + test { + name: "basic" + expr: "y" + container: "com.example" + type_env: { + name: "com.example.y" + ident: { type: { primitive: BOOL } } + } + type_env: { + name: "y" + ident: { type: { primitive: STRING } } + } + bindings: { + key: "com.example.y" + value: { value: { bool_value: true } } + } + bindings: { + key: "y" + value: { value: { string_value: "string" } } + } + value: { bool_value: true } + } + test { + name: "disambiguation" + expr: ".y" + container: "com.example" + type_env: { + name: "com.example.y" + ident: { type: { primitive: STRING } } + } + type_env: { + name: "y" + ident: { type: { primitive: STRING } } + } + bindings: { + key: "com.example.y" + value: { value: { string_value: "com.example.y" } } + } + bindings: { + key: "y" + value: { value: { string_value: "y" } } + } + value: { string_value: "y" } + } + test { + name: "comprehension_shadowing" + expr: "[0].exists(y, y == 0)" + container: "com.example" + type_env: { + name: "com.example.y" + ident: { type: { primitive: INT64 } } + } + bindings: { + key: "com.example.y" + value: { value: { int64_value: 42 } } + } + value: { bool_value: true } + } + test { + name: "comprehension_shadowing_disambiguation" + expr: "['compre'].exists(y, .y == 'y')" + container: "com.example" + type_env: { + name: "y" + ident: { type: { primitive: STRING } } + } + bindings: { + key: "y" + value: { value: { string_value: "y" } } + } + value: { bool_value: true } + } + test { + name: "comprehension_shadowing_parse_only" + expr: "[0].exists(y, y == 0)" + container: "com.example" + type_env: { + name: "com.example.y" + ident: { type: { primitive: INT64 } } + } + bindings: { + key: "com.example.y" + value: { value: { int64_value: 42 } } + } + disable_check: true + value: { bool_value: true } + } + test { + name: "comprehension_shadowing_selector" + expr: "[{'z': 0}].exists(y, y.z == 0)" + type_env: { + name: "y.z" + ident: { type: { primitive: INT64 } } + } + bindings: { + key: "y.z" + value: { value: { int64_value: 42 } } + } + value: { bool_value: true } + } + test { + name: "comprehension_shadowing_selector_parse_only" + expr: "[{'z': 0}].exists(y, y.z == 0)" + type_env: { + name: "y.z" + ident: { type: { primitive: INT64 } } + } + bindings: { + key: "y.z" + value: { value: { int64_value: 42 } } + } + disable_check: true + value: { bool_value: true } + } + test { + name: "comprehension_shadowing_namespaced_selector" + expr: "[{'z': 0}].exists(y, y.z == 0)" + container: "com.example" + type_env: { + name: "com.example.y.z" + ident: { type: { primitive: INT64 } } + } + bindings: { + key: "com.example.y.z" + value: { value: { int64_value: 42 } } + } + value: { bool_value: true } + } + test { + name: "comprehension_shadowing_namespaced_selector_parse_only" + expr: "[{'z': 0}].exists(y, y.z == 0)" + container: "com.example" + type_env: { + name: "com.example.y.z" + ident: { type: { primitive: INT64 } } + } + bindings: { + key: "com.example.y.z" + value: { value: { int64_value: 42 } } + } + disable_check: true + value: { bool_value: true } + } + test { + name: "comprehension_shadowing_namespaced_selector_disambiguation" + expr: "[{'z': 'compre'}].exists(y, .y.z == 'y.z')" + container: "com.example" + type_env: { + name: "com.example.y.z" + ident: { type: { primitive: STRING } } + } + type_env: { + name: "y.z" + ident: { type: { primitive: STRING } } + } + bindings: { + key: "com.example.y.z" + value: { value: { string_value: "com.example.y.z" } } + } + bindings: { + key: "y.z" + value: { value: { string_value: "y.z" } } + } + value: { bool_value: true } + } + test { + name: "comprehension_shadowing_nesting" + expr: "[1].exists(y, [0].exists(y, y == 0))" + container: "com.example" + type_env: { + name: "com.example.y" + ident: { type: { primitive: INT64 } } + } + type_env: { + name: "y" + ident: { type: { primitive: INT64 } } + } + bindings: { + key: "com.example.y" + value: { value: { int64_value: 42 } } + } + bindings: { + key: "y" + value: { value: { int64_value: 42 } } + } value: { bool_value: true } } }