From 15c999aaab63b4383dc648a7250720d845592a17 Mon Sep 17 00:00:00 2001 From: Jonathan Tatum Date: Tue, 6 Jan 2026 23:34:06 +0000 Subject: [PATCH 1/4] Add clarification on scoping rules. Add clarification on comprehension scoping and conformance tests. --- doc/langdef.md | 7 + tests/simple/testdata/bindings_ext.textproto | 59 ++++++ tests/simple/testdata/namespace.textproto | 182 ++++++++++++++++++- 3 files changed, 245 insertions(+), 3 deletions(-) diff --git a/doc/langdef.md b/doc/langdef.md index b60ca347..a048539d 100644 --- a/doc/langdef.md +++ b/doc/langdef.md @@ -184,6 +184,13 @@ 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: List comprehensions (.exists, .all, etc) introduce new scopes +that add variables as simple identifiers. When resolving a name in the body of a +comprehension, the name is compared against any declared variables for the +comprehension before resolving against parent scopes. For example, in +`[1].exists(x, x == 1)`, the interpretation that `x` is the iterator variable is +chosen over any alternative. + ## 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 3a3d93f5..62bf65c0 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 15cc3ace..1403e656 100644 --- a/tests/simple/testdata/namespace.textproto +++ b/tests/simple/testdata/namespace.textproto @@ -32,7 +32,7 @@ section { 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: { @@ -68,4 +68,180 @@ section { disable_check: true ## ensure unchecked ASTs resolve the same as checked ASTs value: { bool_value: true } } + section { + name: "namespace_shadowing" + description: "Namespaced identifiers." + 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: 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: { string_value: "string" } + } + 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_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': 0}].exists(y, .y.z == 0)" + container: "com.example" + type_env: { + name: "com.example.y.z" + ident: { type: { primitive: INT64 } } + } + type_env: { + name: "y.z" + ident: { type: { primitive: INT64 } } + } + bindings: { + key: "com.example.y.z" + value: { value: { int64_value: 42 } } + } + bindings: { + key: "y.z" + value: { value: { int64_value: 42 } } + } + 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 } + } } From d65edd60cd73cf9e217b60d24092d17b031394e4 Mon Sep 17 00:00:00 2001 From: Jonathan Tatum Date: Wed, 7 Jan 2026 00:55:36 +0000 Subject: [PATCH 2/4] Rephrase --- doc/langdef.md | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/doc/langdef.md b/doc/langdef.md index a048539d..7391d3a6 100644 --- a/doc/langdef.md +++ b/doc/langdef.md @@ -186,10 +186,11 @@ possible field selection, then `a.b.c` takes priority over the interpretation Note: List comprehensions (.exists, .all, etc) introduce new scopes that add variables as simple identifiers. When resolving a name in the body of a -comprehension, the name is compared against any declared variables for the -comprehension before resolving against parent scopes. For example, in -`[1].exists(x, x == 1)`, the interpretation that `x` is the iterator variable is -chosen over any alternative. +comprehension, the name is first compared against the variables declared by the +comprehension. If there is a match, that interpretation is chosen. If not, +the name is resolved against parent scopes until a match is found or the global +scope is searched. For example, in `[1].exists(x, x == 1)`, the interpretation +that `x` is the iterator variable is chosen over any alternative. ## Values From b8b1290790e5f776c04aacfc472479365384d831 Mon Sep 17 00:00:00 2001 From: Jonathan Tatum Date: Wed, 7 Jan 2026 01:27:45 +0000 Subject: [PATCH 3/4] Rephrase 2 --- doc/langdef.md | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/doc/langdef.md b/doc/langdef.md index 7391d3a6..78f5ce7c 100644 --- a/doc/langdef.md +++ b/doc/langdef.md @@ -187,10 +187,11 @@ possible field selection, then `a.b.c` takes priority over the interpretation Note: List comprehensions (.exists, .all, etc) introduce new scopes that add variables as simple identifiers. When resolving a name in the body of a comprehension, the name is first compared against the variables declared by the -comprehension. If there is a match, that interpretation is chosen. If not, +comprehension. If there is a match, the name resolves to that variable. If not, the name is resolved against parent scopes until a match is found or the global -scope is searched. For example, in `[1].exists(x, x == 1)`, the interpretation -that `x` is the iterator variable is chosen over any alternative. +scope is searched. For example, in `[1].exists(x, x == 1)`, `x` is a local +variable which shadows any identifier with the same simple name in ancestor +scopes. ## Values From 6bb648ccaeb761cfeab6f7db49af270aab69bc1c Mon Sep 17 00:00:00 2001 From: Jonathan Tatum Date: Wed, 7 Jan 2026 19:15:05 +0000 Subject: [PATCH 4/4] Respond to feedback. --- doc/langdef.md | 22 +++++++----- tests/simple/testdata/namespace.textproto | 43 +++++++++++++++-------- 2 files changed, 43 insertions(+), 22 deletions(-) diff --git a/doc/langdef.md b/doc/langdef.md index 78f5ce7c..efa677c8 100644 --- a/doc/langdef.md +++ b/doc/langdef.md @@ -184,14 +184,20 @@ 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: List comprehensions (.exists, .all, etc) introduce new scopes -that add variables as simple identifiers. When resolving a name in the body of a -comprehension, the name is first compared against the variables declared by the -comprehension. If there is a match, the name resolves to that variable. If not, -the name is resolved against parent scopes until a match is found or the global -scope is searched. For example, in `[1].exists(x, x == 1)`, `x` is a local -variable which shadows any identifier with the same simple name in ancestor -scopes. +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 diff --git a/tests/simple/testdata/namespace.textproto b/tests/simple/testdata/namespace.textproto index 1403e656..4cacd1a9 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,7 +28,7 @@ section { expr: "y" container: "x" type_env: { - name: "x.y", + name: "x.y" ident: { type: { primitive: BOOL } } } type_env: { @@ -65,12 +65,13 @@ 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 { +} +section { name: "namespace_shadowing" - description: "Namespaced identifiers." + description: "Variable shadowing in comprehensions" test { name: "basic" expr: "y" @@ -99,7 +100,7 @@ section { container: "com.example" type_env: { name: "com.example.y" - ident: { type: { primitive: BOOL } } + ident: { type: { primitive: STRING } } } type_env: { name: "y" @@ -107,13 +108,13 @@ section { } bindings: { key: "com.example.y" - value: { value: { bool_value: true } } + value: { value: { string_value: "com.example.y" } } } bindings: { key: "y" - value: { value: { string_value: "string" } } + value: { value: { string_value: "y" } } } - value: { string_value: "string" } + value: { string_value: "y" } } test { name: "comprehension_shadowing" @@ -129,6 +130,20 @@ section { } 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)" @@ -202,23 +217,23 @@ section { } test { name: "comprehension_shadowing_namespaced_selector_disambiguation" - expr: "[{'z': 0}].exists(y, .y.z == 0)" + expr: "[{'z': 'compre'}].exists(y, .y.z == 'y.z')" container: "com.example" type_env: { name: "com.example.y.z" - ident: { type: { primitive: INT64 } } + ident: { type: { primitive: STRING } } } type_env: { name: "y.z" - ident: { type: { primitive: INT64 } } + ident: { type: { primitive: STRING } } } bindings: { key: "com.example.y.z" - value: { value: { int64_value: 42 } } + value: { value: { string_value: "com.example.y.z" } } } bindings: { key: "y.z" - value: { value: { int64_value: 42 } } + value: { value: { string_value: "y.z" } } } value: { bool_value: true } }