@@ -17,18 +17,23 @@ import type {
1717 Constant ,
1818 Expr ,
1919 SourceInfo ,
20+ Expr_Ident ,
21+ ConstantSchema ,
2022} from "@bufbuild/cel-spec/cel/expr/syntax_pb.js" ;
2123import {
2224 type CheckedExpr ,
2325 CheckedExprSchema ,
26+ type ReferenceSchema ,
2427 type Type ,
2528 type TypeSchema ,
2629 Type_PrimitiveType ,
2730} from "@bufbuild/cel-spec/cel/expr/checked_pb.js" ;
2831import { create , type MessageInitShape } from "@bufbuild/protobuf" ;
2932import {
3033 CelScalar ,
34+ celType ,
3135 type CelType ,
36+ type CelValue ,
3237 DURATION ,
3338 listType ,
3439 type mapKeyType ,
@@ -37,17 +42,33 @@ import {
3742 TIMESTAMP ,
3843} from "./type.js" ;
3944import { NullValue } from "@bufbuild/protobuf/wkt" ;
45+ import type { CelEnv } from "./env.js" ;
46+ import { resolveCandidateNames } from "./namespace.js" ;
47+ import { celError } from "./error.js" ;
48+ import { isCelUint } from "./uint.js" ;
49+ import { createScope } from "./scope.js" ;
50+
51+ const noopScope = createScope ( ) ;
4052
4153export class Checker {
54+ private readonly referenceMap : Map <
55+ bigint ,
56+ MessageInitShape < typeof ReferenceSchema >
57+ > = new Map ( ) ;
4258 private readonly typeMap : Map < bigint , CelType > = new Map ( ) ;
59+ private scope = noopScope ;
60+
61+ constructor ( private readonly env : CelEnv ) { }
4362
4463 check ( expr : Expr , sourceInfo : SourceInfo | undefined ) : CheckedExpr {
4564 // Clear each time we check since Checker instances are cached per environment.
4665 this . typeMap . clear ( ) ;
66+ this . scope = noopScope ;
67+ this . referenceMap . clear ( ) ;
4768 return create ( CheckedExprSchema , {
4869 expr : this . checkExpr ( expr ) ,
4970 sourceInfo,
50- // TODO: referenceMap
71+ referenceMap : celReferenceMapToProtoReferenceMap ( this . referenceMap ) ,
5172 typeMap : celTypeMapToProtoTypeMap ( this . typeMap ) ,
5273 } ) ;
5374 }
@@ -56,6 +77,8 @@ export class Checker {
5677 switch ( expr . exprKind . case ) {
5778 case "constExpr" :
5879 return this . checkConstExpr ( expr . id , expr . exprKind . value ) ;
80+ case "identExpr" :
81+ return this . checkIdentExpr ( expr . id , expr . exprKind . value ) ;
5982 default :
6083 throw new Error ( `Unsupported expression kind: ${ expr . exprKind . case } ` ) ;
6184 }
@@ -107,9 +130,75 @@ export class Checker {
107130 } ;
108131 }
109132
133+ private checkIdentExpr (
134+ id : bigint ,
135+ ident : Expr_Ident ,
136+ ) : MessageInitShape < typeof ExprSchema > {
137+ const variable = this . resolveVariable ( ident . name ) ;
138+ if ( variable === undefined ) {
139+ throw celError (
140+ `undeclared reference to '${ ident . name } ' (in container '${ this . env . namespace } ')` ,
141+ id ,
142+ ) ;
143+ }
144+ this . setType ( id , variable . type ) ;
145+ this . setReference ( id , identReference ( variable . name ) ) ;
146+ return {
147+ id,
148+ exprKind : {
149+ case : "identExpr" ,
150+ value : {
151+ name : variable . name ,
152+ } ,
153+ } ,
154+ } ;
155+ }
156+
110157 private setType ( id : bigint , type : CelType ) : void {
111158 this . typeMap . set ( id , type ) ;
112159 }
160+
161+ private setReference (
162+ id : bigint ,
163+ reference : MessageInitShape < typeof ReferenceSchema > ,
164+ ) : void {
165+ this . referenceMap . set ( id , reference ) ;
166+ }
167+
168+ /**
169+ * Resolves a variable according to the CEL name resolution rules.
170+ *
171+ * See https://github.com/google/cel-spec/blob/master/doc/langdef.md#name-resolution
172+ */
173+ private resolveVariable ( name : string ) :
174+ | {
175+ name : string ;
176+ type : CelType ;
177+ }
178+ | undefined {
179+ // First we check for the variable to be in
180+ // the comprehension scope chain if it is not global.
181+ if ( ! name . startsWith ( "." ) ) {
182+ const type = this . scope . find ( name ) ;
183+ if ( type !== undefined ) {
184+ return {
185+ type,
186+ name,
187+ } ;
188+ }
189+ }
190+ // It can be a global because either it is fully qualified or missing in comprehension scope.
191+ for ( const candidate of resolveCandidateNames ( this . env . namespace , name ) ) {
192+ const type = this . env . variables . find ( candidate ) ;
193+ if ( type ) {
194+ return {
195+ type,
196+ name : candidate , // This is an optimization that allows us to partially skip name resolution during eval.
197+ } ;
198+ }
199+ }
200+ return undefined ;
201+ }
113202}
114203
115204export function protoTypeToCelType ( pt : Type ) : CelType {
@@ -272,3 +361,62 @@ function celTypeMapToProtoTypeMap(
272361 }
273362 return protoTypeMap ;
274363}
364+
365+ function identReference (
366+ name : string ,
367+ value ?: CelValue ,
368+ ) : MessageInitShape < typeof ReferenceSchema > {
369+ return {
370+ name,
371+ value : value ? celValueToProtoConstant ( value ) : undefined ,
372+ } ;
373+ }
374+
375+ function protoConstant <
376+ T extends Exclude < Constant [ "constantKind" ] [ "case" ] , undefined > ,
377+ > (
378+ caseName : T ,
379+ value : Extract < Constant [ "constantKind" ] , { case : T } > [ "value" ] ,
380+ ) : MessageInitShape < typeof ConstantSchema > {
381+ return {
382+ constantKind : { case : caseName , value } as Constant [ "constantKind" ] ,
383+ } ;
384+ }
385+
386+ function celValueToProtoConstant (
387+ value : CelValue ,
388+ ) : MessageInitShape < typeof ConstantSchema > {
389+ switch ( typeof value ) {
390+ case "bigint" :
391+ return protoConstant ( "int64Value" , value ) ;
392+ case "number" :
393+ return protoConstant ( "doubleValue" , value ) ;
394+ case "boolean" :
395+ return protoConstant ( "boolValue" , value ) ;
396+ case "string" :
397+ return protoConstant ( "stringValue" , value ) ;
398+ case "object" :
399+ switch ( true ) {
400+ case isCelUint ( value ) :
401+ return protoConstant ( "uint64Value" , value . value ) ;
402+ case null :
403+ return protoConstant ( "nullValue" , NullValue . NULL_VALUE ) ;
404+ case value instanceof Uint8Array :
405+ return protoConstant ( "bytesValue" , value ) ;
406+ }
407+ }
408+ throw new Error ( `unsupported constant type: ${ celType ( value ) } ` ) ;
409+ }
410+
411+ function celReferenceMapToProtoReferenceMap (
412+ referenceMap : Map < bigint , MessageInitShape < typeof ReferenceSchema > > ,
413+ ) : Record < string , MessageInitShape < typeof ReferenceSchema > > {
414+ const protoReferenceMap : Record <
415+ string ,
416+ MessageInitShape < typeof ReferenceSchema >
417+ > = { } ;
418+ for ( const [ id , ref ] of referenceMap . entries ( ) ) {
419+ protoReferenceMap [ id . toString ( ) ] = ref ;
420+ }
421+ return protoReferenceMap ;
422+ }
0 commit comments