Skip to content
Merged
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion packages/cel/src/check.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ const cache = new WeakMap<CelEnv, Checker>();
export function check(env: CelEnv, expr: Expr | ParsedExpr): CheckedExpr {
let checker = cache.get(env);
if (checker === undefined) {
checker = new Checker();
checker = new Checker(env);
cache.set(env, checker);
}
if (isMessage(expr, ExprSchema)) {
Expand Down
10 changes: 0 additions & 10 deletions packages/cel/src/checker.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,19 +20,9 @@ import {
} from "./testing.js";

const filter = createExpressionFilter([
// Ident types
"is",
"ii",
"iu",
"iz",
"ib",
"id",
"ix",
"[]",
"[1]",
'[1, "A"]',

// Call resolution
"fg_s()",
"is.fi_s_s()",
"1 + 2",
Expand Down
110 changes: 109 additions & 1 deletion packages/cel/src/checker.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,18 +17,22 @@ import type {
Constant,
Expr,
SourceInfo,
Expr_Ident,
} from "@bufbuild/cel-spec/cel/expr/syntax_pb.js";
import {
type CheckedExpr,
CheckedExprSchema,
type Reference,
type Type,
type TypeSchema,
Type_PrimitiveType,
} from "@bufbuild/cel-spec/cel/expr/checked_pb.js";
import { create, type MessageInitShape } from "@bufbuild/protobuf";
import {
CelScalar,
celType,
type CelType,
type CelValue,
DURATION,
listType,
type mapKeyType,
Expand All @@ -37,17 +41,25 @@ import {
TIMESTAMP,
} from "./type.js";
import { NullValue } from "@bufbuild/protobuf/wkt";
import type { CelEnv } from "./env.js";
import { resolveCandidateNames } from "./namespace.js";
import { celError } from "./error.js";
import { isCelUint } from "./uint.js";

export class Checker {
private readonly referenceMap: Map<bigint, Reference> = new Map();
Comment thread
srikrsna-buf marked this conversation as resolved.
Outdated
private readonly typeMap: Map<bigint, CelType> = new Map();

constructor(private readonly env: CelEnv) {}

check(expr: Expr, sourceInfo: SourceInfo | undefined): CheckedExpr {
// Clear each time we check since Checker instances are cached per environment.
this.referenceMap.clear();
this.typeMap.clear();
Comment thread
srikrsna-buf marked this conversation as resolved.
return create(CheckedExprSchema, {
expr: this.checkExpr(expr),
sourceInfo,
// TODO: referenceMap
referenceMap: celReferenceMapToProtoReferenceMap(this.referenceMap),
typeMap: celTypeMapToProtoTypeMap(this.typeMap),
});
}
Expand All @@ -56,6 +68,8 @@ export class Checker {
switch (expr.exprKind.case) {
case "constExpr":
return this.checkConstExpr(expr.id, expr.exprKind.value);
case "identExpr":
return this.checkIdentExpr(expr.id, expr.exprKind.value);
default:
throw new Error(`Unsupported expression kind: ${expr.exprKind.case}`);
}
Expand Down Expand Up @@ -107,9 +121,49 @@ export class Checker {
};
}

private checkIdentExpr(
id: bigint,
ident: Expr_Ident,
): MessageInitShape<typeof ExprSchema> {
const found = this.resolveSimpleVariable(ident.name);
if (found) {
this.setType(id, found);
this.setReference(id, identReference(ident.name));
return {
id,
exprKind: {
case: "identExpr",
value: ident,
},
};
}
throw celError(
`undeclared reference to '${ident.name}' (in container '${this.env.namespace}')`,
id,
);
}

private setType(id: bigint, type: CelType): void {
this.typeMap.set(id, type);
}

private setReference(id: bigint, reference: Reference): void {
this.referenceMap.set(id, reference);
}

private resolveSimpleVariable(name: string): CelType | undefined {
const ident = this.env.variables.findLocal(name);
if (ident) {
return ident;
}
for (const candidate of resolveCandidateNames(this.env.namespace, name)) {
const ident = this.env.variables.find(candidate);
if (ident) {
return ident;
}
}
return undefined;
}
}

export function protoTypeToCelType(pt: Type): CelType {
Expand Down Expand Up @@ -272,3 +326,57 @@ function celTypeMapToProtoTypeMap(
}
return protoTypeMap;
}

function identReference(name: string, value?: CelValue): Reference {
Comment thread
srikrsna-buf marked this conversation as resolved.
Outdated
return {
$typeName: "cel.expr.Reference",
name,
value: value ? celValueToProtoConstant(value) : undefined,
overloadId: [],
};
}

function protoConstant<
T extends Exclude<Constant["constantKind"]["case"], undefined>,
>(
caseName: T,
value: Extract<Constant["constantKind"], { case: T }>["value"],
): Constant {
return {
$typeName: "cel.expr.Constant",
constantKind: { case: caseName, value } as Constant["constantKind"],
};
}

function celValueToProtoConstant(value: CelValue): Constant {
switch (typeof value) {
case "bigint":
return protoConstant("int64Value", value);
case "number":
return protoConstant("doubleValue", value);
case "boolean":
return protoConstant("boolValue", value);
case "string":
return protoConstant("stringValue", value);
case "object":
switch (true) {
case isCelUint(value):
return protoConstant("uint64Value", value.value);
case null:
return protoConstant("nullValue", NullValue.NULL_VALUE);
case value instanceof Uint8Array:
return protoConstant("bytesValue", value);
}
}
throw new Error(`unsupported constant type: ${celType(value)}`);
}

function celReferenceMapToProtoReferenceMap(
referenceMap: Map<bigint, Reference>,
): Record<string, Reference> {
const protoReferenceMap: Record<string, Reference> = {};
for (const [id, ref] of referenceMap.entries()) {
protoReferenceMap[id.toString()] = ref;
}
return protoReferenceMap;
}
37 changes: 37 additions & 0 deletions packages/cel/src/scope.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -64,4 +64,41 @@ void suite("scope", () => {
assert.equal(popped, undefined);
});
});
void suite("find() flavors", () => {
void test("find()", () => {
const root = createScope({ a: CelScalar.INT });
const child = root.push({ b: CelScalar.STRING });
const grandChild = child.push({ c: CelScalar.BOOL });
assert.equal(grandChild.find("a"), CelScalar.INT);
assert.equal(grandChild.find("b"), CelScalar.STRING);
assert.equal(grandChild.find("c"), CelScalar.BOOL);
});
void test("findInScope()", () => {
const root = createScope({ a: CelScalar.INT });
const child = root.push({ b: CelScalar.STRING });
const grandChild = child.push({ c: CelScalar.BOOL });
assert.equal(child.findInScope("a"), undefined);
assert.equal(child.findInScope("b"), CelScalar.STRING);
assert.equal(child.findInScope("c"), undefined);
assert.equal(grandChild.findInScope("a"), undefined);
assert.equal(grandChild.findInScope("b"), undefined);
assert.equal(grandChild.findInScope("c"), CelScalar.BOOL);
});
void test("findLocal()", () => {
const root = createScope({ a: CelScalar.INT });
const child = root.push({ b: CelScalar.STRING });
const grandChild = child.push({ c: CelScalar.BOOL });
assert.equal(grandChild.findLocal("a"), undefined);
assert.equal(grandChild.findLocal("b"), CelScalar.STRING);
assert.equal(grandChild.findLocal("c"), CelScalar.BOOL);
});
void test("findGlobal()", () => {
const root = createScope({ a: CelScalar.INT });
const child = root.push({ b: CelScalar.STRING });
const grandChild = child.push({ c: CelScalar.BOOL });
assert.equal(grandChild.findGlobal("a"), CelScalar.INT);
assert.equal(grandChild.findGlobal("b"), undefined);
assert.equal(grandChild.findGlobal("c"), undefined);
});
});
});
45 changes: 45 additions & 0 deletions packages/cel/src/scope.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,10 @@ export interface VariableScope<Vars extends VariableDecl = VariableDecl> {
* All the variables in the scope.
*/
[Symbol.iterator](): IterableIterator<[string, CelType]>;
/**
* The parent VariableScope for the current scope or undefined.
*/
parent: VariableScope | undefined;
/**
* Creates a new VariableScope with the current scope as the parent and the
* provided inputs as the new scope's variables.
Expand All @@ -44,6 +48,18 @@ export interface VariableScope<Vars extends VariableDecl = VariableDecl> {
* Finds the variable by name in the current scope or any parent scopes.
*/
find(name: string): CelType | undefined;
/**
* Finds a variable with a given name in the current scope only.
*/
findInScope(name: string): CelType | undefined;
/**
* Finds a locally scoped variable with a given name, ignoring the root scope.
*/
findLocal(name: string): CelType | undefined;
/**
* Finds a globally scoped variable with a given name, only searching the root scope.
*/
findGlobal(name: string): CelType | undefined;
}

/**
Expand Down Expand Up @@ -83,6 +99,10 @@ class Scope<Vars extends VariableDecl = VariableDecl>
}
}

get parent(): VariableScope | undefined {
return this._parent;
}

push<PushVars extends VariableDecl = VariableDecl>(
inputs: PushVars,
): VariableScope<PushVars & Vars> {
Expand All @@ -99,4 +119,29 @@ class Scope<Vars extends VariableDecl = VariableDecl>
}
return this._parent?.find(name);
}

findInScope(name: string): CelType | undefined {
return this._variables.get(name);
}

findLocal(name: string): CelType | undefined {
if (!this._parent) {
return undefined;
}
if (this._variables.has(name)) {
return this._variables.get(name);
}
return this._parent.findLocal(name);
}

findGlobal(name: string): CelType | undefined {
if (!this._parent) {
return this._variables.get(name);
}
let parent = this._parent;
while (parent.parent) {
parent = parent.parent;
}
return parent.findInScope(name);
}
}