From 42aece1fe704f27772456e142689f93340d32d80 Mon Sep 17 00:00:00 2001 From: jafaircl Date: Tue, 25 Nov 2025 17:19:01 -0500 Subject: [PATCH 1/5] initial checker implementation --- package.json | 1 + packages/cel/src/checker/checker.test.ts | 385 ++++++++++ packages/cel/src/checker/checker.ts | 902 +++++++++++++++++++++++ packages/cel/src/checker/env.ts | 343 +++++++++ packages/cel/src/checker/mapping.ts | 21 + packages/cel/src/checker/scopes.ts | 98 +++ packages/cel/src/checker/types.ts | 362 +++++++++ packages/cel/src/ext/strings/strings.ts | 24 +- packages/cel/src/func.ts | 68 +- packages/cel/src/ident.ts | 71 ++ packages/cel/src/namespace.ts | 25 + packages/cel/src/provider.ts | 199 +++++ packages/cel/src/referenceinfo.test.ts | 90 +++ packages/cel/src/referenceinfo.ts | 69 ++ packages/cel/src/std/cast.ts | 69 +- packages/cel/src/std/logic.ts | 160 ++-- packages/cel/src/std/math.ts | 46 +- packages/cel/src/std/time.ts | 86 ++- packages/cel/src/type.ts | 339 ++++++++- packages/example/src/example.ts | 6 +- 20 files changed, 3204 insertions(+), 160 deletions(-) create mode 100644 packages/cel/src/checker/checker.test.ts create mode 100644 packages/cel/src/checker/checker.ts create mode 100644 packages/cel/src/checker/env.ts create mode 100644 packages/cel/src/checker/mapping.ts create mode 100644 packages/cel/src/checker/scopes.ts create mode 100644 packages/cel/src/checker/types.ts create mode 100644 packages/cel/src/ident.ts create mode 100644 packages/cel/src/provider.ts create mode 100644 packages/cel/src/referenceinfo.test.ts create mode 100644 packages/cel/src/referenceinfo.ts diff --git a/package.json b/package.json index 2e0793a5..66a8b5ac 100644 --- a/package.json +++ b/package.json @@ -25,6 +25,7 @@ "@bufbuild/license-header": "^0.0.4", "@bufbuild/protoc-gen-es": "^2.6.2", "@types/node": "^24.1.0", + "expect-type": "^1.2.2", "tsx": "^4.20.3", "turbo": "^2.6.0", "typescript": "^5.9.2" diff --git a/packages/cel/src/checker/checker.test.ts b/packages/cel/src/checker/checker.test.ts new file mode 100644 index 00000000..0099e073 --- /dev/null +++ b/packages/cel/src/checker/checker.test.ts @@ -0,0 +1,385 @@ +import { celVariable } from "../ident.js"; +import { + type Expr, + type ParsedExpr, +} from "@bufbuild/cel-spec/cel/expr/syntax_pb.js"; +import * as assert from "node:assert/strict"; +import { suite, test } from "node:test"; +import { _CelChecker } from "./checker.js"; +import { parse } from "../parse.js"; +import { + CelScalar, + listType, + mapType, + objectType, + type CelType, +} from "../type.js"; +import { celCheckerEnv } from "./env.js"; +import { createRegistry } from "@bufbuild/protobuf"; +import { TestAllTypes_NestedMessageSchema } from "@bufbuild/cel-spec/cel/expr/conformance/proto3/test_all_types_pb.js"; +import { + AnySchema, + BoolValueSchema, + BytesValueSchema, + DoubleValueSchema, + DurationSchema, + FloatValueSchema, + Int32ValueSchema, + Int64ValueSchema, + ListValueSchema, + StringValueSchema, + StructSchema, + TimestampSchema, + UInt64ValueSchema, + ValueSchema, +} from "@bufbuild/protobuf/wkt"; +import { celFunc, celMemberOverload, celOverload } from "../func.js"; + +function internal__checkForTest(expr: Expr): CelType { + const checker = new _CelChecker( + celCheckerEnv({ + registry: createRegistry(TestAllTypes_NestedMessageSchema), + funcs: [ + celFunc("fg_s", [ + celOverload("fg_s_0", [], CelScalar.STRING, () => ""), + ]), + celFunc("fi_s_s", [ + celMemberOverload( + "fi_s_s_0", + [CelScalar.STRING], + CelScalar.STRING, + (s) => "" + ), + ]), + ], + idents: [ + celVariable("is", CelScalar.STRING), + celVariable("ii", CelScalar.INT), + celVariable("iu", CelScalar.UINT), + celVariable("iz", CelScalar.BOOL), + celVariable("ib", CelScalar.BYTES), + celVariable("id", CelScalar.DOUBLE), + celVariable("ix", CelScalar.NULL), + celVariable("b", listType(CelScalar.STRING)), + celVariable("c", mapType(CelScalar.STRING, CelScalar.BOOL)), + ], + }) + ); + checker.checkExpr(expr); + if (checker.errors.length > 0) { + throw new Error( + `type checking failed: ${checker.errors.map((e) => e.message).join("\n")}` + ); + } + return checker.getType(expr) as CelType; +} + +void suite("checker", () => { + void test("const", () => { + const cases: { expr: ParsedExpr; want: CelType }[] = [ + { expr: parse("1"), want: CelScalar.INT }, + { expr: parse("1.0"), want: CelScalar.DOUBLE }, + { expr: parse("1u"), want: CelScalar.UINT }, + { expr: parse("true"), want: CelScalar.BOOL }, + { expr: parse("false"), want: CelScalar.BOOL }, + { expr: parse('"str"'), want: CelScalar.STRING }, + { expr: parse('b"bytes"'), want: CelScalar.BYTES }, + { expr: parse("null"), want: CelScalar.NULL }, + ]; + for (const c of cases) { + const got = internal__checkForTest(c.expr.expr!); + assert.equal(got.toString(), c.want.toString(), `case ${c.expr}`); + } + }); + + void test("ident", () => { + const cases: { expr: ParsedExpr; want: CelType }[] = [ + { expr: parse("is"), want: CelScalar.STRING }, + { expr: parse("ii"), want: CelScalar.INT }, + { expr: parse("iu"), want: CelScalar.UINT }, + { expr: parse("iz"), want: CelScalar.BOOL }, + { expr: parse("ib"), want: CelScalar.BYTES }, + { expr: parse("id"), want: CelScalar.DOUBLE }, + { expr: parse("ix"), want: CelScalar.NULL }, + { expr: parse("b"), want: listType(CelScalar.STRING) }, + { expr: parse("c"), want: mapType(CelScalar.STRING, CelScalar.BOOL) }, + ]; + for (const c of cases) { + const got = internal__checkForTest(c.expr.expr!); + assert.equal(got.toString(), c.want.toString(), `case ${c.expr}`); + } + }); + + void test("list", () => { + const cases: { expr: ParsedExpr; want: CelType }[] = [ + { expr: parse("[1, 2, 3]"), want: listType(CelScalar.INT) }, + { expr: parse("[1.0, 2.0, 3.0]"), want: listType(CelScalar.DOUBLE) }, + { expr: parse("[true, false, true]"), want: listType(CelScalar.BOOL) }, + { expr: parse('["a", "b", "c"]'), want: listType(CelScalar.STRING) }, + { expr: parse('[b"a", b"b", b"c"]'), want: listType(CelScalar.BYTES) }, + { expr: parse('["a", 1, 1.0]'), want: listType(CelScalar.DYN) }, + ]; + for (const c of cases) { + const got = internal__checkForTest(c.expr.expr!); + assert.equal(got.toString(), c.want.toString(), `case ${c.expr}`); + } + }); + + void test("map", () => { + const cases: { expr: ParsedExpr; want: CelType }[] = [ + { + expr: parse('{"a": 1, "b": 2}'), + want: mapType(CelScalar.STRING, CelScalar.INT), + }, + { + expr: parse('{"a": 1.0, "b": 2.0}'), + want: mapType(CelScalar.STRING, CelScalar.DOUBLE), + }, + { + expr: parse('{"a": true, "b": false}'), + want: mapType(CelScalar.STRING, CelScalar.BOOL), + }, + { + expr: parse('{"a": "x", "b": "y"}'), + want: mapType(CelScalar.STRING, CelScalar.STRING), + }, + { + expr: parse('{"a": b"x", "b": b"y"}'), + want: mapType(CelScalar.STRING, CelScalar.BYTES), + }, + { + expr: parse('{"a": 1, "b": 2.0}'), + want: mapType(CelScalar.STRING, CelScalar.DYN), + }, + { + expr: parse('{1: "a", 2: "b"}'), + want: mapType(CelScalar.INT, CelScalar.STRING), + }, + { + expr: parse('{1u: "a", 2u: "b"}'), + want: mapType(CelScalar.UINT, CelScalar.STRING), + }, + { + expr: parse('{1: "a", 2u: "b"}'), + want: mapType(CelScalar.DYN, CelScalar.STRING), + }, + { + expr: parse('{true: "a", false: "b"}'), + want: mapType(CelScalar.BOOL, CelScalar.STRING), + }, + { + expr: parse('{1: "a", "b": 2}'), + want: mapType(CelScalar.DYN, CelScalar.DYN), + }, + ]; + for (const c of cases) { + const got = internal__checkForTest(c.expr.expr!); + assert.equal(got.toString(), c.want.toString(), `case ${c.expr}`); + } + }); + + void test("message struct", () => { + const cases = [ + { + expr: parse("cel.expr.conformance.proto3.TestAllTypes.NestedMessage{}"), + want: objectType(TestAllTypes_NestedMessageSchema), + }, + { + expr: parse( + "cel.expr.conformance.proto3.TestAllTypes.NestedMessage{bb: 1}" + ), + want: objectType(TestAllTypes_NestedMessageSchema), + }, + { + expr: parse( + "cel.expr.conformance.proto3.TestAllTypes.NestedMessage{}.bb" + ), + want: CelScalar.INT, + }, + ]; + for (const c of cases) { + const got = internal__checkForTest(c.expr.expr!); + assert.equal(got.toString(), c.want.toString(), `case ${c.expr}`); + } + }); + + void test("wkt wrappers", () => { + const cases = [ + { + expr: parse("google.protobuf.BoolValue{}"), + want: objectType(BoolValueSchema), + }, + { + expr: parse("google.protobuf.BoolValue{}.value"), + want: CelScalar.BOOL, + }, + { + expr: parse("google.protobuf.BytesValue{}"), + want: objectType(BytesValueSchema), + }, + { + expr: parse("google.protobuf.BytesValue{}.value"), + want: CelScalar.BYTES, + }, + { + expr: parse("google.protobuf.DoubleValue{}"), + want: objectType(DoubleValueSchema), + }, + { + expr: parse("google.protobuf.DoubleValue{}.value"), + want: CelScalar.DOUBLE, + }, + { + expr: parse("google.protobuf.FloatValue{}"), + want: objectType(FloatValueSchema), + }, + { + expr: parse("google.protobuf.FloatValue{}.value"), + want: CelScalar.DOUBLE, + }, + { + expr: parse("google.protobuf.Int32Value{}"), + want: objectType(Int32ValueSchema), + }, + { + expr: parse("google.protobuf.Int32Value{}.value"), + want: CelScalar.INT, + }, + { + expr: parse("google.protobuf.Int64Value{}"), + want: objectType(Int64ValueSchema), + }, + { + expr: parse("google.protobuf.Int64Value{}.value"), + want: CelScalar.INT, + }, + { + expr: parse("google.protobuf.StringValue{}"), + want: objectType(StringValueSchema), + }, + { + expr: parse("google.protobuf.StringValue{}.value"), + want: CelScalar.STRING, + }, + { + expr: parse("google.protobuf.UInt64Value{value: 42u}"), + want: objectType(UInt64ValueSchema), + }, + { + expr: parse("google.protobuf.UInt64Value{}.value"), + want: CelScalar.UINT, + }, + { + expr: parse("google.protobuf.Any{}"), + want: objectType(AnySchema), + }, + { + expr: parse("google.protobuf.Duration{}"), + want: objectType(DurationSchema), + }, + { + expr: parse("google.protobuf.Duration{}.seconds"), + want: CelScalar.INT, + }, + { + expr: parse("google.protobuf.Timestamp{}"), + want: objectType(TimestampSchema), + }, + { + expr: parse("google.protobuf.Timestamp{}.seconds"), + want: CelScalar.INT, + }, + { + expr: parse("google.protobuf.ListValue{}"), + want: objectType(ListValueSchema), + }, + { + expr: parse("google.protobuf.ListValue{}.values"), + want: listType(objectType(ValueSchema)), + }, + { + expr: parse("google.protobuf.Struct{}"), + want: objectType(StructSchema), + }, + { + expr: parse("google.protobuf.Struct{}.fields"), + want: mapType(CelScalar.STRING, objectType(ValueSchema)), + }, + { + expr: parse("google.protobuf.Value{}"), + want: objectType(ValueSchema), + }, + ]; + for (const c of cases) { + const got = internal__checkForTest(c.expr.expr!); + assert.equal(got.toString(), c.want.toString(), `case ${c.expr}`); + } + }); + + void test("map select", () => { + const cases = [ + { + expr: parse("{'a': 1, 'b': '2'}.a"), + want: CelScalar.DYN, + }, + { + expr: parse("{'a': 1, 'b': 2}.a"), + want: CelScalar.INT, + }, + { + expr: parse("{'a': true, 'b': false}.b"), + want: CelScalar.BOOL, + }, + // TODO: optional syntax + // { + // expr: parse("c.?d"), + // want: optionalCelType(CelScalar.BOOL), + // } + ]; + for (const c of cases) { + const got = internal__checkForTest(c.expr.expr!); + assert.equal(got.toString(), c.want.toString(), `case ${c.expr}`); + } + }); + + void test("call expr", () => { + const cases = [ + { + expr: parse('fg_s()'), + want: CelScalar.STRING, + }, + { + expr: parse('"hello".fi_s_s()'), + want: CelScalar.STRING, + }, + { + expr: parse("size(b)"), + want: CelScalar.INT, + }, + { + expr: parse("c.size()"), + want: CelScalar.INT, + }, + { + expr: parse("'hello'.startsWith('he')"), + want: CelScalar.BOOL, + }, + { + expr: parse("is.startsWith('str')"), + want: CelScalar.BOOL, + }, + { + expr: parse("'hello'.contains('he')"), + want: CelScalar.BOOL, + }, + { + expr: parse("is.contains('str')"), + want: CelScalar.BOOL, + } + ]; + for (const c of cases) { + const got = internal__checkForTest(c.expr.expr!); + assert.equal(got.toString(), c.want.toString(), `case ${c.expr}`); + } + }); +}); diff --git a/packages/cel/src/checker/checker.ts b/packages/cel/src/checker/checker.ts new file mode 100644 index 00000000..1de461ba --- /dev/null +++ b/packages/cel/src/checker/checker.ts @@ -0,0 +1,902 @@ +import { AggregateLiteralElementType, type CelCheckerEnv } from "./env.js"; +import { Mapping } from "./mapping.js"; +import type { CheckedExpr } from "@bufbuild/cel-spec/cel/expr/checked_pb.js"; +import { CheckedExprSchema } from "@bufbuild/cel-spec/cel/expr/checked_pb.js"; +import type { Expr } from "@bufbuild/cel-spec/cel/expr/syntax_pb.js"; +import { create } from "@bufbuild/protobuf"; +import { celError, type CelError } from "../error.js"; +import { + functionType, + isAssignable, + isAssignableList, + mostGeneral, + substitute, +} from "./types.js"; +import { + type CelOpaqueType, + CelScalar, + type CelType, + DURATION, + errorType, + fieldDescToCelType, + isAssignableType, + isDynCelType, + isDynOrErrorCelType, + isErrorCelType, + isExactCelType, + isOptionalCelType, + listType, + type mapKeyType, + mapType, + maybeUnwrapOptionalCelType, + objectType, + optionalCelType, + TIMESTAMP, + typeParamType, +} from "../type.js"; +import { toQualifiedName } from "../namespace.js"; +import { + functionReference, + identReference, + type ReferenceInfo, +} from "../referenceinfo.js"; +import type { CelFunc } from "../func.js"; +import { + LOGICAL_AND, + LOGICAL_OR, + OPT_SELECT, +} from "../gen/dev/cel/expr/operator_const.js"; + +export interface CelChecker { + // +} + +/** + * TODO: this should return a CheckedExpr. We need functions to convert + * types and references to protobuf first. + */ +export function check(expr: Expr, env: CelCheckerEnv): CheckedExpr { + const checker = new _CelChecker(env); + checker.checkExpr(expr); + return create(CheckedExprSchema, { + expr, + // TODO: typeMap, referenceMap, sourceInfo + }); +} + +interface OverloadResolution { + type: CelType; + reference: ReferenceInfo; +} + +function overloadResolution( + type: CelType, + reference: ReferenceInfo +): OverloadResolution { + return { type, reference }; +} + +// TODO: do not export +export class _CelChecker implements CelChecker { + typeMap = new Map(); + referenceMap = new Map(); + errors: CelError[] = []; + mappings: Mapping = new Mapping(); + freeTypeVarCounter = 0; + + constructor(private readonly env: CelCheckerEnv) {} + + checkExpr(expr: Expr): void { + switch (expr.exprKind.case) { + case "constExpr": + return this.checkConstExpr(expr); + case "identExpr": + return this.checkIdentExpr(expr); + case "selectExpr": + return this.checkSelectExpr(expr); + case 'callExpr': + return this.checkCallExpr(expr); + case "listExpr": + return this.checkCreateListExpr(expr); + case "structExpr": + if (!expr.exprKind.value.messageName) { + return this.checkCreateMapExpr(expr); + } + return this.checkCreateStructExpr(expr); + default: + throw new Error(`unexpected expression kind: ${expr.exprKind.case}`); + } + } + + checkConstExpr(expr: Expr): void { + if (expr.exprKind.case !== "constExpr") { + this.errors.push(celError(`expected constExpr`, expr.id)); + return; + } + const constant = expr.exprKind.value; + switch (constant.constantKind.case) { + case "boolValue": + return this.setType(expr, CelScalar.BOOL); + case "bytesValue": + return this.setType(expr, CelScalar.BYTES); + case "doubleValue": + return this.setType(expr, CelScalar.DOUBLE); + case "durationValue": + return this.setType(expr, DURATION); + case "int64Value": + return this.setType(expr, CelScalar.INT); + case "nullValue": + return this.setType(expr, CelScalar.NULL); + case "stringValue": + return this.setType(expr, CelScalar.STRING); + case "timestampValue": + return this.setType(expr, TIMESTAMP); + case "uint64Value": + return this.setType(expr, CelScalar.UINT); + default: + throw new Error( + `unexpected constant kind: ${constant.constantKind.case}` + ); + } + } + + checkIdentExpr(expr: Expr): void { + if (expr.exprKind.case !== "identExpr") { + this.errors.push(celError(`expected identExpr`, expr.id)); + return; + } + const ident = expr.exprKind.value; + // Check to see if the identifier is declared. + const found = this.env.lookupIdent(ident.name); + if (found) { + this.setType(expr, found.type); + this.setReference(expr, identReference(found.name, found.value)); + // TODO: + // // Overwrite the identifier with its fully qualified name. + // e.SetKindCase(c.NewIdent(e.ID(), ident.Name())) + return; + } + const error = celError( + `undeclared reference to '${ + ident.name + }' (in container '${this.env.namespace!.name()}')`, + expr.id + ); + this.setType(expr, errorType(error)); + this.errors.push(error); + } + + checkSelectExpr(expr: Expr): void { + if (expr.exprKind.case !== "selectExpr") { + this.errors.push(celError(`expected selectExpr`, expr.id)); + return; + } + const sel = expr.exprKind.value; + // Before traversing down the tree, try to interpret as qualified name. + const [qname, found] = toQualifiedName(expr); + if (found) { + const ident = this.env.lookupIdent(qname); + if (ident) { + // We don't check for a TestOnly expression here since the `found` result is + // always going to be false for TestOnly expressions. + + // Rewrite the node to be a variable reference to the resolved fully-qualified + // variable name. + this.setType(expr, ident.type); + this.setReference(expr, identReference(ident.name, ident.value)); + // TODO: + // e.SetKindCase(c.NewIdent(e.ID(), ident.Name())) + return; + } + } + + let resultType = this.checkSelectField(expr, sel.operand, sel.field, false); + if (sel.testOnly) { + resultType = CelScalar.BOOL; + } + this.setType(expr, substitute(this.mappings, resultType, false)); + } + + checkOptSelect(expr: Expr): void { + if (expr.exprKind.case !== "callExpr") { + this.errors.push(celError(`expected callExpr`, expr.id)); + return; + } + // Collect metadata related to the opt select call packaged by the parser. + const call = expr.exprKind.value; + if (call.args.length !== 2 || call.target) { + const msg = `incorrect signature${ + call.target ? " member call with" : "" + } argument count: ${call.args.length}`; + this.errors.push( + celError(`unsupported optional field selection: ${msg}`) + ); + return; + } + + const operand = call.args[0]; + const field = call.args[1]; + if ( + field.exprKind.case !== "constExpr" || + field.exprKind.value.constantKind.case !== "stringValue" + ) { + this.errors.push( + celError(`unsupported optional field selection: ${field}`, field.id) + ); + return; + } + // Perform type-checking using the field selection logic. + const resultType = this.checkSelectField( + expr, + operand, + field.exprKind.value.constantKind.value, + true + ); + this.setType(expr, substitute(this.mappings, resultType, false)); + this.setReference(expr, functionReference(["select_optional_field"])); + } + + checkSelectField( + expr: Expr, + operand: Expr | undefined, + field: string, + optional: boolean + ): CelType { + if (!operand) { + this.errors.push(celError(`expected select operand`, expr.id)); + return errorType(celError(`invalid select operand`, expr.id)); + } + // Interpret as field selection, first traversing down the operand. + this.checkExpr(operand); + let operandType = this.getType(operand); + if (!operandType) { + this.errors.push( + celError(`unable to determine type of operand`, expr.id) + ); + return errorType(celError(`invalid select operand`, expr.id)); + } + operandType = substitute(this.mappings, operandType, false); + + // If the target type is 'optional', unwrap it for the sake of this check. + const isOpt = isOptionalCelType(operandType); + const targetType = maybeUnwrapOptionalCelType(operandType); + // Assume error type by default as most types do not support field selection. + let resultType: CelType = errorType( + celError( + `type '${operandType.toString()}' does not support field selection`, + expr.id + ) + ); + switch (targetType.kind) { + case "map": + // Maps yield their value type as the selection result type. + return targetType.value; + case "object": + // Objects yield their field type declaration as the selection result type, but only if + // the field is defined. + const fieldType = this.lookupFieldType( + expr.id, + targetType.desc.typeName, + field + ); + if (fieldType) { + resultType = fieldType; + } + break; + case "type_param": + // Set the operand type to DYN to prevent assignment to a potentially incorrect type + // at a later point in type-checking. The isAssignable call will update the type + // substitutions for the type param under the covers. + this.isAssignable(CelScalar.DYN, targetType); + // Also, set the result type to DYN. + resultType = CelScalar.DYN; + break; + default: + // Dynamic / error values are treated as DYN type. Errors are handled this way as well + // in order to allow forward progress on the check. + if (!isDynOrErrorCelType(targetType)) { + this.errors.push( + celError( + `type '${operandType.toString()}' does not support field selection`, + expr.id + ) + ); + } + resultType = CelScalar.DYN; + break; + } + + // If the target type was optional coming in, then the result must be optional going out. + if (isOpt || optional) { + return optionalCelType(resultType); + } + return resultType; + } + + checkCallExpr(expr: Expr): void { + if (expr.exprKind.case !== "callExpr") { + this.errors.push(celError(`expected callExpr`, expr.id)); + return; + } + const call = expr.exprKind.value; + const fnName = call.function; + if (fnName === OPT_SELECT) { + return this.checkOptSelect(expr); + } + + const args = call.args; + // Traverse arguments. + for (const arg of args) { + this.checkExpr(arg); + } + + // Regular static call with simple name. + if (!call.target) { + // Check for the existence of the function. + const fn = this.env.lookupFunction(fnName); + if (!fn) { + const err = celError( + `undeclared reference to '${fnName}' (in container '${this.env.namespace!.name()}')`, + expr.id + ); + this.errors.push(err); + this.setType(expr, errorType(err)); + return; + } + // TODO: + // // Overwrite the function name with its fully qualified resolved name. + // e.SetKindCase(c.NewCall(e.ID(), fn.Name(), args...)) + // Check to see whether the overload resolves. + this.resolveOverloadOrError(expr, fn, undefined, args); + return; + } + + // If a receiver 'target' is present, it may either be a receiver function, or a namespaced + // function, but not both. Given a.b.c() either a.b.c is a function or c is a function with + // target a.b. + // + // Check whether the target is a namespaced function name. + const target = call.target as Expr; + let [qualifiedPrefix, maybeQualified] = toQualifiedName(target); + if (maybeQualified) { + const maybeQualifiedName = qualifiedPrefix + "." + fnName; + const fn = this.env.lookupFunction(maybeQualifiedName); + if (fn) { + // The function name is namespaced and so preserving the target operand would + // be an inaccurate representation of the desired evaluation behavior. + // Overwrite with fully-qualified resolved function name sans receiver target. + // TODO: + // e.SetKindCase(c.NewCall(e.ID(), fn.Name(), args...)) + this.resolveOverloadOrError(expr, fn, undefined, args); + } + } + + // Regular instance call. + this.checkExpr(target); + const fn = this.env.lookupFunction(fnName); + // Function found, attempt overload resolution. + if (fn) { + this.resolveOverloadOrError(expr, fn, target, args); + return; + } + // Function name not declared, record error. + const err = celError( + `undeclared reference to '${fnName}' (in container '${this.env.namespace!.name()}')`, + expr.id + ); + this.errors.push(err); + this.setType(expr, errorType(err)); + } + + resolveOverloadOrError( + call: Expr, + fn: CelFunc, + target?: Expr, + args: Expr[] = [] + ) { + // Attempt to resolve the overload. + const resolution = this.resolveOverload(call, fn, target, args); + if (!resolution) { + const err = celError(`no matching overload for '${fn.name}'`, call.id); + this.errors.push(err); + this.setType(call, errorType(err)); + return; + } + // Overload found. + this.setType(call, resolution.type); + this.setReference(call, resolution.reference); + } + + resolveOverload( + call: Expr, + fn: CelFunc, + target?: Expr, + args: Expr[] = [] + ): OverloadResolution | undefined { + const argTypes: CelType[] = []; + if (target) { + const targetType = this.getType(target); + if (!targetType) { + this.errors.push( + celError(`unable to determine type of target`, call.id) + ); + return; + } + argTypes.push(targetType); + } + for (const arg of args) { + const argType = this.getType(arg); + if (!argType) { + this.errors.push( + celError(`unable to determine type of argument`, call.id) + ); + return; + } + argTypes.push(argType); + } + + let resultType: CelType | undefined; + let checkedRef: ReferenceInfo | undefined; + for (const overload of fn.overloads) { + // Determine whether the overload is currently considered. + if (this.env.isOverloadDisabled(overload.id)) { + continue; + } + + // Ensure the call style for the overload matches. + if ( + (target && !overload.isMemberFunction) || + (!target && overload.isMemberFunction) + ) { + // not a compatible call style. + continue; + } + + // Alternative type-checking behavior when the logical operators are compacted into + // variadic AST representations. + if (fn.name === LOGICAL_AND || fn.name === LOGICAL_OR) { + checkedRef = functionReference([fn.name]); + for (const argType of argTypes) { + if (!this.isAssignable(argType, CelScalar.BOOL)) { + const err = celError( + `expected type 'bool' but got '${argType.toString()}'`, + call.id + ); + this.errors.push(err); + resultType = errorType(err); + } + } + if (resultType && isErrorCelType(resultType)) { + return undefined; + } + return overloadResolution(CelScalar.BOOL, checkedRef); + } + + let overloadType: CelOpaqueType = functionType( + overload.result, + ...overload.parameters + ); + let typeParams = overload.typeParams(); + if (typeParams.length > 0) { + // Instantiate overload's type with fresh type variables. + const substitutions = new Mapping(); + for (const tp of typeParams) { + substitutions.add(typeParamType(tp), this.newTypeVar()); + } + overloadType = substitute( + substitutions, + overloadType, + false + ) as CelOpaqueType; + } + + const candidateArgTypes: CelType[] = overloadType.parameters.slice(1); + if (this.isAssignableList(argTypes, candidateArgTypes)) { + if (!checkedRef) { + checkedRef = functionReference([overload.id]); + } else { + checkedRef.addOverload(overload.id); + } + // First matching overload, determines result type. + const fnResultType = substitute( + this.mappings, + overloadType.parameters[0], + false + ); + if (!resultType) { + resultType = fnResultType; + } else if ( + !isDynCelType(resultType) && + !isExactCelType(fnResultType, resultType) + ) { + resultType = CelScalar.DYN; + } + } + } + if (!resultType) { + for (let i = 0; i < argTypes.length; i++) { + argTypes[i] = substitute(this.mappings, argTypes[i], true); + } + this.errors.push( + celError( + // TODO: improve error message with arg types + `no matching overload for '${fn.name}' applied to '(${argTypes + .map((t) => t.toString()) + .join(", ")})'`, + call.id + ) + ); + return; + } + return overloadResolution(resultType, checkedRef as ReferenceInfo); + } + + checkCreateListExpr(expr: Expr): void { + if (expr.exprKind.case !== "listExpr") { + this.errors.push(celError(`expected listExpr`, expr.id)); + return; + } + const create = expr.exprKind.value; + let elemsType: CelType | undefined; + const optionalIndices = create.optionalIndices; + const optionals: Record = {}; + for (const idx of optionalIndices) { + optionals[idx] = true; + } + for (let i = 0; i < create.elements.length; i++) { + const e = create.elements[i]; + this.checkExpr(e); + let elemType = this.getType(e); + if (optionals[i] && elemType) { + const isOptional = isOptionalCelType(elemType); + elemType = maybeUnwrapOptionalCelType(elemType); + if (!isOptional && !isDynCelType(elemType)) { + this.errors.push( + celError( + `expected type '${optionalCelType( + elemType + ).toString()}' but got '${elemType.toString()}'`, + e.id + ) + ); + return; + } + } + elemsType = this.joinTypes(e, elemsType, elemType); + } + if (!elemsType) { + // If the list is empty, assign free type var to elem type. + elemsType = this.newTypeVar(); + } + this.setType(expr, listType(elemsType)); + } + + checkCreateMapExpr(expr: Expr): void { + if (expr.exprKind.case !== "structExpr") { + this.errors.push(celError(`expected mapExpr`, expr.id)); + return; + } + const mapVal = expr.exprKind.value; + let mapKeyType: CelType | undefined; + let mapValueType: CelType | undefined; + for (const entry of mapVal.entries) { + if (entry.keyKind.case !== "mapKey") { + this.errors.push(celError(`expected map key`, entry.id)); + return; + } + const key = entry.keyKind.value; + this.checkExpr(key); + mapKeyType = this.joinTypes(key, mapKeyType, this.getType(key)); + + const val = entry.value; + if (!val) { + this.errors.push(celError(`expected map value`, entry.id)); + return; + } + this.checkExpr(val); + let valType = this.getType(val); + if (entry.optionalEntry) { + let isOptional = isOptionalCelType(valType!); + valType = maybeUnwrapOptionalCelType(valType!); + if (!isOptional && !isDynCelType(valType!)) { + const expected = optionalCelType(valType!); + this.errors.push( + celError( + `expected type '${expected.toString()}' but got '${valType!.toString()}'`, + val.id + ) + ); + } + } + mapValueType = this.joinTypes(val, mapValueType, valType); + } + if (!mapKeyType) { + // If the map is empty, assign free type variables to typeKey and value type. + mapKeyType = this.newTypeVar(); + mapValueType = this.newTypeVar(); + } + this.setType( + expr, + mapType(mapKeyType as mapKeyType, mapValueType as CelType) + ); + } + + checkCreateStructExpr(expr: Expr): void { + if (expr.exprKind.case !== "structExpr") { + this.errors.push(celError(`expected structExpr`, expr.id)); + return; + } + const msgVal = expr.exprKind.value; + // Determine the type of the message. + let resultType: CelType = errorType( + celError(`'${msgVal.messageName}' is not a message type`, expr.id) + ); + const ident = this.env.lookupIdent(msgVal.messageName); + if (!ident) { + const error = celError( + `undeclared reference to '${ + msgVal.messageName + }' (in container '${this.env.namespace!.name()}')`, + expr.id + ); + this.setType(expr, errorType(error)); + this.errors.push(error); + return; + } + // Ensure the type name is fully qualified in the AST. + let typeName = ident.name; + // TODO: + // if msgVal.TypeName() != typeName { + // e.SetKindCase(c.NewStruct(e.ID(), typeName, msgVal.Fields())) + // msgVal = e.AsStruct() + // } + this.setReference(expr, identReference(typeName, undefined)); + const identKind = ident.type.kind; + if (identKind !== "error") { + if (identKind !== "object") { + this.errors.push( + celError(`'${ident.type.name}' is not a type`, expr.id) + ); + } else { + resultType = objectType(ident.type.desc); + // Backwards compatibility test between well-known types and message types + // In this context, the type is being instantiated by its protobuf name which + // is not ideal or recommended, but some users expect this to work. + if (isWellKnownType(resultType)) { + typeName = getWellKnownTypeName(resultType)!; + } else if (resultType.kind === "object") { + typeName = resultType.desc.typeName; + } else { + const error = celError( + `'${ident.type.name}' is not a message type`, + expr.id + ); + this.errors.push(error); + resultType = errorType(error); + } + } + } + this.setType(expr, resultType); + + // Check the field initializers. + for (const field of msgVal.entries) { + if (field.keyKind.case !== "fieldKey") { + this.errors.push(celError(`expected field key`, field.id)); + return; + } + const fieldName = field.keyKind.value; + const value = field.value; + if (!value) { + this.errors.push(celError(`expected field value`, field.id)); + return; + } + this.checkExpr(value); + + let fieldType: CelType = errorType( + celError(`unable to determine type of field '${fieldName}'`, field.id) + ); + const ft = this.lookupFieldType(field.id, typeName, fieldName); + if (ft) { + fieldType = ft; + } + + const valType = this.getType(value); + if (field.optionalEntry) { + let isOptional = isOptionalCelType(valType!); + const unwrapped = maybeUnwrapOptionalCelType(valType!); + if (!isOptional && !isDynCelType(unwrapped)) { + const expected = optionalCelType(unwrapped); + this.errors.push( + celError( + `expected type '${expected.toString()}' but got '${unwrapped.toString()}'`, + value.id + ) + ); + } + } + if (!this.isAssignable(fieldType, valType!)) { + this.errors.push( + celError( + `expected type '${fieldType.toString()}' but got '${valType?.toString()}'`, + value.id + ) + ); + } + } + } + + joinTypes( + expr: Expr, + previous: CelType | undefined, + current: CelType | undefined + ): CelType | undefined { + if (!previous) { + return current; + } + if (this.isAssignable(previous, current!)) { + return mostGeneral(previous, current!); + } + if ( + this.env.aggregateLiteralElementType === + AggregateLiteralElementType.DynElementType + ) { + return CelScalar.DYN; + } + const err = celError( + `expected type '${previous.toString()}' but got '${current?.toString()}'`, + expr.id + ); + this.errors.push(err); + return errorType(err); + } + + newTypeVar(): CelType { + const id = this.freeTypeVarCounter; + this.freeTypeVarCounter++; + return typeParamType(`_var${id}`); + } + + isAssignable(t1: CelType, t2: CelType): boolean { + const subs = isAssignable(this.mappings, t1, t2); + if (subs) { + this.mappings = subs; + return true; + } + return false; + } + + isAssignableList(l1: CelType[], l2: CelType[]): boolean { + const subs = isAssignableList(this.mappings, l1, l2); + if (subs) { + this.mappings = subs; + return true; + } + return false; + } + + setType(expr: Expr, type: CelType): void { + const found = this.typeMap.get(expr.id); + if (found && found.kind !== type.kind) { + this.errors.push( + celError(`incompatible type already exists for expression`, expr.id) + ); + return; + } + this.typeMap.set(expr.id, type); + } + + getType(expr: Expr): CelType | undefined { + return this.typeMap.get(expr.id); + } + + setReference(expr: Expr, ref: ReferenceInfo): void { + const old = this.referenceMap.get(expr.id); + if (old && !old.equals(ref)) { + this.errors.push( + celError( + `reference already exists for expression: ${expr}(${expr.id}) old:${old}, new:${ref}`, + expr.id + ) + ); + return; + } + this.referenceMap.set(expr.id, ref); + } + + lookupFieldType( + id: bigint, + structType: string, + fieldName: string + ): CelType | undefined { + const msg = this.env.registry.getMessage(structType); + if (!msg) { + // This should not happen, anyway, report an error. + this.errors.push( + celError(`unexpected failed resolution of '${structType}'`, id) + ); + return undefined; + } + const field = msg.field[fieldName]; + if (!field) { + this.errors.push(celError(`undefined field '${fieldName}'`, id)); + return undefined; + } + return fieldDescToCelType(field); + } +} + +function isWellKnownType(t: CelType): boolean { + switch (t.kind) { + case "scalar": + switch (t.scalar) { + case "bool": + case "bytes": + case "double": + case "int": + case "string": + case "uint": + return isAssignableType(t, CelScalar.NULL); + case "dyn": + case "null_type": + return true; + default: + return false; + } + case "object": + switch (t.desc.typeName) { + case "google.protobuf.Any": + case "google.protobuf.Timestamp": + case "google.protobuf.Duration": + return true; + default: + return false; + } + case "list": + return isDynCelType(t.element); + case "map": + return t.key.scalar === "string" && isDynCelType(t.value); + default: + return false; + } +} + +function getWellKnownTypeName(t: CelType): string | undefined { + switch (t.kind) { + case "scalar": + switch (t.scalar) { + case "bool": + return "google.protobuf.BoolValue"; + case "bytes": + return "google.protobuf.BytesValue"; + case "double": + return "google.protobuf.DoubleValue"; + case "int": + return "google.protobuf.Int64Value"; + case "string": + return "google.protobuf.StringValue"; + case "uint": + return "google.protobuf.UInt64Value"; + case "dyn": + return "google.protobuf.Value"; + case "null_type": + return "google.protobuf.NullValue"; + default: + return undefined; + } + case "object": + switch (t.desc.typeName) { + case "google.protobuf.Any": + case "google.protobuf.Timestamp": + case "google.protobuf.Duration": + return t.desc.typeName; + default: + return undefined; + } + case "list": + return "google.protobuf.ListValue"; + case "map": + return "google.protobuf.Struct"; + default: + return undefined; + } +} diff --git a/packages/cel/src/checker/env.ts b/packages/cel/src/checker/env.ts new file mode 100644 index 00000000..bb60cbc6 --- /dev/null +++ b/packages/cel/src/checker/env.ts @@ -0,0 +1,343 @@ +import * as olc from "../gen/dev/cel/expr/overload_const.js"; +import { _CelChecker } from "./checker.js"; +import { type Registry } from "@bufbuild/protobuf"; +import { Namespace } from "../namespace.js"; +import { Group, Scopes } from "./scopes.js"; +import { type CelFunc } from "../func.js"; +import { celConstant, type CelIdent, celVariable } from "../ident.js"; +import { createRegistryWithWKT } from "../registry.js"; +import { CelScalar, objectType } from "../type.js"; +import { STD_FUNCS } from "../std/std.js"; + +const privateSymbol = Symbol.for("@bufbuild/cel/checker/env"); + +export enum AggregateLiteralElementType { + DynElementType = 1, + HomogenousElementType = 2, +} + +/** + * CEL checker environment. + * + * The environment defines the functions and types that are available + * during CEL expression checking. + */ +export interface CelCheckerEnv { + [privateSymbol]: unknown; + /** + * Namespace of the environment. + */ + readonly namespace: Namespace | undefined; + /** + * The protobuf registry to use. + */ + readonly registry: Registry; + /** + * The declarations available in this environment. + */ + readonly declarations: Scopes; + /** + * The aggregate literal element type strategy to use. + */ + readonly aggregateLiteralElementType: AggregateLiteralElementType; + /** + * The filtered overload ids. + */ + readonly filteredOverloadIds: Set; + + addIdents(idents: CelIdent[]): void; + + addFunctions(funcs: CelFunc[]): void; + + lookupIdent(name: string): CelIdent | undefined; + + lookupFunction(name: string): CelFunc | undefined; + + isOverloadDisabled(overloadID: string): boolean; + + validatedDeclarations(): Scopes; +} + +export interface CelCheckerEnvOptions { + /** + * Namespace of the environment. + */ + namespace?: string; + /** + * The protobuf registry to use. + */ + registry?: Registry; + /** + * Additional functions to add. + * + * All functions must be unique. This can be used to override any std function. + */ + funcs?: CelFunc[]; + /** + * Idents available in this environment. + */ + idents?: CelIdent[]; + /** + * Whether to enforce homogenous types in aggregate literals. + */ + homogenousAggregateLiterals?: boolean; + /** + * Whether to allow cross-type numeric comparisons. + */ + crossTypeNumericComparisons?: boolean; +} + +const crossTypeNumericComparisonOverloads = new Set([ + // double <-> int | uint + olc.LESS_DOUBLE_INT64, + olc.LESS_DOUBLE_UINT64, + olc.LESS_EQUALS_DOUBLE_INT64, + olc.LESS_EQUALS_DOUBLE_UINT64, + olc.GREATER_DOUBLE_INT64, + olc.GREATER_DOUBLE_UINT64, + olc.GREATER_EQUALS_DOUBLE_INT64, + olc.GREATER_EQUALS_DOUBLE_UINT64, + // int <-> double | uint + olc.LESS_INT64_DOUBLE, + olc.LESS_INT64_UINT64, + olc.LESS_EQUALS_INT64_DOUBLE, + olc.LESS_EQUALS_INT64_UINT64, + olc.GREATER_INT64_DOUBLE, + olc.GREATER_INT64_UINT64, + olc.GREATER_EQUALS_INT64_DOUBLE, + olc.GREATER_EQUALS_INT64_UINT64, + // uint <-> double | int + olc.LESS_UINT64_DOUBLE, + olc.LESS_UINT64_INT64, + olc.LESS_EQUALS_UINT64_DOUBLE, + olc.LESS_EQUALS_UINT64_INT64, + olc.GREATER_UINT64_DOUBLE, + olc.GREATER_UINT64_INT64, + olc.GREATER_EQUALS_UINT64_DOUBLE, + olc.GREATER_EQUALS_UINT64_INT64, +]); + +export function celCheckerEnv(options?: CelCheckerEnvOptions): CelCheckerEnv { + const idents = new Map(); + if (options?.idents) { + for (const ident of options.idents) { + idents.set(ident.name, ident); + } + } + const funcs = new Map(); + for (const func of STD_FUNCS.declarations) { + funcs.set(func.name, func); + } + if (options?.funcs) { + for (const func of options.funcs) { + funcs.set(func.name, func); + } + } + const declarations = new Scopes(new Group(idents, funcs)); + declarations.push(); + + let aggLitElemType = AggregateLiteralElementType.DynElementType; + if (options?.homogenousAggregateLiterals) { + aggLitElemType = AggregateLiteralElementType.HomogenousElementType; + } + + let filteredOverloadIds = crossTypeNumericComparisonOverloads; + if (options?.crossTypeNumericComparisons) { + filteredOverloadIds = new Set(); + } + return new _CelCheckerEnv( + new Namespace(options?.namespace ?? ""), + options?.registry + ? createRegistryWithWKT(options.registry) + : createRegistryWithWKT(), + declarations, + aggLitElemType, + filteredOverloadIds + ); +} + +class _CelCheckerEnv implements CelCheckerEnv { + [privateSymbol] = {}; + constructor( + public readonly namespace: Namespace, + public readonly registry: Registry, + public readonly declarations: Scopes, + public readonly aggregateLiteralElementType: AggregateLiteralElementType, + public readonly filteredOverloadIds: Set + ) {} + + /** + * AddIdents configures the checker with a list of variable declarations. + * + * If there are overlapping declarations, the method will error. + */ + addIdents(idents: CelIdent[]): void { + let errMsgs: string[] = []; + for (const ident of idents) { + const errMsg = this.#addIdent(ident); + if (errMsg) { + errMsgs.push(errMsg); + } + } + if (errMsgs.length > 0) { + throw new Error(errMsgs.join("\n")); + } + } + + /** + * AddFunctions configures the checker with a list of function declarations. + * + * If there are overlapping declarations, the method will error. + */ + addFunctions(funcs: CelFunc[]): void { + let errMsgs: string[] = []; + for (const fn of funcs) { + errMsgs = errMsgs.concat(this.#setFunction(fn)); + } + if (errMsgs.length > 0) { + throw new Error(errMsgs.join("\n")); + } + } + + /** + * LookupIdent returns an identifier in the Env. + * Returns undefined if no such identifier is found in the Env. + */ + lookupIdent(name: string): CelIdent | undefined { + for (const candidate of this.namespace.resolveCandidateNames(name)) { + const ident = this.declarations.findIdent(candidate); + if (ident) { + return ident; + } + + // Next try to import the name as a reference to a message type. + const msg = this.registry.getMessage(candidate); + if (msg) { + return celVariable(candidate, objectType(msg)); + } + + // Next try to import this as an enum value by splitting the name in a type prefix and + // the enum inside. + const lastDot = candidate.lastIndexOf("."); + if (lastDot !== -1) { + const enumTypeName = candidate.substring(0, lastDot); + const enumValueName = candidate.substring(lastDot + 1); + const enumType = this.registry.getEnum(enumTypeName); + if (enumType) { + const enumValueDesc = enumType.values.find( + (v) => v.name === enumValueName + ); + if (enumValueDesc) { + return celConstant(candidate, CelScalar.INT, enumValueDesc.number); + } + } + } + } + return undefined; + } + + /** + * LookupFunction returns a function declaration in the env. + * Returns undefined if no such function is found in the env. + */ + lookupFunction(name: string): CelFunc | undefined { + for (const candidate of this.namespace.resolveCandidateNames(name)) { + const fn = this.declarations.findFunction(candidate); + if (fn) { + return fn; + } + } + return undefined; + } + + /** + * setFunction adds the function declaration to the Env. + * Adds a function decl if one doesn't already exist, then adds all overloads from the Decl. + * If overload overlaps with an existing overload, adds to the errors in the Env instead. + */ + #setFunction(fn: CelFunc): string[] { + const errMsgs: string[] = []; + let current = this.declarations.findFunction(fn.name); + if (current) { + // TODO: merge overloads + // current = current.merge(fn) + return [ + `function ${fn.name} already declared. merging overloads not yet supported`, + ]; + } else { + current = fn; + } + // TODO: check macros + // for (const overload of current.overloads) { + // for _, macro := range parser.AllMacros { + // if macro.Function() == current.Name() && + // macro.IsReceiverStyle() == overload.IsMemberFunction() && + // macro.ArgCount() == len(overload.ArgTypes()) { + // errMsgs = append(errMsgs, overlappingMacroError(current.Name(), macro.ArgCount())) + // } + // } + // if len(errMsgs) > 0 { + // return errMsgs + // } + // } + this.declarations.setFunction(current); + return errMsgs; + } + + /** + * addIdent adds the Decl to the declarations in the Env. + * Returns a non-empty errorMsg if the identifier is already declared in the scope + */ + #addIdent(ident: CelIdent): string | null { + const current = this.declarations.findIdentInScope(ident.name); + if (current) { + if (current.declarationIsEquivalent(ident)) { + return null; + } + return `overlapping identifier for name '${ident.name}'`; + } + this.declarations.addIdent(ident); + return null; + } + + /** + * isOverloadDisabled returns whether the overloadID is disabled in the current environment. + */ + isOverloadDisabled(overloadID: string): boolean { + return this.filteredOverloadIds.has(overloadID); + } + + /** + * validatedDeclarations returns a reference to the validated variable and function declaration scope stack. + * must be copied before use. + */ + validatedDeclarations(): Scopes { + return this.declarations; + } + + /** + * enterScope creates a new Env instance with a new innermost declaration scope. + */ + enterScope(): CelCheckerEnv { + return new _CelCheckerEnv( + this.namespace, + this.registry, + this.declarations.push(), + this.aggregateLiteralElementType, + this.filteredOverloadIds + ); + } + + /** + * exitScope creates a new Env instance with the nearest outer declaration scope. + */ + exitScope(): CelCheckerEnv { + return new _CelCheckerEnv( + this.namespace, + this.registry, + this.declarations.pop(), + this.aggregateLiteralElementType, + this.filteredOverloadIds + ); + } +} diff --git a/packages/cel/src/checker/mapping.ts b/packages/cel/src/checker/mapping.ts new file mode 100644 index 00000000..fc4564ae --- /dev/null +++ b/packages/cel/src/checker/mapping.ts @@ -0,0 +1,21 @@ +import { type CelType } from "../type.js"; + +export class Mapping { + #mapping: Map; + + constructor(mapping: Map = new Map()) { + this.#mapping = mapping; + } + + add(from: CelType, to: CelType): void { + this.#mapping.set(from.toString(), to); + } + + find(from: CelType): CelType | undefined { + return this.#mapping.get(from.toString()); + } + + copy(): Mapping { + return new Mapping(new Map(this.#mapping)); + } +} diff --git a/packages/cel/src/checker/scopes.ts b/packages/cel/src/checker/scopes.ts new file mode 100644 index 00000000..304fbbb2 --- /dev/null +++ b/packages/cel/src/checker/scopes.ts @@ -0,0 +1,98 @@ +import { type CelFunc } from "../func.js"; +import { type CelIdent } from "../ident.js"; + +/** + * Group is a set of Decls that is pushed on or popped off a Scopes as a unit. + * Contains separate namespaces for identifier and function Decls. + */ +export class Group { + constructor( + public readonly idents: Map = new Map(), + public readonly functions: Map = new Map() + ) {} + + /** + * Creates a new Group instance with a shallow copy of the variables and functions. + */ + copy(): Group { + return new Group(new Map(this.idents), new Map(this.functions)); + } +} + +/** + * Scopes represents nested Decl sets where the Scopes value contains a Groups containing all + * identifiers in scope and an optional parent representing outer scopes. + * Each Groups value is a mapping of names to Decls in the ident and function namespaces. + * Lookups are performed such that bindings in inner scopes shadow those in outer scopes. + */ +export class Scopes { + constructor( + public readonly scopes = new Group(), + public readonly parent?: Scopes + ) { + this.scopes = scopes; + this.parent = parent; + } + + /** + * Creates a copy of the current Scopes values, including a copy of its parent if present. + */ + copy(): Scopes { + return new Scopes(this.scopes.copy(), this.parent?.copy()); + } + + /** + * Creates a new Scopes value which references the current Scope as its parent. + */ + push(): Scopes { + return new Scopes(new Group(), this); + } + + /** + * Returns the parent Scopes value for the current scope, or the current scope if the parent is undefined. + */ + pop(): Scopes { + return this.parent ?? this; + } + + /** + * Adds the ident in the current scope. + */ + addIdent(ident: CelIdent): void { + this.scopes.idents.set(ident.name, ident); + } + + /** + * Finds the first ident with a matching name in Scopes, or undefined if one cannot be found. + */ + findIdent(name: string): CelIdent | undefined { + if (this.scopes.idents.has(name)) { + return this.scopes.idents.get(name); + } + return this.parent?.findIdent(name); + } + + /** + * Finds the first ident Decl with a matching name in the current Scopes value, or undefined if one cannot be found. + */ + findIdentInScope(name: string): CelIdent | undefined { + return this.scopes.idents.get(name); + } + + /** + * Adds the function in the current scope. + */ + setFunction(func: CelFunc): void { + this.scopes.functions.set(func.name, func); + } + + /** + * Finds the first function with a matching name in Scopes, or undefined if one cannot be found. + */ + findFunction(name: string): CelFunc | undefined { + if (this.scopes.functions.has(name)) { + return this.scopes.functions.get(name); + } + return this.parent?.findFunction(name); + } +} diff --git a/packages/cel/src/checker/types.ts b/packages/cel/src/checker/types.ts new file mode 100644 index 00000000..61002146 --- /dev/null +++ b/packages/cel/src/checker/types.ts @@ -0,0 +1,362 @@ +import { + type CelListType, + type CelMapType, + type CelOpaqueType, + CelScalar, + type CelType, + isAssignableType, + isDynCelType, + isDynOrErrorCelType, + isExactCelType, + listType, + type mapKeyType, + mapType, + opaqueType, + typeParamTypeWithParam, +} from "../type.js"; +import { Mapping } from "./mapping.js"; + +/** + * isEqualOrLessSpecific checks whether one type is equal or less specific than the other one. + * A type is less specific if it matches the other type using the DYN type. + */ +function isEqualOrLessSpecific(t1: CelType, t2: CelType): boolean { + // The first type is less specific. + if (isDynCelType(t1) || t1.kind === "type") { + return true; + } + // The first type is not less specific. + if (isDynCelType(t2) || t2.kind === "type") { + return false; + } + // Types must be of the same kind to be equal. + if (t1.kind != t2.kind) { + return false; + } + + // With limited exceptions for ANY and JSON values, the types must agree and be equivalent in + // order to return true. + switch (t1.kind) { + case "opaque": + if ( + t1.name !== (t2 as CelOpaqueType).name || + t1.parameters.length !== (t2 as CelOpaqueType).parameters.length + ) { + return false; + } + for (let i = 0; i < t1.parameters.length; i++) { + if ( + !isEqualOrLessSpecific( + t1.parameters[i], + (t2 as CelOpaqueType).parameters[i] + ) + ) { + return false; + } + } + return true; + case "list": + return isEqualOrLessSpecific(t1.element, (t2 as CelListType).element); + case "map": + return ( + isEqualOrLessSpecific(t1.key, (t2 as CelMapType).key) && + isEqualOrLessSpecific(t1.value, (t2 as CelMapType).value) + ); + case "scalar": + return t1.scalar === "type"; + default: + return isExactCelType(t1, t2); + } +} + +function internalIsAssignable(m: Mapping, t1: CelType, t2: CelType): boolean { + // Process type parameters. + const kind1 = t1.kind; + const kind2 = t2.kind; + + if (kind2 === "type_param") { + // If t2 is a valid type substitution for t1, return true. + const [valid, t2HasSub] = isValidTypeSubstitution(m, t1, t2); + if (valid) { + return true; + } + // If t2 is not a valid type sub for t1, and already has a known substitution return false + // since it is not possible for t1 to be a substitution for t2. + if (!valid && t2HasSub) { + return false; + } + // Otherwise, fall through to check whether t1 is a possible substitution for t2. + } + if (kind1 === "type_param") { + // Return whether t1 is a valid substitution for t2. If not, do no additional checks as the + // possible type substitutions have been searched in both directions. + const [valid, _] = isValidTypeSubstitution(m, t2, t1); + return valid; + } + // Next check for wildcard types. + if (isDynOrErrorCelType(t1) || isDynOrErrorCelType(t2)) { + return true; + } + // Preserve the nullness checks of the legacy type-checker. + if (t1.toString() === CelScalar.NULL.toString()) { + return internalIsAssignableNull(t2); + } + if (t2.toString() === CelScalar.NULL.toString()) { + return internalIsAssignableNull(t1); + } + // Test for when the types do not need to agree, but are more specific than dyn. + switch (kind1) { + case "scalar": + case "object": + // Test whether t2 is assignable from t1. The order of this check won't usually matter; + // however, there may be cases where type capabilities are expanded beyond what is supported + // in the current common/types package. For example, an interface designation for a group of + // Struct types. + return isAssignableType(t2, t1); + case "type": + return kind2 === "type"; + case "opaque": + return ( + t1.kind == t2.kind && + t1.name == t2.name && + internalIsAssignableList(m, t1.parameters, t2.parameters) + ); + case "list": + if (kind2 !== "list") { + return false; + } + return internalIsAssignable(m, t1.element, (t2 as CelListType).element); + case "map": + if (kind2 !== "map") { + return false; + } + return ( + internalIsAssignable(m, t1.key, (t2 as CelMapType).key) && + internalIsAssignable(m, t1.value, (t2 as CelMapType).value) + ); + default: + return false; + } +} + +/** + * isValidTypeSubstitution returns whether t2 (or its type substitution) is a valid type + * substitution for t1, and whether t2 has a type substitution in mapping m. + * + * The type t2 is a valid substitution for t1 if any of the following statements is true + * - t2 has a type substitution (t2sub) equal to t1 + * - t2 has a type substitution (t2sub) assignable to t1 + * - t2 does not occur within t1. + */ +function isValidTypeSubstitution( + m: Mapping, + t1: CelType, + t2: CelType +): [boolean, boolean] { + // Early return if the t1 and t2 are the same instance. + const kind1 = t1.kind; + const kind2 = t2.kind; + if (kind1 === kind2 && isExactCelType(t1, t2)) { + return [true, true]; + } + const t2Sub = m.find(t2); + if (t2Sub) { + // Early return if t1 and t2Sub are the same instance as otherwise the mapping + // might mark a type as being a subtitution for itself. + if (kind1 == t2Sub.kind && isExactCelType(t1, t2Sub)) { + return [true, true]; + } + // If the types are compatible, pick the more general type and return true + if (internalIsAssignable(m, t1, t2Sub)) { + const t2New = mostGeneral(t1, t2Sub); + // only update the type reference map if the target type does not occur within it. + if (notReferencedIn(m, t2, t2New)) { + m.add(t2, t2New); + } + // acknowledge the type agreement, and that the substitution is already tracked. + return [true, true]; + } + return [false, true]; + } + if (notReferencedIn(m, t2, t1)) { + m.add(t2, t1); + return [true, false]; + } + return [false, false]; +} + +/** + * internalIsAssignableList returns true if the element types at each index in the list are + * assignable from l1[i] to l2[i]. The list lengths must also agree for the lists to be + * assignable. + */ +function internalIsAssignableList( + m: Mapping, + l1: CelType[], + l2: CelType[] +): boolean { + if (l1.length !== l2.length) { + return false; + } + for (let i = 0; i < l1.length; i++) { + if (!internalIsAssignable(m, l1[i], l2[i])) { + return false; + } + } + return true; +} + +/** + * internalIsAssignableNull returns true if the type is nullable. + */ +function internalIsAssignableNull(t: CelType): boolean { + return isLegacyNullable(t) || isAssignableType(CelScalar.NULL, t); +} + +/** + * isLegacyNullable preserves the null-ness compatibility of the original type-checker implementation. + */ +function isLegacyNullable(t: CelType): boolean { + switch (t.kind) { + case "opaque": + case "object": + // TODO; go calls out "any", "timestamp", and "duration" as nullable types. + return true; + default: + return false; + } +} + +/** + * isAssignable returns an updated type substitution mapping if t1 is assignable to t2. + */ +export function isAssignable( + m: Mapping, + t1: CelType, + t2: CelType +): Mapping | undefined { + const mCopy = m.copy(); + if (internalIsAssignable(mCopy, t1, t2)) { + return mCopy; + } + return undefined; +} + +/** + * isAssignableList returns an updated type substitution mapping if l1 is assignable to l2. + */ +export function isAssignableList( + m: Mapping, + l1: CelType[], + l2: CelType[] +): Mapping | undefined { + const mCopy = m.copy(); + if (internalIsAssignableList(mCopy, l1, l2)) { + return mCopy; + } + return undefined; +} + +/** + * mostGeneral returns the more general of two types which are known to unify. + */ +export function mostGeneral(t1: CelType, t2: CelType): CelType { + if (isEqualOrLessSpecific(t1, t2)) { + return t1; + } + return t2; +} + +/** + * notReferencedIn checks whether the type doesn't appear directly or transitively within the other + * type. This is a standard requirement for type unification, commonly referred to as the "occurs + * check". + */ +function notReferencedIn(m: Mapping, t: CelType, withinType: CelType): boolean { + if (isExactCelType(t, withinType)) { + return false; + } + switch (withinType.kind) { + case "type_param": + const wtSub = m.find(withinType); + if (!wtSub) { + return true; + } + return notReferencedIn(m, t, wtSub); + case "opaque": + for (const pt of withinType.parameters) { + if (!notReferencedIn(m, t, pt)) { + return false; + } + } + return true; + case "list": + return notReferencedIn(m, t, withinType.element); + case "map": + return ( + notReferencedIn(m, t, withinType.key) && + notReferencedIn(m, t, withinType.value) + ); + case "type": + if (withinType.type) { + return notReferencedIn(m, t, withinType.type); + } + return true; + default: + return true; + } +} + +/** + * substitute replaces all direct and indirect occurrences of bound type parameters. Unbound type + * parameters are replaced by DYN if typeParamToDyn is true. + */ +export function substitute(m: Mapping, t: CelType, typeParamToDyn: boolean): CelType { + const tSub = m.find(t); + if (tSub) { + return substitute(m, tSub, typeParamToDyn); + } + if (typeParamToDyn && t.kind === "type_param") { + return CelScalar.DYN; + } + switch (t.kind) { + case "opaque": + return opaqueType( + t.name, + substituteParams(m, t.parameters, typeParamToDyn) + ); + case "list": + return listType(substitute(m, t.element, typeParamToDyn)); + case "map": + return mapType( + substitute(m, t.key, typeParamToDyn) as mapKeyType, + substitute(m, t.value, typeParamToDyn) + ); + case "type": + if (t.type) { + return typeParamTypeWithParam(substitute(m, t.type, typeParamToDyn)); + } + return t; + default: + return t; + } +} + +function substituteParams( + m: Mapping, + typeParams: CelType[], + typeParamToDyn: boolean +): CelType[] { + const subParams: CelType[] = []; + for (let i = 0; i < typeParams.length; i++) { + subParams.push(substitute(m, typeParams[i], typeParamToDyn)); + } + return subParams; +} + +export function functionType( + resultType: CelType, + ...argTypes: CelType[] +): CelOpaqueType { + return opaqueType("function", [resultType, ...argTypes]); +} diff --git a/packages/cel/src/ext/strings/strings.ts b/packages/cel/src/ext/strings/strings.ts index 401042a0..45131a2c 100644 --- a/packages/cel/src/ext/strings/strings.ts +++ b/packages/cel/src/ext/strings/strings.ts @@ -29,6 +29,7 @@ import { isReflectMessage } from "@bufbuild/protobuf/reflect"; const charAt = celFunc("charAt", [ celOverload( + 'string_char_at_int', [CelScalar.STRING, CelScalar.INT], CelScalar.STRING, (str, index) => { @@ -43,11 +44,13 @@ const charAt = celFunc("charAt", [ const indexOf = celFunc("indexOf", [ celOverload( + 'string_index_of_string', [CelScalar.STRING, CelScalar.STRING], CelScalar.INT, (str, substr) => BigInt(str.indexOf(substr)), ), celOverload( + 'string_index_of_string_int', [CelScalar.STRING, CelScalar.STRING, CelScalar.INT], CelScalar.INT, (str, substr, startN) => { @@ -62,11 +65,13 @@ const indexOf = celFunc("indexOf", [ const lastIndexOf = celFunc("lastIndexOf", [ celOverload( + 'string_last_index_of_string', [CelScalar.STRING, CelScalar.STRING], CelScalar.INT, (str, substr) => BigInt(str.lastIndexOf(substr)), ), celOverload( + 'string_last_index_of_string_int', [CelScalar.STRING, CelScalar.STRING, CelScalar.INT], CelScalar.INT, (str, substr, startN) => { @@ -80,7 +85,7 @@ const lastIndexOf = celFunc("lastIndexOf", [ ]); const lowerAscii = celFunc("lowerAscii", [ - celOverload([CelScalar.STRING], CelScalar.STRING, (str) => { + celOverload('string_lower_ascii', [CelScalar.STRING], CelScalar.STRING, (str) => { // Only lower case ascii characters. let result = ""; for (let i = 0; i < str.length; i++) { @@ -96,7 +101,7 @@ const lowerAscii = celFunc("lowerAscii", [ ]); const upperAscii = celFunc("upperAscii", [ - celOverload([CelScalar.STRING], CelScalar.STRING, (str) => { + celOverload('string_upper_ascii', [CelScalar.STRING], CelScalar.STRING, (str) => { let result = ""; for (let i = 0; i < str.length; i++) { const c = str.charCodeAt(i); @@ -129,11 +134,13 @@ function replaceOp(str: string, substr: string, repl: string, num: number) { const replace = celFunc("replace", [ celOverload( + 'string_replace_string_string', [CelScalar.STRING, CelScalar.STRING, CelScalar.STRING], CelScalar.STRING, (str, substr, repl) => replaceOp(str, substr, repl, str.length), ), celOverload( + 'string_replace_string_string_int', [CelScalar.STRING, CelScalar.STRING, CelScalar.STRING, CelScalar.INT], CelScalar.STRING, (str, substr, repl, num) => replaceOp(str, substr, repl, Number(num)), @@ -149,11 +156,13 @@ function splitOp(str: string, sep: string, num?: number) { const split = celFunc("split", [ celOverload( + 'string_split_string', [CelScalar.STRING, CelScalar.STRING], listType(CelScalar.STRING), splitOp, ), celOverload( + 'string_split_string_int', [CelScalar.STRING, CelScalar.STRING, CelScalar.INT], listType(CelScalar.STRING), (str, sep, num) => splitOp(str, sep, Number(num)), @@ -183,8 +192,9 @@ function substringOp(str: string, start: bigint, end?: bigint) { } const substring = celFunc("substring", [ - celOverload([CelScalar.STRING, CelScalar.INT], CelScalar.STRING, substringOp), + celOverload('string_substring_int', [CelScalar.STRING, CelScalar.INT], CelScalar.STRING, substringOp), celOverload( + 'string_substring_int_int', [CelScalar.STRING, CelScalar.INT, CelScalar.INT], CelScalar.STRING, substringOp, @@ -199,7 +209,7 @@ const WHITE_SPACE = new Set([ ]); const trim = celFunc("trim", [ - celOverload([CelScalar.STRING], CelScalar.STRING, (str) => { + celOverload('string_trim', [CelScalar.STRING], CelScalar.STRING, (str) => { // Trim using the unicode white space definition. let start = 0; let end = str.length - 1; @@ -229,8 +239,9 @@ function joinOp(list: CelList, sep = "") { } const join = celFunc("join", [ - celOverload([listType(CelScalar.DYN)], CelScalar.STRING, joinOp), + celOverload('list_join', [listType(CelScalar.DYN)], CelScalar.STRING, joinOp), celOverload( + 'list_join_string', [listType(CelScalar.DYN), CelScalar.STRING], CelScalar.STRING, joinOp, @@ -251,7 +262,7 @@ const QUOTE_MAP: Map = new Map([ ]); const quote = celFunc("strings.quote", [ - celOverload([CelScalar.STRING], CelScalar.STRING, (str) => { + celOverload('strings_quote', [CelScalar.STRING], CelScalar.STRING, (str) => { let result = '"'; for (let i = 0; i < str.length; i++) { const c = str.charCodeAt(i); @@ -563,6 +574,7 @@ function formatImpl(format: string, args: CelList) { const format = celFunc("format", [ celOverload( + 'string_format', [CelScalar.STRING, listType(CelScalar.DYN)], CelScalar.STRING, formatImpl, diff --git a/packages/cel/src/func.ts b/packages/cel/src/func.ts index 27bd33ef..463961b0 100644 --- a/packages/cel/src/func.ts +++ b/packages/cel/src/func.ts @@ -74,6 +74,10 @@ export function celFunc( */ export interface CelOverload

{ [privateOverloadSymbol]: unknown; + /** + * Identifier of the overload. + */ + readonly id: string; /** * Array of parameter types. */ @@ -86,6 +90,14 @@ export interface CelOverload

{ * Implementation for this overload. */ readonly impl: (...args: CelValueTuple

) => CelInput; + /** + * Whether this is a member function overload. + */ + readonly isMemberFunction: boolean; + /** + * TypeParams returns the type parameter names associated with the overload. + */ + typeParams(): string[]; } /** @@ -95,11 +107,27 @@ export function celOverload< const P extends readonly CelType[], const R extends CelType, >( + id: string, parameters: P, result: R, impl: (...args: CelValueTuple

) => CelInput, ): CelOverload { - return new FuncOverload(parameters, result, impl); + return new FuncOverload(id, parameters, result, impl, false); +} + +/** + * Creates a new member function CelOverload. + */ +export function celMemberOverload< + const P extends readonly CelType[], + const R extends CelType, +>( + id: string, + parameters: P, + result: R, + impl: (...args: CelValueTuple

) => CelInput, +): CelOverload { + return new FuncOverload(id, parameters, result, impl, true); } class Func implements CelFunc { @@ -152,11 +180,16 @@ class FuncOverload { [privateOverloadSymbol] = {}; constructor( + private readonly _id: string, private readonly _parameters: P, private readonly _result: R, private readonly _impl: (...args: CelValueTuple

) => CelInput, + private readonly _isMemberFunction: boolean, ) {} + get id() { + return this._id; + } get parameters() { return this._parameters; } @@ -166,6 +199,33 @@ class FuncOverload get impl() { return this._impl; } + get isMemberFunction() { + return this._isMemberFunction; + } + typeParams(): string[] { + function collectParamNames(paramNames: string[], arg: CelType) { + switch (arg.kind) { + case 'type_param': + paramNames.push(arg.name); + break; + case 'list': + collectParamNames(paramNames, arg.element); + break; + case 'map': + collectParamNames(paramNames, arg.key); + collectParamNames(paramNames, arg.value); + break; + default: + break; + } + } + const typeNames: string[] = []; + collectParamNames(typeNames, this._result); + for (const paramType of this._parameters) { + collectParamNames(typeNames, paramType); + } + return typeNames; + } } /** @@ -173,6 +233,7 @@ class FuncOverload */ export class FuncRegistry implements Dispatcher { private functions = new Map(); + private functionDeclarations = new Map(); constructor(funcs?: CelFunc[]) { funcs && this.add(funcs); @@ -198,6 +259,7 @@ export class FuncRegistry implements Dispatcher { return; } call = nameOrFunc; + this.functionDeclarations.set(nameOrFunc.name, nameOrFunc); nameOrFunc = nameOrFunc.name; } if (call === undefined) { @@ -219,6 +281,10 @@ export class FuncRegistry implements Dispatcher { } this.functions.set(name, call); } + + get declarations(): CelFunc[] { + return Array.from(this.functionDeclarations.values()); + } } export class OrderedDispatcher implements Dispatcher { diff --git a/packages/cel/src/ident.ts b/packages/cel/src/ident.ts new file mode 100644 index 00000000..5a06c710 --- /dev/null +++ b/packages/cel/src/ident.ts @@ -0,0 +1,71 @@ +import { type CelValue, type CelType, isEquivalentCelType } from "./type.js"; + +const privateIdentSymbol = Symbol.for("@bufbuild/cel/ident"); + +/** + * A CEL ident definition. + */ +export interface CelIdent { + [privateIdentSymbol]: unknown; + + readonly name: string; + + readonly type: CelType; + + readonly value?: CelValue; + + readonly doc?: string; + + /** + * DeclarationIsEquivalent returns true if one variable declaration has the + * same name and same type as the input. + */ + declarationIsEquivalent(other: CelIdent): boolean; +} + +/** + * Creates a new CelVariable. + */ +export function celVariable( + name: string, + type: CelType, + doc?: string +): CelIdent { + return new Ident(name, type, undefined, doc); +} + +/** + * Creates a new CelConstant. + */ +export function celConstant( + name: string, + type: CelType, + value: CelValue, + doc?: string +): CelIdent { + return new Ident(name, type, value, doc); +} + +class Ident implements CelIdent { + [privateIdentSymbol]: unknown; + + constructor( + public readonly name: string, + public readonly type: CelType, + public readonly value?: CelValue, + public readonly doc?: string + ) {} + + declarationIsEquivalent(other: CelIdent): boolean { + if (this === other) { + return true; + } + // If either type is undefined, we cannot be equivalent. + if (!this.type || !other.type) { + return false; + } + return ( + this.name === other.name && isEquivalentCelType(this.type, other.type) + ); + } +} diff --git a/packages/cel/src/namespace.ts b/packages/cel/src/namespace.ts index 10fc5131..369f03e0 100644 --- a/packages/cel/src/namespace.ts +++ b/packages/cel/src/namespace.ts @@ -12,6 +12,8 @@ // See the License for the specific language governing permissions and // limitations under the License. +import type { Expr } from "@bufbuild/cel-spec/cel/expr/syntax_pb.js"; + export class Namespace { private readonly _name: string; private readonly _aliases: Map; @@ -76,3 +78,26 @@ export class Namespace { return alias + qualifier; } } + +/** + * ToQualifiedName converts an expression AST into a qualified name if possible, with a boolean + * 'found' value that indicates if the conversion is successful. + */ +export function toQualifiedName(e: Expr): [string, boolean] { + switch (e.exprKind.case) { + case 'identExpr': + return [e.exprKind.value.name, true]; + case 'selectExpr': + const sel = e.exprKind.value; + // Test only expressions are not valid as qualified names. + if (sel.testOnly || !sel.operand) { + return ["", false]; + } + const [qual, found] = toQualifiedName(sel.operand); + if (found) { + return [qual + "." + sel.field, true]; + } + break; + } + return ["", false]; +} \ No newline at end of file diff --git a/packages/cel/src/provider.ts b/packages/cel/src/provider.ts new file mode 100644 index 00000000..23dcef13 --- /dev/null +++ b/packages/cel/src/provider.ts @@ -0,0 +1,199 @@ +import { + type DescFile, + type DescMessage, + type MutableRegistry, + type Registry, + createMutableRegistry, +} from "@bufbuild/protobuf"; +import { type CelIdent } from "./ident.js"; +import { + CelScalar, + type CelType, + type CelValue, + DURATION, + fieldDescToCelType, + isEquivalentCelType, + listType, + mapType, + objectType, + TIMESTAMP, +} from "./type.js"; +import { celError } from "./error.js"; + +/** + * Provider specifies functions for creating new object instances and for resolving + * enum values by name. + */ +export interface Provider { + /** + * enumValue returns the numeric value of the given enum value name. + */ + enumValue(name: string): bigint | undefined; + + /** + * findIdent takes a qualified identifier name and returns a CelIdent if one exists. + */ + findIdent(ident: string): CelValue | undefined; + + /** + * findStructType returns the Descriptor given a qualified type name. + */ + findStructType(name: string): CelType | undefined; + + /** + * findStructFieldNames returns the set of field names for the given struct type, + * if the type exists in the registry. + */ + findStructFieldNames(typeName: string): string[] | undefined; + + /** + * findStructFieldType returns the field type for a checked type value. + */ + findStructFieldType(typeName: string, fieldName: string): CelType | undefined; + + // /** + // * newValue creates a new type value from a qualified name and map of field + // * name to value. + // */ + // newValue(typeName: string, fields: Record): CelValue | undefined; +} + +// TODO: this could probably be used by the planner as well +export class CelRegistry implements Provider { + protected revTypeMap = new Map(); + protected pbdb: MutableRegistry; + + constructor(public readonly idents: CelIdent[] = [], pbRegistry?: Registry) { + this.registerType( + CelScalar.BOOL, + CelScalar.BYTES, + CelScalar.DOUBLE, + DURATION, + CelScalar.INT, + listType(CelScalar.DYN), + mapType(CelScalar.DYN, CelScalar.DYN), + CelScalar.NULL, + CelScalar.STRING, + TIMESTAMP, + CelScalar.TYPE, + CelScalar.UINT, + ) + this.pbdb = pbRegistry + ? createMutableRegistry(pbRegistry) + : createMutableRegistry(); + for (const ident of idents) { + this.registerType(ident.type); + } + } + + /** + * copy copies the current state of the registry into its own memory space. + */ + copy(): CelRegistry { + const newReg = new CelRegistry([...this.idents], this.pbdb); + for (const [k, v] of this.revTypeMap) { + newReg.revTypeMap.set(k, v); + } + return newReg; + } + + enumValue(name: string): bigint | undefined { + const lastDot = name.lastIndexOf("."); + if (lastDot < 0) { + return undefined; + } + const enumName = name.substring(0, lastDot); + const enumValue = name.substring(lastDot + 1); + const _enum = this.pbdb.getEnum(enumName); + if (!_enum) { + return undefined; + } + const enumNumber = _enum.values.find((v) => v.name === enumValue); + if (!enumNumber) { + return undefined; + } + return BigInt(enumNumber.number); + } + + findIdent(identName: string): CelValue | undefined { + if (this.revTypeMap.has(identName)) { + return this.revTypeMap.get(identName); + } + const enumValue = this.enumValue(identName); + if (enumValue !== undefined) { + return enumValue; + } + return undefined; + } + + findStructType(typeName: string): CelType | undefined { + const struct = this.pbdb.getMessage(typeName); + if (struct) { + return objectType(struct); + } + return undefined; + } + + findStructFieldNames(typeName: string): string[] | undefined { + const struct = this.pbdb.getMessage(typeName); + if (!struct) { + return undefined; + } + return struct.fields.map((f) => f.name); + } + + findStructFieldType( + typeName: string, + fieldName: string + ): CelType | undefined { + const struct = this.pbdb.getMessage(typeName); + if (!struct) { + return undefined; + } + const field = struct.fields.find((f) => f.name === fieldName); + if (!field) { + return undefined; + } + return fieldDescToCelType(field); + } + + /** + * Registers the contents of a protocol buffer `FileDescriptor` + */ + registerDescriptor(fd: DescFile): void { + return this.registerAllTypes(fd); + } + + /** + * Registers a protocol buffer message and its dependencies. + */ + registerMessage(message: DescMessage): void { + this.pbdb.add(message); + return this.registerAllTypes(message.file); + } + + /** + * Registers a type value with the provider which ensures the provider is aware of how to + * map the type to an identifier. + */ + registerType(...types: CelType[]): void { + for (const type of types) { + if (!this.revTypeMap.has(type.name)) { + this.revTypeMap.set(type.name, type); + continue; + } + const existing = this.revTypeMap.get(type.name) as CelType; + if (!isEquivalentCelType(existing, type)) { + throw celError( + `type registration conflict. found: ${existing.toString()}, input: ${type.toString()}` + ); + } + } + } + + private registerAllTypes(fd: DescFile): void { + for (const msg of fd.messages) { + this.registerType(objectType(msg)); + } + } +} diff --git a/packages/cel/src/referenceinfo.test.ts b/packages/cel/src/referenceinfo.test.ts new file mode 100644 index 00000000..d795bb59 --- /dev/null +++ b/packages/cel/src/referenceinfo.test.ts @@ -0,0 +1,90 @@ +import * as assert from "node:assert/strict"; +import { suite, test } from "node:test"; +import { ADD_BYTES, ADD_DOUBLE } from "./gen/dev/cel/expr/overload_const.js"; +import { functionReference, identReference } from "./referenceinfo.js"; +import { toCel } from "./value.js"; + +void suite("ReferenceInfo", () => { + void test("equals", () => { + const testCases = [ + { + name: "single overload equal", + a: functionReference([ADD_BYTES]), + b: functionReference([ADD_BYTES]), + equal: true, + }, + { + name: "single overload not equal", + a: functionReference([ADD_BYTES]), + b: functionReference([ADD_DOUBLE]), + equal: false, + }, + { + name: "single and multiple overload not equal", + a: functionReference([ADD_BYTES]), + b: functionReference([ADD_BYTES, ADD_DOUBLE]), + equal: false, + }, + { + name: "multiple overloads equal", + a: functionReference([ADD_BYTES, ADD_DOUBLE]), + b: functionReference([ADD_DOUBLE, ADD_BYTES]), + equal: true, + }, + { + name: "identifier reference equal", + a: identReference("BYTES"), + b: identReference("BYTES"), + equal: true, + }, + { + name: "identifier reference not equal", + a: identReference("BYTES"), + b: identReference("TRUE"), + equal: false, + }, + { + name: "identifier and constant reference not equal", + a: identReference("BYTES"), + b: identReference("BYTES", toCel(new TextEncoder().encode("bytes"))), + equal: false, + }, + { + name: "constant references equal", + a: identReference("BYTES", toCel(new TextEncoder().encode("bytes"))), + b: identReference("BYTES", toCel(new TextEncoder().encode("bytes"))), + equal: true, + }, + { + name: "constant references not equal", + a: identReference("BYTES", toCel(new TextEncoder().encode("bytes"))), + b: identReference( + "BYTES", + toCel(new TextEncoder().encode("bytes-other")) + ), + equal: false, + }, + { + name: "constant and overload reference not equal", + a: identReference("BYTES", toCel(new TextEncoder().encode("bytes"))), + b: functionReference([ADD_DOUBLE, ADD_BYTES]), + equal: false, + }, + ]; + for (const tc of testCases) { + assert.equal( + tc.a.equals(tc.b), + tc.equal, + `unexpected equality for ${tc.name}` + ); + } + }); + + void test("add overload", () => { + const add = functionReference([ADD_BYTES]); + add.addOverload(ADD_DOUBLE); + assert.equal(functionReference([ADD_BYTES, ADD_DOUBLE]).equals(add), true); + add.addOverload(ADD_DOUBLE); + assert.equal(functionReference([ADD_BYTES, ADD_DOUBLE]).equals(add), true); + }); +}); diff --git a/packages/cel/src/referenceinfo.ts b/packages/cel/src/referenceinfo.ts new file mode 100644 index 00000000..f0e029bf --- /dev/null +++ b/packages/cel/src/referenceinfo.ts @@ -0,0 +1,69 @@ +import { equals } from "./equals.js"; +import type { CelValue } from "./type.js"; + +/** + * ReferenceInfo contains a CEL native representation of an identifier + * reference which may refer to either a qualified identifier name, a set of + * overload ids, or a constant value from an enum. + */ +export class ReferenceInfo { + constructor( + public readonly name?: string, + public readonly value?: CelValue, + public readonly overloadIds: Set = new Set() + ) {} + + /** + * AddOverload appends a function overload ID to the ReferenceInfo. + */ + addOverload(id: string) { + this.overloadIds.add(id); + } + + /** + * Equals returns whether two references are identical to each other. + */ + equals(other: ReferenceInfo) { + if (this.name !== other.name) { + return false; + } + if (this.overloadIds.size !== other.overloadIds.size) { + return false; + } + const otherOverloads = other.overloadIds; + for (const overload of this.overloadIds) { + if (!otherOverloads.has(overload)) { + return false; + } + } + if (this.value && !other.value) { + return false; + } + if (other.value && !this.value) { + return false; + } + if (this.value && other.value && !equals(this.value, other.value)) { + return false; + } + return true; + } +} + +/** + * identReference creates a ReferenceInfo instance for an identifier with an + * optional constant value. + */ +export function identReference(name: string, value?: CelValue): ReferenceInfo { + return new ReferenceInfo(name, value); +} + +/** + * functionReference creates a ReferenceInfo instance for a set of function + * overloads. + */ +export function functionReference( + overloadIds: Set | string[], +): ReferenceInfo { + return new ReferenceInfo("", undefined, new Set(overloadIds)); +} + diff --git a/packages/cel/src/std/cast.ts b/packages/cel/src/std/cast.ts index 5c2879bd..340994a7 100644 --- a/packages/cel/src/std/cast.ts +++ b/packages/cel/src/std/cast.ts @@ -37,6 +37,7 @@ import { import { celUint } from "../uint.js"; import { getMsgDesc } from "../eval.js"; import { parseDuration } from "../duration.js"; +import * as olc from "../gen/dev/cel/expr/overload_const.js"; const INT = "int"; const UINT = "uint"; @@ -50,35 +51,35 @@ const TYPE = "type"; const DYN = "dyn"; const intFunc = celFunc(INT, [ - celOverload([CelScalar.INT], CelScalar.INT, (x) => x), - celOverload([CelScalar.UINT], CelScalar.INT, (x) => { + celOverload(olc.INT_TO_INT, [CelScalar.INT], CelScalar.INT, (x) => x), + celOverload(olc.UINT_TO_INT, [CelScalar.UINT], CelScalar.INT, (x) => { const val = x.value; if (isOverflowInt(val)) { throw overflow(INT, CelScalar.INT); } return x.value; }), - celOverload([CelScalar.DOUBLE], CelScalar.INT, (x) => { + celOverload(olc.DOUBLE_TO_INT, [CelScalar.DOUBLE], CelScalar.INT, (x) => { if (isOverflowIntNum(x)) { throw overflow(INT, CelScalar.INT); } return BigInt(Math.trunc(x)); }), - celOverload([CelScalar.STRING], CelScalar.INT, (x) => { + celOverload(olc.STRING_TO_INT, [CelScalar.STRING], CelScalar.INT, (x) => { const val = BigInt(x); if (isOverflowInt(val)) { throw overflow(INT, CelScalar.INT); } return val; }), - celOverload([TIMESTAMP_TYPE], CelScalar.INT, (x) => { + celOverload(olc.TIMESTAMP_TO_INT, [TIMESTAMP_TYPE], CelScalar.INT, (x) => { const val = x.message.seconds; if (isOverflowInt(val)) { throw overflow(INT, CelScalar.INT); } return BigInt(val); }), - celOverload([DURATION_TYPE], CelScalar.INT, (x) => { + celOverload(olc.DURATION_TO_INT, [DURATION_TYPE], CelScalar.INT, (x) => { const val = x.message.seconds; if (isOverflowInt(val)) { throw overflow(INT, CelScalar.INT); @@ -88,20 +89,20 @@ const intFunc = celFunc(INT, [ ]); const uintFunc = celFunc(UINT, [ - celOverload([CelScalar.UINT], CelScalar.UINT, (x) => x), - celOverload([CelScalar.INT], CelScalar.UINT, (x) => { + celOverload(olc.UINT_TO_UINT, [CelScalar.UINT], CelScalar.UINT, (x) => x), + celOverload(olc.INT_TO_UINT, [CelScalar.INT], CelScalar.UINT, (x) => { if (isOverflowUint(x)) { throw overflow(UINT, CelScalar.UINT); } return celUint(x); }), - celOverload([CelScalar.DOUBLE], CelScalar.UINT, (x) => { + celOverload(olc.DOUBLE_TO_UINT, [CelScalar.DOUBLE], CelScalar.UINT, (x) => { if (isOverflowUintNum(x)) { throw overflow(UINT, CelScalar.UINT); } return celUint(BigInt(Math.trunc(x))); }), - celOverload([CelScalar.STRING], CelScalar.UINT, (x) => { + celOverload(olc.STRING_TO_UINT, [CelScalar.STRING], CelScalar.UINT, (x) => { const val = BigInt(x); if (isOverflowUint(val)) { throw overflow(UINT, CelScalar.UINT); @@ -111,15 +112,15 @@ const uintFunc = celFunc(UINT, [ ]); const doubleFunc = celFunc(DOUBLE, [ - celOverload([CelScalar.DOUBLE], CelScalar.DOUBLE, (x) => x), - celOverload([CelScalar.INT], CelScalar.DOUBLE, (x) => Number(x)), - celOverload([CelScalar.UINT], CelScalar.DOUBLE, (x) => Number(x.value)), - celOverload([CelScalar.STRING], CelScalar.DOUBLE, (x) => Number(x)), + celOverload(olc.DOUBLE_TO_DOUBLE, [CelScalar.DOUBLE], CelScalar.DOUBLE, (x) => x), + celOverload(olc.INT_TO_DOUBLE, [CelScalar.INT], CelScalar.DOUBLE, (x) => Number(x)), + celOverload(olc.DOUBLE_TO_UINT, [CelScalar.UINT], CelScalar.DOUBLE, (x) => Number(x.value)), + celOverload(olc.STRING_TO_DOUBLE, [CelScalar.STRING], CelScalar.DOUBLE, (x) => Number(x)), ]); const boolFunc = celFunc(BOOL, [ - celOverload([CelScalar.BOOL], CelScalar.BOOL, (x) => x), - celOverload([CelScalar.STRING], CelScalar.BOOL, (x) => { + celOverload(olc.BOOL_TO_BOOL, [CelScalar.BOOL], CelScalar.BOOL, (x) => x), + celOverload(olc.STRING_TO_BOOL, [CelScalar.STRING], CelScalar.BOOL, (x) => { switch (x) { case "true": case "True": @@ -140,19 +141,19 @@ const boolFunc = celFunc(BOOL, [ ]); const bytesFunc = celFunc(BYTES, [ - celOverload([CelScalar.BYTES], CelScalar.BYTES, (x) => x), - celOverload([CelScalar.STRING], CelScalar.BYTES, (x) => Buffer.from(x)), + celOverload(olc.BYTES_TO_BYTES, [CelScalar.BYTES], CelScalar.BYTES, (x) => x), + celOverload(olc.STRING_TO_BYTES, [CelScalar.STRING], CelScalar.BYTES, (x) => Buffer.from(x)), ]); const stringFunc = celFunc(STRING, [ - celOverload([CelScalar.STRING], CelScalar.STRING, (x) => x), - celOverload([CelScalar.BOOL], CelScalar.STRING, (x) => + celOverload(olc.STRING_TO_STRING, [CelScalar.STRING], CelScalar.STRING, (x) => x), + celOverload(olc.BOOL_TO_STRING, [CelScalar.BOOL], CelScalar.STRING, (x) => x ? "true" : "false", ), - celOverload([CelScalar.INT], CelScalar.STRING, (x) => x.toString()), - celOverload([CelScalar.UINT], CelScalar.STRING, (x) => x.value.toString()), - celOverload([CelScalar.DOUBLE], CelScalar.STRING, (x) => x.toString()), - celOverload([CelScalar.BYTES], CelScalar.STRING, (x) => { + celOverload(olc.INT_TO_STRING, [CelScalar.INT], CelScalar.STRING, (x) => x.toString()), + celOverload(olc.UINT_TO_STRING, [CelScalar.UINT], CelScalar.STRING, (x) => x.value.toString()), + celOverload(olc.DOUBLE_TO_STRING, [CelScalar.DOUBLE], CelScalar.STRING, (x) => x.toString()), + celOverload(olc.BYTES_TO_STRING, [CelScalar.BYTES], CelScalar.STRING, (x) => { const coder = new TextDecoder(undefined, { fatal: true }); try { return coder.decode(x); @@ -160,38 +161,38 @@ const stringFunc = celFunc(STRING, [ throw new Error(`Failed to decode bytes as string: ${e}`); } }), - celOverload([TIMESTAMP_TYPE], CelScalar.STRING, (x) => + celOverload(olc.TIMESTAMP_TO_STRING, [TIMESTAMP_TYPE], CelScalar.STRING, (x) => toJson(TimestampSchema, x.message), ), - celOverload([DURATION_TYPE], CelScalar.STRING, (x) => + celOverload(olc.DURATION_TO_STRING, [DURATION_TYPE], CelScalar.STRING, (x) => toJson(DurationSchema, x.message), ), ]); const timestampFunc = celFunc(TIMESTAMP, [ - celOverload([TIMESTAMP_TYPE], TIMESTAMP_TYPE, (x) => x), - celOverload([CelScalar.STRING], TIMESTAMP_TYPE, (x) => { + celOverload(olc.TIMESTAMP_TO_TIMESTAMP, [TIMESTAMP_TYPE], TIMESTAMP_TYPE, (x) => x), + celOverload(olc.STRING_TO_TIMESTAMP, [CelScalar.STRING], TIMESTAMP_TYPE, (x) => { try { return fromJson(TimestampSchema, x); } catch (e) { throw new Error(`Failed to parse timestamp: ${e}`); } }), - celOverload([CelScalar.INT], TIMESTAMP_TYPE, (x) => + celOverload(olc.INT_TO_TIMESTAMP, [CelScalar.INT], TIMESTAMP_TYPE, (x) => timestampFromMs(Number(x)), ), ]); const durationFunc = celFunc(DURATION, [ - celOverload([DURATION_TYPE], DURATION_TYPE, (x) => x), - celOverload([CelScalar.STRING], DURATION_TYPE, parseDuration), - celOverload([CelScalar.INT], DURATION_TYPE, (x) => + celOverload(olc.DURATION_TO_DURATION, [DURATION_TYPE], DURATION_TYPE, (x) => x), + celOverload(olc.STRING_TO_DURATION, [CelScalar.STRING], DURATION_TYPE, parseDuration), + celOverload(olc.INT_TO_DURATION, [CelScalar.INT], DURATION_TYPE, (x) => create(DurationSchema, { seconds: x }), ), ]); const typeFunc = celFunc(TYPE, [ - celOverload([CelScalar.DYN], CelScalar.TYPE, (v) => { + celOverload(olc.TYPE_CONVERT_TYPE, [CelScalar.DYN], CelScalar.TYPE, (v) => { if (isMessage(v)) { return objectType(getMsgDesc(v.$typeName)); } @@ -200,7 +201,7 @@ const typeFunc = celFunc(TYPE, [ ]); const dynFunc = celFunc(DYN, [ - celOverload([CelScalar.DYN], CelScalar.DYN, (x) => x), + celOverload(olc.TO_DYN, [CelScalar.DYN], CelScalar.DYN, (x) => x), ]); export function addCasts(funcs: FuncRegistry) { diff --git a/packages/cel/src/std/logic.ts b/packages/cel/src/std/logic.ts index 371d852b..50d6bc1b 100644 --- a/packages/cel/src/std/logic.ts +++ b/packages/cel/src/std/logic.ts @@ -17,6 +17,7 @@ import { celFunc, celOverload, type CallDispatch, + celMemberOverload, } from "../func.js"; import * as opc from "../gen/dev/cel/expr/operator_const.js"; import * as olc from "../gen/dev/cel/expr/overload_const.js"; @@ -48,7 +49,7 @@ const notStrictlyFalse: CallDispatch = { }; const notFunc = celFunc(opc.LOGICAL_NOT, [ - celOverload([CelScalar.BOOL], CelScalar.BOOL, (x) => !x), + celOverload(olc.LOGICAL_NOT, [CelScalar.BOOL], CelScalar.BOOL, (x) => !x), ]); const and: CallDispatch = { @@ -102,11 +103,12 @@ const or: CallDispatch = { }; const eqFunc = celFunc(opc.EQUALS, [ - celOverload([CelScalar.DYN, CelScalar.DYN], CelScalar.BOOL, equals), + celOverload(olc.EQUALS, [CelScalar.DYN, CelScalar.DYN], CelScalar.BOOL, equals), ]); const neFunc = celFunc(opc.NOT_EQUALS, [ celOverload( + olc.NOT_EQUALS, [CelScalar.DYN, CelScalar.DYN], CelScalar.BOOL, (lhs, rhs) => !equals(lhs, rhs), @@ -118,22 +120,22 @@ function ltOp(lhs: T, rhs: T) { } // biome-ignore format: Easier to read it like a table const ltFunc = celFunc(opc.LESS, [ - celOverload([CelScalar.BOOL, CelScalar.BOOL], CelScalar.BOOL, ltOp), - celOverload([CelScalar.BYTES, CelScalar.BYTES], CelScalar.BOOL, (l, r) => compareBytes(l, r) < 0), - celOverload([CelScalar.DOUBLE, CelScalar.DOUBLE], CelScalar.BOOL, ltOp), - celOverload([CelScalar.STRING, CelScalar.STRING], CelScalar.BOOL, ltOp), - celOverload([CelScalar.INT, CelScalar.INT], CelScalar.BOOL, ltOp), - celOverload([CelScalar.INT, CelScalar.UINT], CelScalar.BOOL, (l, r) => l < r.value), - celOverload([CelScalar.UINT, CelScalar.INT], CelScalar.BOOL, (l, r) => l.value < r), - celOverload([CelScalar.UINT, CelScalar.UINT], CelScalar.BOOL, (l, r) => l.value < r.value), + celOverload(olc.LESS_BOOL, [CelScalar.BOOL, CelScalar.BOOL], CelScalar.BOOL, ltOp), + celOverload(olc.LESS_BYTES, [CelScalar.BYTES, CelScalar.BYTES], CelScalar.BOOL, (l, r) => compareBytes(l, r) < 0), + celOverload(olc.LESS_DOUBLE, [CelScalar.DOUBLE, CelScalar.DOUBLE], CelScalar.BOOL, ltOp), + celOverload(olc.LESS_STRING, [CelScalar.STRING, CelScalar.STRING], CelScalar.BOOL, ltOp), + celOverload(olc.LESS_INT64, [CelScalar.INT, CelScalar.INT], CelScalar.BOOL, ltOp), + celOverload(olc.LESS_INT64_UINT64, [CelScalar.INT, CelScalar.UINT], CelScalar.BOOL, (l, r) => l < r.value), + celOverload(olc.LESS_UINT64_INT64, [CelScalar.UINT, CelScalar.INT], CelScalar.BOOL, (l, r) => l.value < r), + celOverload(olc.LESS_UINT64, [CelScalar.UINT, CelScalar.UINT], CelScalar.BOOL, (l, r) => l.value < r.value), // TODO investigate: ECMAScript relational operators support mixed bigint/number operands, // but removing the coercion to number here fails the conformance test "not_lt_dyn_int_big_lossy_double" - celOverload([CelScalar.INT, CelScalar.DOUBLE], CelScalar.BOOL, (l, r) => Number(l) < r), - celOverload([CelScalar.DOUBLE, CelScalar.INT], CelScalar.BOOL, (l, r) => l < Number(r)), - celOverload([CelScalar.DOUBLE, CelScalar.UINT], CelScalar.BOOL, (l, r) => l < Number(r.value)), - celOverload([CelScalar.UINT, CelScalar.DOUBLE], CelScalar.BOOL, (l, r) => Number(l.value) < r), - celOverload([DURATION, DURATION], CelScalar.BOOL, (l, r) => compareDuration(l, r) < 0), - celOverload([TIMESTAMP, TIMESTAMP], CelScalar.BOOL, (l, r) => compareTimestamp(l, r) < 0), + celOverload(olc.LESS_INT64_DOUBLE, [CelScalar.INT, CelScalar.DOUBLE], CelScalar.BOOL, (l, r) => Number(l) < r), + celOverload(olc.LESS_DOUBLE_INT64, [CelScalar.DOUBLE, CelScalar.INT], CelScalar.BOOL, (l, r) => l < Number(r)), + celOverload(olc.LESS_DOUBLE_UINT64, [CelScalar.DOUBLE, CelScalar.UINT], CelScalar.BOOL, (l, r) => l < Number(r.value)), + celOverload(olc.LESS_UINT64_DOUBLE, [CelScalar.UINT, CelScalar.DOUBLE], CelScalar.BOOL, (l, r) => Number(l.value) < r), + celOverload(olc.LESS_DURATION, [DURATION, DURATION], CelScalar.BOOL, (l, r) => compareDuration(l, r) < 0), + celOverload(olc.LESS_TIMESTAMP, [TIMESTAMP, TIMESTAMP], CelScalar.BOOL, (l, r) => compareTimestamp(l, r) < 0), ]); function lteOp(lhs: T, rhs: T) { @@ -141,20 +143,20 @@ function lteOp(lhs: T, rhs: T) { } // biome-ignore format: Easier to read it like a table const leFunc = celFunc(opc.LESS_EQUALS, [ - celOverload([CelScalar.BOOL, CelScalar.BOOL], CelScalar.BOOL, lteOp), - celOverload([CelScalar.BYTES, CelScalar.BYTES], CelScalar.BOOL, (l, r) => compareBytes(l, r) <= 0), - celOverload([CelScalar.DOUBLE, CelScalar.DOUBLE], CelScalar.BOOL, lteOp), - celOverload([CelScalar.STRING, CelScalar.STRING], CelScalar.BOOL, lteOp), - celOverload([CelScalar.INT, CelScalar.INT], CelScalar.BOOL, lteOp), - celOverload([CelScalar.INT, CelScalar.UINT], CelScalar.BOOL, (l, r) => l <= r.value), - celOverload([CelScalar.UINT, CelScalar.INT], CelScalar.BOOL, (l, r) => l.value <= r), - celOverload([CelScalar.UINT, CelScalar.UINT], CelScalar.BOOL, (l, r) => l.value <= r.value), - celOverload([CelScalar.INT, CelScalar.DOUBLE], CelScalar.BOOL, (l, r) => Number(l) <= r), - celOverload([CelScalar.DOUBLE, CelScalar.INT], CelScalar.BOOL, (l, r) => l <= Number(r)), - celOverload([CelScalar.DOUBLE, CelScalar.UINT], CelScalar.BOOL, (l, r) => l <= Number(r.value)), - celOverload([CelScalar.UINT, CelScalar.DOUBLE], CelScalar.BOOL, (l, r) => Number(l.value) <= r), - celOverload([DURATION, DURATION], CelScalar.BOOL, (l, r) => compareDuration(l, r) <= 0), - celOverload([TIMESTAMP, TIMESTAMP], CelScalar.BOOL, (l, r) => compareTimestamp(l, r) <= 0), + celOverload(olc.LESS_EQUALS_BOOL, [CelScalar.BOOL, CelScalar.BOOL], CelScalar.BOOL, lteOp), + celOverload(olc.LESS_EQUALS_BYTES, [CelScalar.BYTES, CelScalar.BYTES], CelScalar.BOOL, (l, r) => compareBytes(l, r) <= 0), + celOverload(olc.LESS_EQUALS_DOUBLE, [CelScalar.DOUBLE, CelScalar.DOUBLE], CelScalar.BOOL, lteOp), + celOverload(olc.LESS_EQUALS_STRING, [CelScalar.STRING, CelScalar.STRING], CelScalar.BOOL, lteOp), + celOverload(olc.LESS_EQUALS_INT64, [CelScalar.INT, CelScalar.INT], CelScalar.BOOL, lteOp), + celOverload(olc.LESS_EQUALS_INT64_UINT64, [CelScalar.INT, CelScalar.UINT], CelScalar.BOOL, (l, r) => l <= r.value), + celOverload(olc.LESS_EQUALS_UINT64_INT64, [CelScalar.UINT, CelScalar.INT], CelScalar.BOOL, (l, r) => l.value <= r), + celOverload(olc.LESS_EQUALS_UINT64, [CelScalar.UINT, CelScalar.UINT], CelScalar.BOOL, (l, r) => l.value <= r.value), + celOverload(olc.LESS_EQUALS_INT64_DOUBLE, [CelScalar.INT, CelScalar.DOUBLE], CelScalar.BOOL, (l, r) => Number(l) <= r), + celOverload(olc.LESS_EQUALS_DOUBLE_INT64, [CelScalar.DOUBLE, CelScalar.INT], CelScalar.BOOL, (l, r) => l <= Number(r)), + celOverload(olc.LESS_EQUALS_DOUBLE_UINT64, [CelScalar.DOUBLE, CelScalar.UINT], CelScalar.BOOL, (l, r) => l <= Number(r.value)), + celOverload(olc.LESS_EQUALS_UINT64_DOUBLE, [CelScalar.UINT, CelScalar.DOUBLE], CelScalar.BOOL, (l, r) => Number(l.value) <= r), + celOverload(olc.LESS_EQUALS_DURATION, [DURATION, DURATION], CelScalar.BOOL, (l, r) => compareDuration(l, r) <= 0), + celOverload(olc.LESS_EQUALS_TIMESTAMP, [TIMESTAMP, TIMESTAMP], CelScalar.BOOL, (l, r) => compareTimestamp(l, r) <= 0), ]); function gtOp(lhs: T, rhs: T) { @@ -162,20 +164,20 @@ function gtOp(lhs: T, rhs: T) { } // biome-ignore format: Easier to read it like a table const gtFunc = celFunc(opc.GREATER, [ - celOverload([CelScalar.BOOL, CelScalar.BOOL], CelScalar.BOOL, gtOp), - celOverload([CelScalar.BYTES, CelScalar.BYTES], CelScalar.BOOL, (l, r) => compareBytes(l, r) > 0), - celOverload([CelScalar.DOUBLE, CelScalar.DOUBLE], CelScalar.BOOL, gtOp), - celOverload([CelScalar.STRING, CelScalar.STRING], CelScalar.BOOL, gtOp), - celOverload([CelScalar.INT, CelScalar.INT], CelScalar.BOOL, gtOp), - celOverload([CelScalar.INT, CelScalar.UINT], CelScalar.BOOL, (l, r) => l > r.value), - celOverload([CelScalar.UINT, CelScalar.INT], CelScalar.BOOL, (l, r) => l.value > r), - celOverload([CelScalar.UINT, CelScalar.UINT], CelScalar.BOOL, (l, r) => l.value > r.value), - celOverload([CelScalar.INT, CelScalar.DOUBLE], CelScalar.BOOL, (l, r) => Number(l) > r), - celOverload([CelScalar.DOUBLE, CelScalar.INT], CelScalar.BOOL, (l, r) => l > Number(r)), - celOverload([CelScalar.DOUBLE, CelScalar.UINT], CelScalar.BOOL, (l, r) => l > Number(r.value)), - celOverload([CelScalar.UINT, CelScalar.DOUBLE], CelScalar.BOOL, (l, r) => Number(l.value) > r), - celOverload([DURATION, DURATION], CelScalar.BOOL, (l, r) => compareDuration(l, r) > 0), - celOverload([TIMESTAMP, TIMESTAMP], CelScalar.BOOL, (l, r) => compareTimestamp(l, r) > 0), + celOverload(olc.GREATER_BOOL, [CelScalar.BOOL, CelScalar.BOOL], CelScalar.BOOL, gtOp), + celOverload(olc.GREATER_BYTES, [CelScalar.BYTES, CelScalar.BYTES], CelScalar.BOOL, (l, r) => compareBytes(l, r) > 0), + celOverload(olc.GREATER_DOUBLE, [CelScalar.DOUBLE, CelScalar.DOUBLE], CelScalar.BOOL, gtOp), + celOverload(olc.GREATER_STRING, [CelScalar.STRING, CelScalar.STRING], CelScalar.BOOL, gtOp), + celOverload(olc.GREATER_INT64, [CelScalar.INT, CelScalar.INT], CelScalar.BOOL, gtOp), + celOverload(olc.GREATER_INT64_UINT64, [CelScalar.INT, CelScalar.UINT], CelScalar.BOOL, (l, r) => l > r.value), + celOverload(olc.GREATER_UINT64_INT64, [CelScalar.UINT, CelScalar.INT], CelScalar.BOOL, (l, r) => l.value > r), + celOverload(olc.GREATER_UINT64, [CelScalar.UINT, CelScalar.UINT], CelScalar.BOOL, (l, r) => l.value > r.value), + celOverload(olc.GREATER_INT64_DOUBLE, [CelScalar.INT, CelScalar.DOUBLE], CelScalar.BOOL, (l, r) => Number(l) > r), + celOverload(olc.GREATER_DOUBLE_INT64, [CelScalar.DOUBLE, CelScalar.INT], CelScalar.BOOL, (l, r) => l > Number(r)), + celOverload(olc.GREATER_DOUBLE_UINT64, [CelScalar.DOUBLE, CelScalar.UINT], CelScalar.BOOL, (l, r) => l > Number(r.value)), + celOverload(olc.GREATER_UINT64_DOUBLE, [CelScalar.UINT, CelScalar.DOUBLE], CelScalar.BOOL, (l, r) => Number(l.value) > r), + celOverload(olc.GREATER_DURATION, [DURATION, DURATION], CelScalar.BOOL, (l, r) => compareDuration(l, r) > 0), + celOverload(olc.GREATER_TIMESTAMP, [TIMESTAMP, TIMESTAMP], CelScalar.BOOL, (l, r) => compareTimestamp(l, r) > 0), ]); function gteOp(lhs: T, rhs: T) { @@ -183,36 +185,36 @@ function gteOp(lhs: T, rhs: T) { } // biome-ignore format: Easier to read it like a table const geFunc = celFunc(opc.GREATER_EQUALS, [ - celOverload([CelScalar.BOOL, CelScalar.BOOL], CelScalar.BOOL, gteOp), - celOverload([CelScalar.BYTES, CelScalar.BYTES], CelScalar.BOOL, (l, r) => compareBytes(l, r) >= 0), - celOverload([CelScalar.DOUBLE, CelScalar.DOUBLE], CelScalar.BOOL, gteOp), - celOverload([CelScalar.STRING, CelScalar.STRING], CelScalar.BOOL, gteOp), - celOverload([CelScalar.INT, CelScalar.INT], CelScalar.BOOL, gteOp), - celOverload([CelScalar.INT, CelScalar.UINT], CelScalar.BOOL, (l, r) => l >= r.value), - celOverload([CelScalar.UINT, CelScalar.INT], CelScalar.BOOL, (l, r) => l.value >= r), - celOverload([CelScalar.UINT, CelScalar.UINT], CelScalar.BOOL, (l, r) => l.value >= r.value), - celOverload([CelScalar.INT, CelScalar.DOUBLE], CelScalar.BOOL, (l, r) => Number(l) >= r), - celOverload([CelScalar.DOUBLE, CelScalar.INT], CelScalar.BOOL, (l, r) => l >= Number(r)), - celOverload([CelScalar.DOUBLE, CelScalar.UINT], CelScalar.BOOL, (l, r) => l >= Number(r.value)), - celOverload([CelScalar.UINT, CelScalar.DOUBLE], CelScalar.BOOL, (l, r) => Number(l.value) >= r), - celOverload([DURATION, DURATION], CelScalar.BOOL, (l, r) => compareDuration(l, r) >= 0), - celOverload([TIMESTAMP, TIMESTAMP], CelScalar.BOOL, (l, r) => compareTimestamp(l, r) >= 0), + celOverload(olc.GREATER_EQUALS_BOOL, [CelScalar.BOOL, CelScalar.BOOL], CelScalar.BOOL, gteOp), + celOverload(olc.GREATER_EQUALS_BYTES, [CelScalar.BYTES, CelScalar.BYTES], CelScalar.BOOL, (l, r) => compareBytes(l, r) >= 0), + celOverload(olc.GREATER_EQUALS_DOUBLE, [CelScalar.DOUBLE, CelScalar.DOUBLE], CelScalar.BOOL, gteOp), + celOverload(olc.GREATER_EQUALS_STRING, [CelScalar.STRING, CelScalar.STRING], CelScalar.BOOL, gteOp), + celOverload(olc.GREATER_EQUALS_INT64, [CelScalar.INT, CelScalar.INT], CelScalar.BOOL, gteOp), + celOverload(olc.GREATER_EQUALS_INT64_UINT64, [CelScalar.INT, CelScalar.UINT], CelScalar.BOOL, (l, r) => l >= r.value), + celOverload(olc.GREATER_EQUALS_UINT64_INT64, [CelScalar.UINT, CelScalar.INT], CelScalar.BOOL, (l, r) => l.value >= r), + celOverload(olc.GREATER_EQUALS_UINT64, [CelScalar.UINT, CelScalar.UINT], CelScalar.BOOL, (l, r) => l.value >= r.value), + celOverload(olc.GREATER_EQUALS_INT64_DOUBLE, [CelScalar.INT, CelScalar.DOUBLE], CelScalar.BOOL, (l, r) => Number(l) >= r), + celOverload(olc.GREATER_EQUALS_DOUBLE_INT64, [CelScalar.DOUBLE, CelScalar.INT], CelScalar.BOOL, (l, r) => l >= Number(r)), + celOverload(olc.GREATER_EQUALS_DOUBLE_UINT64, [CelScalar.DOUBLE, CelScalar.UINT], CelScalar.BOOL, (l, r) => l >= Number(r.value)), + celOverload(olc.GREATER_EQUALS_UINT64_DOUBLE, [CelScalar.UINT, CelScalar.DOUBLE], CelScalar.BOOL, (l, r) => Number(l.value) >= r), + celOverload(olc.GREATER_EQUALS_DURATION, [DURATION, DURATION], CelScalar.BOOL, (l, r) => compareDuration(l, r) >= 0), + celOverload(olc.GREATER_EQUALS_TIMESTAMP, [TIMESTAMP, TIMESTAMP], CelScalar.BOOL, (l, r) => compareTimestamp(l, r) >= 0), ]); const containsFunc = celFunc(olc.CONTAINS, [ - celOverload([CelScalar.STRING, CelScalar.STRING], CelScalar.BOOL, (x, y) => + celMemberOverload(olc.CONTAINS_STRING, [CelScalar.STRING, CelScalar.STRING], CelScalar.BOOL, (x, y) => x.includes(y), ), ]); const endsWithFunc = celFunc(olc.ENDS_WITH, [ - celOverload([CelScalar.STRING, CelScalar.STRING], CelScalar.BOOL, (x, y) => + celMemberOverload(olc.ENDS_WITH_STRING, [CelScalar.STRING, CelScalar.STRING], CelScalar.BOOL, (x, y) => x.endsWith(y), ), ]); const startsWithFunc = celFunc(olc.STARTS_WITH, [ - celOverload([CelScalar.STRING, CelScalar.STRING], CelScalar.BOOL, (x, y) => + celMemberOverload(olc.STARTS_WITH_STRING, [CelScalar.STRING, CelScalar.STRING], CelScalar.BOOL, (x, y) => x.startsWith(y), ), ]); @@ -274,7 +276,8 @@ export function matchesString(x: string, y: string): boolean { } const matchesFunc = celFunc(olc.MATCHES, [ - celOverload( + celMemberOverload( + olc.MATCHES_STRING, [CelScalar.STRING, CelScalar.STRING], CelScalar.BOOL, matchesString, @@ -282,25 +285,38 @@ const matchesFunc = celFunc(olc.MATCHES, [ ]); const sizeFunc = celFunc(olc.SIZE, [ - celOverload([CelScalar.STRING], CelScalar.INT, (x) => { + celOverload(olc.SIZE_STRING, [CelScalar.STRING], CelScalar.INT, (x) => { let size = 0; for (const _ of x) { size++; } return BigInt(size); }), - celOverload([CelScalar.BYTES], CelScalar.INT, (x) => BigInt(x.length)), - celOverload([listType(CelScalar.DYN)], CelScalar.INT, (x) => BigInt(x.size)), - celOverload([mapType(CelScalar.INT, CelScalar.DYN)], CelScalar.INT, (x) => + celMemberOverload(olc.SIZE_STRING_INST, [CelScalar.STRING], CelScalar.INT, (x) => { + let size = 0; + for (const _ of x) { + size++; + } + return BigInt(size); + }), + celOverload(olc.SIZE_BYTES, [CelScalar.BYTES], CelScalar.INT, (x) => BigInt(x.length)), + celMemberOverload(olc.SIZE_BYTES_INST, [CelScalar.BYTES], CelScalar.INT, (x) => BigInt(x.length)), + celOverload(olc.SIZE_LIST, [listType(CelScalar.DYN)], CelScalar.INT, (x) => BigInt(x.size)), + celMemberOverload(olc.SIZE_LIST_INST, [listType(CelScalar.DYN)], CelScalar.INT, (x) => BigInt(x.size)), + // TODO: this may need to be one dyn like in other implementations + celOverload(olc.SIZE_MAP + '_int_key', [mapType(CelScalar.INT, CelScalar.DYN)], CelScalar.INT, (x) => + BigInt(x.size), + ), + celOverload(olc.SIZE_MAP + '_uint_key', [mapType(CelScalar.UINT, CelScalar.DYN)], CelScalar.INT, (x) => BigInt(x.size), ), - celOverload([mapType(CelScalar.UINT, CelScalar.DYN)], CelScalar.INT, (x) => + celOverload(olc.SIZE_MAP + '_bool_key', [mapType(CelScalar.BOOL, CelScalar.DYN)], CelScalar.INT, (x) => BigInt(x.size), ), - celOverload([mapType(CelScalar.BOOL, CelScalar.DYN)], CelScalar.INT, (x) => + celOverload(olc.SIZE_MAP + '_string_key', [mapType(CelScalar.STRING, CelScalar.DYN)], CelScalar.INT, (x) => BigInt(x.size), ), - celOverload([mapType(CelScalar.STRING, CelScalar.DYN)], CelScalar.INT, (x) => + celMemberOverload(olc.SIZE_MAP_INST, [mapType(CelScalar.DYN, CelScalar.DYN)], CelScalar.INT, (x) => BigInt(x.size), ), ]); @@ -311,6 +327,7 @@ function mapInOp(x: CelValue, y: CelMap) { const inFunc = celFunc(opc.IN, [ celOverload( + olc.IN_LIST, [CelScalar.DYN, listType(CelScalar.DYN)], CelScalar.BOOL, (x, y) => { @@ -322,22 +339,27 @@ const inFunc = celFunc(opc.IN, [ return false; }, ), + // TODO: this may need to be one dyn like in other implementations celOverload( + olc.IN_MAP, [CelScalar.DYN, mapType(CelScalar.STRING, CelScalar.DYN)], CelScalar.BOOL, mapInOp, ), celOverload( + olc.IN_MAP + '_int_key', [CelScalar.DYN, mapType(CelScalar.INT, CelScalar.DYN)], CelScalar.BOOL, mapInOp, ), celOverload( + olc.IN_MAP + '_uint_key', [CelScalar.DYN, mapType(CelScalar.UINT, CelScalar.DYN)], CelScalar.BOOL, mapInOp, ), celOverload( + olc.IN_MAP + '_bool_key', [CelScalar.DYN, mapType(CelScalar.BOOL, CelScalar.DYN)], CelScalar.BOOL, mapInOp, diff --git a/packages/cel/src/std/math.ts b/packages/cel/src/std/math.ts index 9d401489..8e0505e5 100644 --- a/packages/cel/src/std/math.ts +++ b/packages/cel/src/std/math.ts @@ -17,6 +17,7 @@ import { DurationSchema, TimestampSchema } from "@bufbuild/protobuf/wkt"; import { type FuncRegistry, celOverload, celFunc } from "../func.js"; import * as opc from "../gen/dev/cel/expr/operator_const.js"; +import * as olc from "../gen/dev/cel/expr/overload_const.js"; import { CelScalar, DURATION, @@ -107,14 +108,14 @@ function subtractDurationOrTimestamp< } const add = celFunc(opc.ADD, [ - celOverload([CelScalar.INT, CelScalar.INT], CelScalar.INT, (lhs, rhs) => { + celOverload(olc.ADD_INT64, [CelScalar.INT, CelScalar.INT], CelScalar.INT, (lhs, rhs) => { const val = lhs + rhs; if (isOverflowInt(val)) { throw overflow(opc.SUBTRACT, CelScalar.INT); } return val; }), - celOverload([CelScalar.UINT, CelScalar.UINT], CelScalar.UINT, (lhs, rhs) => { + celOverload(olc.ADD_UINT64, [CelScalar.UINT, CelScalar.UINT], CelScalar.UINT, (lhs, rhs) => { const val = lhs.value + rhs.value; if (isOverflowUint(val)) { throw overflow(opc.SUBTRACT, CelScalar.UINT); @@ -122,16 +123,19 @@ const add = celFunc(opc.ADD, [ return celUint(val); }), celOverload( + olc.ADD_DOUBLE, [CelScalar.DOUBLE, CelScalar.DOUBLE], CelScalar.DOUBLE, (lhs, rhs) => lhs + rhs, ), celOverload( + olc.ADD_STRING, [CelScalar.STRING, CelScalar.STRING], CelScalar.STRING, (lhs, rhs) => lhs + rhs, ), celOverload( + olc.ADD_BYTES, [CelScalar.BYTES, CelScalar.BYTES], CelScalar.BYTES, (lhs, rhs) => { @@ -141,13 +145,14 @@ const add = celFunc(opc.ADD, [ return val; }, ), - celOverload([TIMESTAMP, TIMESTAMP], TIMESTAMP, addTimestamp), - celOverload([TIMESTAMP, DURATION], TIMESTAMP, addTimestamp), - celOverload([DURATION, TIMESTAMP], TIMESTAMP, (lhs, rhs) => + celOverload('add_timestamp_timestamp', [TIMESTAMP, TIMESTAMP], TIMESTAMP, addTimestamp), + celOverload(olc.ADD_TIMESTAMP_DURATION, [TIMESTAMP, DURATION], TIMESTAMP, addTimestamp), + celOverload(olc.ADD_DURATION_TIMESTAMP, [DURATION, TIMESTAMP], TIMESTAMP, (lhs, rhs) => addTimestamp(rhs, lhs), ), - celOverload([DURATION, DURATION], DURATION, addDuration), + celOverload(olc.ADD_DURATION_DURATION, [DURATION, DURATION], DURATION, addDuration), celOverload( + olc.ADD_LIST, [listType(CelScalar.DYN), listType(CelScalar.DYN)], listType(CelScalar.DYN), celListConcat, @@ -155,14 +160,14 @@ const add = celFunc(opc.ADD, [ ]); const subtract = celFunc(opc.SUBTRACT, [ - celOverload([CelScalar.INT, CelScalar.INT], CelScalar.INT, (lhs, rhs) => { + celOverload(olc.SUBTRACT_INT64, [CelScalar.INT, CelScalar.INT], CelScalar.INT, (lhs, rhs) => { const val = lhs - rhs; if (isOverflowInt(val)) { throw overflow(opc.SUBTRACT, CelScalar.INT); } return val; }), - celOverload([CelScalar.UINT, CelScalar.UINT], CelScalar.UINT, (lhs, rhs) => { + celOverload(olc.SUBTRACT_UINT64, [CelScalar.UINT, CelScalar.UINT], CelScalar.UINT, (lhs, rhs) => { const val = lhs.value - rhs.value; if (isOverflowUint(val)) { throw overflow(opc.SUBTRACT, CelScalar.UINT); @@ -170,13 +175,14 @@ const subtract = celFunc(opc.SUBTRACT, [ return celUint(val); }), celOverload( + olc.SUBTRACT_DOUBLE, [CelScalar.DOUBLE, CelScalar.DOUBLE], CelScalar.DOUBLE, (lhs, rhs) => lhs - rhs, ), - celOverload([TIMESTAMP, TIMESTAMP], DURATION, subtractDurationOrTimestamp), - celOverload([DURATION, DURATION], DURATION, subtractDurationOrTimestamp), - celOverload([TIMESTAMP, DURATION], TIMESTAMP, (lhs, rhs) => + celOverload(olc.SUBTRACT_TIMESTAMP_TIMESTAMP, [TIMESTAMP, TIMESTAMP], DURATION, subtractDurationOrTimestamp), + celOverload(olc.SUBTRACT_DURATION_DURATION, [DURATION, DURATION], DURATION, subtractDurationOrTimestamp), + celOverload(olc.SUBTRACT_TIMESTAMP_DURATION, [TIMESTAMP, DURATION], TIMESTAMP, (lhs, rhs) => createTimestamp( lhs.message.seconds - rhs.message.seconds, lhs.message.nanos - rhs.message.nanos, @@ -185,14 +191,14 @@ const subtract = celFunc(opc.SUBTRACT, [ ]); const multiply = celFunc(opc.MULTIPLY, [ - celOverload([CelScalar.INT, CelScalar.INT], CelScalar.INT, (lhs, rhs) => { + celOverload(olc.MULTIPLY_INT64, [CelScalar.INT, CelScalar.INT], CelScalar.INT, (lhs, rhs) => { const product = lhs * rhs; if (isOverflowInt(product)) { throw overflow(opc.MULTIPLY, CelScalar.INT); } return product; }), - celOverload([CelScalar.UINT, CelScalar.UINT], CelScalar.UINT, (lhs, rhs) => { + celOverload(olc.MULTIPLY_UINT64, [CelScalar.UINT, CelScalar.UINT], CelScalar.UINT, (lhs, rhs) => { const product = lhs.value * rhs.value; if (isOverflowUint(product)) { throw overflow(opc.MULTIPLY, CelScalar.UINT); @@ -200,6 +206,7 @@ const multiply = celFunc(opc.MULTIPLY, [ return celUint(product); }), celOverload( + olc.MULTIPLY_DOUBLE, [CelScalar.DOUBLE, CelScalar.DOUBLE], CelScalar.DOUBLE, (lhs, rhs) => lhs * rhs, @@ -207,7 +214,7 @@ const multiply = celFunc(opc.MULTIPLY, [ ]); const divide = celFunc(opc.DIVIDE, [ - celOverload([CelScalar.INT, CelScalar.INT], CelScalar.INT, (lhs, rhs) => { + celOverload(olc.DIVIDE_INT64, [CelScalar.INT, CelScalar.INT], CelScalar.INT, (lhs, rhs) => { if (rhs === 0n) { throw divisionByZero(CelScalar.INT); } @@ -217,11 +224,12 @@ const divide = celFunc(opc.DIVIDE, [ return lhs / rhs; }), celOverload( + olc.DIVIDE_DOUBLE, [CelScalar.DOUBLE, CelScalar.DOUBLE], CelScalar.DOUBLE, (lhs, rhs) => lhs / rhs, ), - celOverload([CelScalar.UINT, CelScalar.UINT], CelScalar.UINT, (lhs, rhs) => { + celOverload(olc.DIVIDE_UINT64, [CelScalar.UINT, CelScalar.UINT], CelScalar.UINT, (lhs, rhs) => { if (rhs.value === 0n) { throw divisionByZero(CelScalar.UINT); } @@ -230,13 +238,13 @@ const divide = celFunc(opc.DIVIDE, [ ]); const modulo = celFunc(opc.MODULO, [ - celOverload([CelScalar.INT, CelScalar.INT], CelScalar.INT, (lhs, rhs) => { + celOverload(olc.MODULO_INT64, [CelScalar.INT, CelScalar.INT], CelScalar.INT, (lhs, rhs) => { if (rhs === 0n) { throw moduloByZero(CelScalar.INT); } return lhs % rhs; }), - celOverload([CelScalar.UINT, CelScalar.UINT], CelScalar.UINT, (lhs, rhs) => { + celOverload(olc.MODULO_UINT64, [CelScalar.UINT, CelScalar.UINT], CelScalar.UINT, (lhs, rhs) => { if (rhs.value === 0n) { throw moduloByZero(CelScalar.UINT); } @@ -245,14 +253,14 @@ const modulo = celFunc(opc.MODULO, [ ]); const negate = celFunc(opc.NEGATE, [ - celOverload([CelScalar.INT], CelScalar.INT, (arg) => { + celOverload(olc.NEGATE_INT64, [CelScalar.INT], CelScalar.INT, (arg) => { const val = -arg; if (isOverflowInt(val)) { throw overflow(opc.NEGATE, CelScalar.INT); } return val; }), - celOverload([CelScalar.DOUBLE], CelScalar.DOUBLE, (arg) => -arg), + celOverload(olc.NEGATE_DOUBLE, [CelScalar.DOUBLE], CelScalar.DOUBLE, (arg) => -arg), ]); function overflow(op: string, type: CelType) { diff --git a/packages/cel/src/std/time.ts b/packages/cel/src/std/time.ts index 12368935..e612cc82 100644 --- a/packages/cel/src/std/time.ts +++ b/packages/cel/src/std/time.ts @@ -15,7 +15,7 @@ import { timestampDate, TimestampSchema } from "@bufbuild/protobuf/wkt"; import { CelScalar, TIMESTAMP, DURATION, type CelValue } from "../type.js"; -import { type FuncRegistry, celOverload, celFunc } from "../func.js"; +import { type FuncRegistry, celFunc, celMemberOverload } from "../func.js"; import * as olc from "../gen/dev/cel/expr/overload_const.js"; import { toJson } from "@bufbuild/protobuf"; @@ -158,12 +158,14 @@ function makeTimeOp(t: TimeFunc) { type TimeFunc = (date: Date) => number; const getFullYearFunc = celFunc(olc.TIME_GET_FULL_YEAR, [ - celOverload( + celMemberOverload( + olc.TIMESTAMP_TO_YEAR, [TIMESTAMP], CelScalar.INT, makeTimeOp((d) => d.getFullYear()), ), - celOverload( + celMemberOverload( + olc.TIMESTAMP_TO_YEAR_WITH_TZ, [TIMESTAMP, CelScalar.STRING], CelScalar.INT, makeTimeOp((d) => d.getFullYear()), @@ -171,12 +173,14 @@ const getFullYearFunc = celFunc(olc.TIME_GET_FULL_YEAR, [ ]); const getMonthFunc = celFunc(olc.TIME_GET_MONTH, [ - celOverload( + celMemberOverload( + olc.TIMESTAMP_TO_MONTH, [TIMESTAMP], CelScalar.INT, makeTimeOp((d) => d.getMonth()), ), - celOverload( + celMemberOverload( + olc.TIMESTAMP_TO_MONTH_WITH_TZ, [TIMESTAMP, CelScalar.STRING], CelScalar.INT, makeTimeOp((d) => d.getMonth()), @@ -184,12 +188,14 @@ const getMonthFunc = celFunc(olc.TIME_GET_MONTH, [ ]); const getDateFunc = celFunc(olc.TIME_GET_DATE, [ - celOverload( + celMemberOverload( + olc.TIMESTAMP_TO_DAY_OF_MONTH_ONE_BASED, [TIMESTAMP], CelScalar.INT, makeTimeOp((d) => d.getDate()), ), - celOverload( + celMemberOverload( + olc.TIMESTAMP_TO_DAY_OF_MONTH_ONE_BASED_WITH_TZ, [TIMESTAMP, CelScalar.STRING], CelScalar.INT, makeTimeOp((d) => d.getDate()), @@ -197,12 +203,14 @@ const getDateFunc = celFunc(olc.TIME_GET_DATE, [ ]); const getDayOfMonthFunc = celFunc(olc.TIME_GET_DAY_OF_MONTH, [ - celOverload( + celMemberOverload( + olc.TIMESTAMP_TO_DAY_OF_MONTH_ZERO_BASED, [TIMESTAMP], CelScalar.INT, makeTimeOp((d) => d.getDate() - 1), ), - celOverload( + celMemberOverload( + olc.TIMESTAMP_TO_DAY_OF_MONTH_ZERO_BASED_WITH_TZ, [TIMESTAMP, CelScalar.STRING], CelScalar.INT, makeTimeOp((d) => d.getDate() - 1), @@ -210,12 +218,14 @@ const getDayOfMonthFunc = celFunc(olc.TIME_GET_DAY_OF_MONTH, [ ]); const getDayOfWeekFunc = celFunc(olc.TIME_GET_DAY_OF_WEEK, [ - celOverload( + celMemberOverload( + olc.TIMESTAMP_TO_DAY_OF_WEEK, [TIMESTAMP], CelScalar.INT, makeTimeOp((d) => d.getDay()), ), - celOverload( + celMemberOverload( + olc.TIMESTAMP_TO_DAY_OF_WEEK_WITH_TZ, [TIMESTAMP, CelScalar.STRING], CelScalar.INT, makeTimeOp((d) => d.getDay()), @@ -223,12 +233,14 @@ const getDayOfWeekFunc = celFunc(olc.TIME_GET_DAY_OF_WEEK, [ ]); const getDayOfYearFunc = celFunc(olc.TIME_GET_DAY_OF_YEAR, [ - celOverload( + celMemberOverload( + olc.TIMESTAMP_TO_DAY_OF_YEAR, [TIMESTAMP], CelScalar.INT, makeTimeOp((d) => getDayOfYear(d)), ), - celOverload( + celMemberOverload( + olc.TIMESTAMP_TO_DAY_OF_YEAR_WITH_TZ, [TIMESTAMP, CelScalar.STRING], CelScalar.INT, makeTimeOp((d) => getDayOfYear(d)), @@ -236,59 +248,83 @@ const getDayOfYearFunc = celFunc(olc.TIME_GET_DAY_OF_YEAR, [ ]); const getSecondsFunc = celFunc(olc.TIME_GET_SECONDS, [ - celOverload( + celMemberOverload( + olc.TIMESTAMP_TO_SECONDS, [TIMESTAMP], CelScalar.INT, makeTimeOp((d) => d.getSeconds()), ), - celOverload( + celMemberOverload( + olc.TIMESTAMP_TO_SECONDS_WITH_TZ, [TIMESTAMP, CelScalar.STRING], CelScalar.INT, makeTimeOp((d) => d.getSeconds()), ), - celOverload([DURATION], CelScalar.INT, (dur) => dur.message.seconds), + celMemberOverload( + olc.DURATION_TO_SECONDS, + [DURATION], + CelScalar.INT, + (dur) => dur.message.seconds + ), ]); const getMinutesFunc = celFunc(olc.TIME_GET_MINUTES, [ - celOverload( + celMemberOverload( + olc.TIMESTAMP_TO_MINUTES, [TIMESTAMP], CelScalar.INT, makeTimeOp((d) => d.getMinutes()), ), - celOverload( + celMemberOverload( + olc.TIMESTAMP_TO_MINUTES_WITH_TZ, [TIMESTAMP, CelScalar.STRING], CelScalar.INT, makeTimeOp((d) => d.getMinutes()), ), - celOverload([DURATION], CelScalar.INT, (dur) => dur.message.seconds / 60n), + celMemberOverload( + olc.DURATION_TO_MINUTES, + [DURATION], + CelScalar.INT, + (dur) => dur.message.seconds / 60n + ), ]); const getHoursFunc = celFunc(olc.TIME_GET_HOURS, [ - celOverload( + celMemberOverload( + olc.TIMESTAMP_TO_HOURS, [TIMESTAMP], CelScalar.INT, makeTimeOp((d) => d.getHours()), ), - celOverload( + celMemberOverload( + olc.TIMESTAMP_TO_HOURS_WITH_TZ, [TIMESTAMP, CelScalar.STRING], CelScalar.INT, makeTimeOp((d) => d.getHours()), ), - celOverload([DURATION], CelScalar.INT, (dur) => dur.message.seconds / 3600n), + celMemberOverload( + olc.DURATION_TO_HOURS, + [DURATION], + CelScalar.INT, + (dur) => dur.message.seconds / 3600n + ), ]); const getMillisecondsFunc = celFunc(olc.TIME_GET_MILLISECONDS, [ - celOverload( + celMemberOverload( + olc.TIMESTAMP_TO_MILLISECONDS, [TIMESTAMP], CelScalar.INT, makeTimeOp((d) => d.getMilliseconds()), ), - celOverload( + celMemberOverload( + olc.TIMESTAMP_TO_MILLISECONDS_WITH_TZ, [TIMESTAMP, CelScalar.STRING], CelScalar.INT, makeTimeOp((d) => d.getMilliseconds()), ), - celOverload( + celMemberOverload( + olc.DURATION_TO_MILLISECONDS, [DURATION], CelScalar.INT, (dur) => BigInt(dur.message.nanos) / 1000000n, diff --git a/packages/cel/src/type.ts b/packages/cel/src/type.ts index 6cd615e5..c5d97fa1 100644 --- a/packages/cel/src/type.ts +++ b/packages/cel/src/type.ts @@ -12,7 +12,7 @@ // See the License for the specific language governing permissions and // limitations under the License. -import type { DescMessage, Message, MessageShape } from "@bufbuild/protobuf"; +import { ScalarType, type DescField, type DescMessage, type Message, type MessageShape } from "@bufbuild/protobuf"; import { isCelList, type CelList } from "./list.js"; import { isCelMap, type CelMap } from "./map.js"; import { isCelUint, type CelUint } from "./uint.js"; @@ -22,7 +22,8 @@ import { type ReflectMap, type ReflectMessage, } from "@bufbuild/protobuf/reflect"; -import { TimestampSchema, DurationSchema } from "@bufbuild/protobuf/wkt"; +import { TimestampSchema, DurationSchema, AnySchema } from "@bufbuild/protobuf/wkt"; +import { isCelError, type CelError } from "./error.js"; const privateSymbol = Symbol.for("@bufbuild/cel/type"); @@ -36,7 +37,14 @@ export type CelType = | CelMapType | CelObjectType | CelTypeType - | CelScalarType; + | CelScalarType + | CelErrorType + | CelOpaqueType + | CelTypeParamType; + +export type CelNullableType = T & { + readonly wrapped: T; +} /** * Scalar CEL value types. @@ -99,6 +107,24 @@ export interface CelObjectType readonly name: Desc["typeName"]; } +export interface CelErrorType extends celTypeShared { + readonly kind: "error"; + readonly name: "error"; + readonly error: CelError; +} + +export interface CelOpaqueType + extends celTypeShared { + readonly kind: "opaque"; + readonly name: string; + readonly parameters: T[] +} + +export interface CelTypeParamType extends celTypeShared { + readonly kind: "type_param"; + readonly name: string; +} + interface celTypeShared { [privateSymbol]: unknown; /** @@ -186,6 +212,67 @@ export function objectType( }; } +export function errorType(error: CelError): CelErrorType { + return { + [privateSymbol]: {}, + kind: "error", + name: "error", + error, + toString() { + return error.message; + }, + }; +} + +export function opaqueType< + const T extends CelType = CelType, +>( + name: string, + parameters: T[], +): CelOpaqueType { + return { + [privateSymbol]: {}, + kind: "opaque", + name, + parameters, + toString() { + if (parameters.length === 0) { + return name; + } + return `${name}(${parameters.map((p) => p.toString()).join(", ")})`; + }, + }; +} + +export function typeParamType( + name: string, +): CelTypeParamType { + return { + [privateSymbol]: {}, + kind: "type_param", + name, + toString() { + return name; + } + } +} + +/** + * TypeTypeWithParam creates a type with a type parameter. + * Used for type-checking purposes, but equivalent to TypeType otherwise. + */ +export function typeParamTypeWithParam(param: CelType): CelTypeType { + return { + [privateSymbol]: {}, + kind: "type", + type: param, + name: "type", + toString() { + return `type(${param.toString()})`; + }, + }; +} + function celScalarType< const S extends | "int" @@ -209,7 +296,7 @@ function celScalarType< } as const; } -type mapKeyType = +export type mapKeyType = | typeof CelScalar.INT | typeof CelScalar.UINT | typeof CelScalar.BOOL @@ -290,6 +377,8 @@ export function celType(v: CelValue): CelType { return mapType(CelScalar.DYN, CelScalar.DYN); case isCelUint(v): return CelScalar.UINT; + case isCelError(v): + return errorType(v); default: // This can also be a case statement, but TS fails to // narrow the type. @@ -311,3 +400,245 @@ export function isCelType(v: unknown): v is CelType { export function isObjectCelType(v: NonNullable): v is CelType { return privateSymbol in v; } + +export function isDynCelType(v: CelType): v is typeof CelScalar.DYN | CelObjectType { + switch (v.kind) { + case 'scalar': + return v.scalar === 'dyn'; + case 'object': + return v.desc.typeName === AnySchema.typeName; + default: + return false; + } +} + +export function isErrorCelType(v: CelType): v is CelErrorType { + return isCelType(v) && v.kind === 'error'; +} + +export function isDynOrErrorCelType(v: CelType): v is CelErrorType | typeof CelScalar.DYN | CelObjectType { + return isErrorCelType(v) || isDynCelType(v); +} + +export function optionalCelType(paramType: CelType): CelOpaqueType { + return opaqueType('optional_type', [paramType]); +} + +export function isOptionalCelType(v: CelType): v is CelOpaqueType { + switch (v.kind) { + case 'opaque': + return v.name === 'optional_type' + default: + return false; + } +} + +export function maybeUnwrapOptionalCelType(v: CelType): CelType { + if (isOptionalCelType(v)) { + return v.parameters[0]; + } + return v; +} + +/** + * Creates an instance of a nullable type with the provided wrapped type. + * + * Note: only primitive types are supported as wrapped types. + */ +export function nullableType(paramType: CelType): CelNullableType { + return { + ...paramType, + wrapped: paramType, + }; +} + +export function isNullableCelType(v: CelType): v is CelNullableType { + return isCelType(v) && 'wrapped' in v && isCelType(v.wrapped); +} + +/** + * isExactType indicates whether the two types are exactly the same. This + * check also verifies type parameter type names. + */ +export function isExactCelType(self: CelType, other: CelType): boolean { + return _isTypeInternal(self, other, true); +} + +/** + * isEquivalentType indicates whether two types are equivalent. This check + * ignores type parameter type names. + */ +export function isEquivalentCelType(self: CelType, other: CelType): boolean { + return _isTypeInternal(self, other, false); +} + +function _isTypeInternal(self: CelType, other: CelType, checkTypeParamName: boolean): boolean { + if (self === other) { + return true; + } + if (self.kind !== other.kind) { + return false; + } + if ( + (checkTypeParamName || self.kind != 'type') && + self.name != other.name + ) { + return false; + } + switch (self.kind) { + case 'list': + return _isTypeInternal(self.element, (other as CelListType).element, checkTypeParamName); + case 'map': + return _isTypeInternal(self.key, (other as CelMapType).key, checkTypeParamName) && + _isTypeInternal(self.value, (other as CelMapType).value, checkTypeParamName); + case 'type': + return _isTypeInternal(self.type, (other as CelTypeType).type, checkTypeParamName); + case 'object': + return self.desc.typeName === (other as CelObjectType).desc.typeName; + case 'scalar': + return (self as CelScalarType).scalar === (other as CelScalarType).scalar; + default: + return false; + } +} + +/** + * A mapping of Protobuf Scalar types to CEL types. + */ +// biome-ignore format: indented for readability +export const ProtoScalarCELPrimitives = { + [ScalarType.BOOL]: CelScalar.BOOL, + [ScalarType.BYTES]: CelScalar.BYTES, + [ScalarType.DOUBLE]: CelScalar.DOUBLE, + [ScalarType.FLOAT]: CelScalar.DOUBLE, + [ScalarType.INT32]: CelScalar.INT, + [ScalarType.INT64]: CelScalar.INT, + [ScalarType.SINT32]: CelScalar.INT, + [ScalarType.SINT64]: CelScalar.INT, + [ScalarType.UINT32]: CelScalar.UINT, + [ScalarType.UINT64]: CelScalar.UINT, + [ScalarType.FIXED32]: CelScalar.UINT, + [ScalarType.FIXED64]: CelScalar.UINT, + [ScalarType.SFIXED32]: CelScalar.INT, + [ScalarType.SFIXED64]: CelScalar.INT, + [ScalarType.STRING]: CelScalar.STRING, +} as const; + +export const CheckedWellKnownCELTypes: Record = { + // Wrapper types. + "google.protobuf.BoolValue": nullableType(CelScalar.BOOL), + "google.protobuf.BytesValue": nullableType(CelScalar.BYTES), + "google.protobuf.DoubleValue": nullableType(CelScalar.DOUBLE), + "google.protobuf.FloatValue": nullableType(CelScalar.DOUBLE), + "google.protobuf.Int64Value": nullableType(CelScalar.INT), + "google.protobuf.Int32Value": nullableType(CelScalar.INT), + "google.protobuf.UInt64Value": nullableType(CelScalar.UINT), + "google.protobuf.UInt32Value": nullableType(CelScalar.UINT), + "google.protobuf.StringValue": nullableType(CelScalar.STRING), + // Well-known types. + 'google.protobuf.Any': objectType(AnySchema), + 'google.protobuf.Timestamp': TIMESTAMP, + 'google.protobuf.Duration': DURATION, + // Json types. + "google.protobuf.ListValue": listType(CelScalar.DYN), + "google.protobuf.NullValue": CelScalar.NULL, + "google.protobuf.Struct": mapType(CelScalar.STRING, CelScalar.DYN), + "google.protobuf.Value": CelScalar.DYN, +} as const; + +export function fieldDescToCelType(field: DescField) { + switch (field.fieldKind) { + case 'scalar': + return ProtoScalarCELPrimitives[field.scalar] + case 'enum': + return CelScalar.INT; + case 'message': + if (CheckedWellKnownCELTypes[field.message.typeName]) { + return CheckedWellKnownCELTypes[field.message.typeName]; + } + return objectType(field.message); + case 'list': + switch (field.listKind) { + case 'enum': + return listType(CelScalar.INT); + case 'message': + return listType(objectType(field.message)); + case 'scalar': + return listType(ProtoScalarCELPrimitives[field.scalar]); + } + case 'map': + const keyType = ProtoScalarCELPrimitives[field.mapKey]; + switch (field.mapKind) { + case 'enum': + return mapType(keyType, CelScalar.INT); + case 'message': + return mapType(keyType, objectType(field.message)); + case 'scalar': + return mapType(keyType, ProtoScalarCELPrimitives[field.scalar]); + } + } +} + +/** + * isAssignableType determines whether the current type is type-check assignable from the input fromType. + */ +export function isAssignableType(t: CelType, fromType: CelType): boolean { + if (isExactCelType(t, CelScalar.NULL)) { + return isAssignableType(CelScalar.NULL, fromType); + } + if (isNullableCelType(t)) { + return isAssignableType(t.wrapped, fromType); + } + return defaultIsAssignableType(t, fromType); +} + + +/** + * defaultIsAssignableType provides the standard definition of what it means for one type to be assignable to another + * where any of the following may return a true result: + * - The from types are the same instance + * - The target type is dynamic + * - The fromType has the same kind and type name as the target type, and all parameters of the target type + * are isAssignableType() from the parameters of the fromType. + */ +function defaultIsAssignableType(t: CelType, fromType: CelType): boolean { + if (t === fromType || isDynCelType(t)) { + return true; + } + if (t.kind !== fromType.kind || t.name !== fromType.name) { + return false; + } + switch (t.kind) { + case 'list': + if (fromType.kind !== 'list') { + return false; + } + return isAssignableType(t.element, fromType.element); + case 'map': + if (fromType.kind !== 'map') { + return false; + } + return isAssignableType(t.key, fromType.key) && + isAssignableType(t.value, fromType.value); + case 'type': + if (fromType.kind !== 'type') { + return false; + } + return isAssignableType(t.type, fromType.type); + case 'opaque': + if (fromType.kind !== 'opaque') { + return false; + } + if (t.parameters.length !== fromType.parameters.length) { + return false; + } + for (let i = 0; i < t.parameters.length; i++) { + if (!isAssignableType(t.parameters[i], fromType.parameters[i])) { + return false; + } + } + return true; + default: + return true; + } +} \ No newline at end of file diff --git a/packages/example/src/example.ts b/packages/example/src/example.ts index e14e05c0..8a446ebc 100644 --- a/packages/example/src/example.ts +++ b/packages/example/src/example.ts @@ -16,7 +16,7 @@ import { celEnv, CelScalar, celFunc, - celOverload, + celMemberOverload, parse, plan, run, @@ -51,7 +51,9 @@ console.log(result); // true // Provide a new function: const similar = celFunc("similar", [ - celOverload( + celMemberOverload( + // Overload ID. + 'similar_string', // Parameter types. [CelScalar.STRING, CelScalar.STRING], // Return type. From c80362d5c44a524d950759cd7006402f2abfbaf9 Mon Sep 17 00:00:00 2001 From: jafaircl Date: Thu, 27 Nov 2025 08:38:38 -0500 Subject: [PATCH 2/5] add comprehension handling --- packages/cel/src/checker/checker.test.ts | 59 ++++++ packages/cel/src/checker/checker.ts | 238 +++++++++++++++++++++-- packages/cel/src/checker/env.ts | 38 +++- packages/cel/src/func.ts | 3 + packages/cel/src/std/logic.ts | 155 +++++++++------ 5 files changed, 410 insertions(+), 83 deletions(-) diff --git a/packages/cel/src/checker/checker.test.ts b/packages/cel/src/checker/checker.test.ts index 0099e073..b0980178 100644 --- a/packages/cel/src/checker/checker.test.ts +++ b/packages/cel/src/checker/checker.test.ts @@ -344,6 +344,34 @@ void suite("checker", () => { void test("call expr", () => { const cases = [ + { + expr: parse("is == 'str'"), + want: CelScalar.BOOL, + }, + { + expr: parse("ii + 1"), + want: CelScalar.INT, + }, + { + expr: parse("iu + 1u"), + want: CelScalar.UINT, + }, + { + expr: parse("id + 1.0"), + want: CelScalar.DOUBLE, + }, + { + expr: parse("iz && true"), + want: CelScalar.BOOL, + }, + { + expr: parse("iz || false"), + want: CelScalar.BOOL, + }, + { + expr: parse("!iz"), + want: CelScalar.BOOL, + }, { expr: parse('fg_s()'), want: CelScalar.STRING, @@ -382,4 +410,35 @@ void suite("checker", () => { assert.equal(got.toString(), c.want.toString(), `case ${c.expr}`); } }); + + void test('macros', () => { + const cases = [ + { + expr: parse('[1, 2, 3].all(e, e > 0)'), + want: CelScalar.BOOL, + }, + { + expr: parse('[1, 2, 3].exists(e, e > 0)'), + want: CelScalar.BOOL, + }, + { + expr: parse('[1, 2, 3].exists_one(e, e > 2)'), + want: CelScalar.BOOL, + }, + { + expr: parse('[1, 2, 3].filter(e, e > 2)'), + // TODO: other implementations can type this correctly + want: listType(CelScalar.DYN), + }, + { + expr: parse('[1, 2, 3].map(e, e + 1)'), + // TODO: other implementations can type this correctly + want: listType(CelScalar.DYN), + }, + ] + for (const c of cases) { + const got = internal__checkForTest(c.expr.expr!); + assert.equal(got.toString(), c.want.toString(), `case ${c.expr}`); + } + }) }); diff --git a/packages/cel/src/checker/checker.ts b/packages/cel/src/checker/checker.ts index 1de461ba..0980a0ac 100644 --- a/packages/cel/src/checker/checker.ts +++ b/packages/cel/src/checker/checker.ts @@ -2,7 +2,13 @@ import { AggregateLiteralElementType, type CelCheckerEnv } from "./env.js"; import { Mapping } from "./mapping.js"; import type { CheckedExpr } from "@bufbuild/cel-spec/cel/expr/checked_pb.js"; import { CheckedExprSchema } from "@bufbuild/cel-spec/cel/expr/checked_pb.js"; -import type { Expr } from "@bufbuild/cel-spec/cel/expr/syntax_pb.js"; +import { + Expr_CallSchema, + Expr_CreateStructSchema, + Expr_IdentSchema, + type Expr, + type Expr_CreateStruct, +} from "@bufbuild/cel-spec/cel/expr/syntax_pb.js"; import { create } from "@bufbuild/protobuf"; import { celError, type CelError } from "../error.js"; import { @@ -46,6 +52,7 @@ import { LOGICAL_OR, OPT_SELECT, } from "../gen/dev/cel/expr/operator_const.js"; +import { celVariable } from "../ident.js"; export interface CelChecker { // @@ -57,7 +64,7 @@ export interface CelChecker { */ export function check(expr: Expr, env: CelCheckerEnv): CheckedExpr { const checker = new _CelChecker(env); - checker.checkExpr(expr); + checker.check(expr); return create(CheckedExprSchema, { expr, // TODO: typeMap, referenceMap, sourceInfo @@ -84,7 +91,16 @@ export class _CelChecker implements CelChecker { mappings: Mapping = new Mapping(); freeTypeVarCounter = 0; - constructor(private readonly env: CelCheckerEnv) {} + constructor(private env: CelCheckerEnv) {} + + check(expr: Expr): void { + this.checkExpr(expr); + // Walk over the final type map substituting any type parameters either by their bound value + // or by DYN. + for (const [id, t] of this.typeMap.entries()) { + this.typeMap.set(id, substitute(this.mappings, t, true)); + } + } checkExpr(expr: Expr): void { switch (expr.exprKind.case) { @@ -94,7 +110,7 @@ export class _CelChecker implements CelChecker { return this.checkIdentExpr(expr); case "selectExpr": return this.checkSelectExpr(expr); - case 'callExpr': + case "callExpr": return this.checkCallExpr(expr); case "listExpr": return this.checkCreateListExpr(expr); @@ -103,6 +119,8 @@ export class _CelChecker implements CelChecker { return this.checkCreateMapExpr(expr); } return this.checkCreateStructExpr(expr); + case "comprehensionExpr": + return this.checkComprehensionExpr(expr); default: throw new Error(`unexpected expression kind: ${expr.exprKind.case}`); } @@ -151,9 +169,11 @@ export class _CelChecker implements CelChecker { if (found) { this.setType(expr, found.type); this.setReference(expr, identReference(found.name, found.value)); - // TODO: - // // Overwrite the identifier with its fully qualified name. - // e.SetKindCase(c.NewIdent(e.ID(), ident.Name())) + // Overwrite the identifier with its fully qualified name. + expr.exprKind = { + case: "identExpr", + value: create(Expr_IdentSchema, { name: found.name }), + }; return; } const error = celError( @@ -184,8 +204,10 @@ export class _CelChecker implements CelChecker { // variable name. this.setType(expr, ident.type); this.setReference(expr, identReference(ident.name, ident.value)); - // TODO: - // e.SetKindCase(c.NewIdent(e.ID(), ident.Name())) + expr.exprKind = { + case: "identExpr", + value: create(Expr_IdentSchema, { name: ident.name }), + }; return; } } @@ -343,9 +365,14 @@ export class _CelChecker implements CelChecker { this.setType(expr, errorType(err)); return; } - // TODO: - // // Overwrite the function name with its fully qualified resolved name. - // e.SetKindCase(c.NewCall(e.ID(), fn.Name(), args...)) + // Overwrite the function name with its fully qualified resolved name. + expr.exprKind = { + case: "callExpr", + value: create(Expr_CallSchema, { + function: fn.name, + args: args, + }), + }; // Check to see whether the overload resolves. this.resolveOverloadOrError(expr, fn, undefined, args); return; @@ -365,8 +392,13 @@ export class _CelChecker implements CelChecker { // The function name is namespaced and so preserving the target operand would // be an inaccurate representation of the desired evaluation behavior. // Overwrite with fully-qualified resolved function name sans receiver target. - // TODO: - // e.SetKindCase(c.NewCall(e.ID(), fn.Name(), args...)) + expr.exprKind = { + case: "callExpr", + value: create(Expr_CallSchema, { + function: fn.name, + args: args, + }), + }; this.resolveOverloadOrError(expr, fn, undefined, args); } } @@ -626,7 +658,7 @@ export class _CelChecker implements CelChecker { this.errors.push(celError(`expected structExpr`, expr.id)); return; } - const msgVal = expr.exprKind.value; + let msgVal = expr.exprKind.value; // Determine the type of the message. let resultType: CelType = errorType( celError(`'${msgVal.messageName}' is not a message type`, expr.id) @@ -645,11 +677,16 @@ export class _CelChecker implements CelChecker { } // Ensure the type name is fully qualified in the AST. let typeName = ident.name; - // TODO: - // if msgVal.TypeName() != typeName { - // e.SetKindCase(c.NewStruct(e.ID(), typeName, msgVal.Fields())) - // msgVal = e.AsStruct() - // } + if (msgVal.messageName !== typeName) { + expr.exprKind = { + case: "structExpr", + value: create(Expr_CreateStructSchema, { + messageName: typeName, + entries: msgVal.entries, + }), + }; + msgVal = expr.exprKind.value as Expr_CreateStruct; + } this.setReference(expr, identReference(typeName, undefined)); const identKind = ident.type.kind; if (identKind !== "error") { @@ -725,6 +762,149 @@ export class _CelChecker implements CelChecker { } } + checkComprehensionExpr(expr: Expr): void { + if (expr.exprKind.case !== "comprehensionExpr") { + this.errors.push(celError(`expected comprehensionExpr`, expr.id)); + return; + } + const comp = expr.exprKind.value; + if (!comp.iterRange) { + // This should not happen, anyway, report an error. + this.errors.push(celError(`expected comprehension iter_range`, expr.id)); + return; + } + this.checkExpr(comp.iterRange); + if (!comp.accuInit) { + // This should not happen, anyway, report an error. + this.errors.push(celError(`expected comprehension accu_init`, expr.id)); + return; + } + this.checkExpr(comp.accuInit); + let rangeType = this.getType(comp.iterRange); + if (!rangeType) { + // This should not happen, anyway, report an error. + this.errors.push( + celError( + `unable to determine type of comprehension iter_range`, + expr.id + ) + ); + return; + } + rangeType = substitute(this.mappings, rangeType, false); + + // Create a scope for the comprehension since it has a local accumulation variable. + // This scope will contain the accumulation variable used to compute the result. + const accuType = this.getType(comp.accuInit); + if (!accuType) { + // This should not happen, anyway, report an error. + this.errors.push( + celError(`unable to determine type of comprehension accu_init`, expr.id) + ); + return; + } + this.env = this.env.enterScope(); + this.env.addIdents([celVariable(comp.accuVar, accuType)]); + + let varType: CelType | undefined; + let var2Type: CelType | undefined; + switch (rangeType.kind) { + case "list": + // varType represents the list element type for one-variable comprehensions. + varType = rangeType.element; + if (comp.iterVar2) { + // varType represents the list index (int) for two-variable comprehensions, + // and var2Type represents the list element type. + var2Type = varType; + varType = CelScalar.INT; + } + break; + case "map": + // varType represents the map entry key for all comprehension types. + varType = rangeType.key; + if (comp.iterVar2) { + // var2Type represents the map entry value for two-variable comprehensions. + var2Type = rangeType.value; + } + break; + case "error": + case "type_param": + case "scalar": + if (rangeType.kind === "scalar" && rangeType.scalar !== "dyn") { + const err = celError( + `expression of type '${rangeType.kind}' cannot be range of a comprehension (must be list, map, or dynamic)` + ); + this.errors.push(err); + varType = errorType(err); + if (comp.iterVar2) { + var2Type = errorType(err); + } + break; + } + // Set the range type to DYN to prevent assignment to a potentially incorrect type + // at a later point in type-checking. The isAssignable call will update the type + // substitutions for the type param under the covers. + this.isAssignable(CelScalar.DYN, rangeType); + // Set the range iteration variable to type DYN as well. + varType = CelScalar.DYN; + if (comp.iterVar2) { + var2Type = CelScalar.DYN; + } + break; + default: + const err = celError( + `expression of type '${rangeType.kind}' cannot be range of a comprehension (must be list, map, or dynamic)`, + expr.id + ); + this.errors.push(err); + varType = errorType(err); + if (comp.iterVar2) { + var2Type = errorType(err); + } + break; + } + + // Create a block scope for the loop. + this.env = this.env.enterScope(); + this.env.addIdents([celVariable(comp.iterVar, varType)]); + if (comp.iterVar2) { + this.env.addIdents([celVariable(comp.iterVar2, var2Type!)]); + } + // Check the variable references in the condition and step. + if (!comp.loopCondition) { + this.errors.push( + celError(`expected comprehension loop_condition`, expr.id) + ); + return; + } + this.checkExpr(comp.loopCondition); + this.assertType(comp.loopCondition, CelScalar.BOOL); + if (!comp.loopStep) { + this.errors.push(celError(`expected comprehension loop_step`, expr.id)); + return; + } + this.checkExpr(comp.loopStep); + this.assertType(comp.loopStep, accuType); + // Exit the loop's block scope before checking the result. + this.env = this.env.exitScope(); + if (!comp.result) { + this.errors.push(celError(`expected comprehension result`, expr.id)); + return; + } + this.checkExpr(comp.result); + + // Exit the comprehension scope. + this.env = this.env.enterScope(); + const resultType = this.getType(comp.result); + if (!resultType) { + this.errors.push( + celError(`unable to determine type of comprehension result`, expr.id) + ); + return; + } + this.setType(expr, substitute(this.mappings, resultType, false)); + } + joinTypes( expr: Expr, previous: CelType | undefined, @@ -803,6 +983,24 @@ export class _CelChecker implements CelChecker { this.referenceMap.set(expr.id, ref); } + assertType(expr: Expr, t: CelType): void { + const exprType = this.getType(expr); + if (!exprType) { + this.errors.push( + celError(`unable to determine type of expression`, expr.id) + ); + return; + } + if (!this.isAssignable(t, exprType)) { + this.errors.push( + celError( + `expected type '${t.toString()}' but got '${exprType.toString()}'`, + expr.id + ) + ); + } + } + lookupFieldType( id: bigint, structType: string, diff --git a/packages/cel/src/checker/env.ts b/packages/cel/src/checker/env.ts index bb60cbc6..8713c2df 100644 --- a/packages/cel/src/checker/env.ts +++ b/packages/cel/src/checker/env.ts @@ -44,18 +44,44 @@ export interface CelCheckerEnv { * The filtered overload ids. */ readonly filteredOverloadIds: Set; - + /** + * AddIdents configures the checker with a list of variable declarations. + * + * If there are overlapping declarations, the method will error. + */ addIdents(idents: CelIdent[]): void; - + /** + * AddFunctions configures the checker with a list of function declarations. + * + * If there are overlapping declarations, the method will error. + */ addFunctions(funcs: CelFunc[]): void; - + /** + * LookupIdent returns an identifier in the Env. + * Returns undefined if no such identifier is found in the Env. + */ lookupIdent(name: string): CelIdent | undefined; - + /** + * LookupFunction returns a function declaration in the env. + * Returns undefined if no such function is found in the env. + */ lookupFunction(name: string): CelFunc | undefined; - + /** + * IsOverloadDisabled returns whether the overloadID is disabled in the current environment. + */ isOverloadDisabled(overloadID: string): boolean; - + /** + * validatedDeclarations returns a reference to the validated variable and function declaration scope stack. + */ validatedDeclarations(): Scopes; + /** + * enterScope creates a new Env instance with a new innermost declaration scope. + */ + enterScope(): CelCheckerEnv; + /** + * exitScope creates a new Env instance with the nearest outer declaration scope. + */ + exitScope(): CelCheckerEnv; } export interface CelCheckerEnvOptions { diff --git a/packages/cel/src/func.ts b/packages/cel/src/func.ts index 463961b0..7ab460ea 100644 --- a/packages/cel/src/func.ts +++ b/packages/cel/src/func.ts @@ -266,6 +266,9 @@ export class FuncRegistry implements Dispatcher { throw new Error("dispatch is required with name"); } this.addCall(nameOrFunc, call); + if (call instanceof Func) { + this.functionDeclarations.set(nameOrFunc, call); + } } /** diff --git a/packages/cel/src/std/logic.ts b/packages/cel/src/std/logic.ts index 50d6bc1b..dbc7a52d 100644 --- a/packages/cel/src/std/logic.ts +++ b/packages/cel/src/std/logic.ts @@ -16,7 +16,6 @@ import { type FuncRegistry, celFunc, celOverload, - type CallDispatch, celMemberOverload, } from "../func.js"; import * as opc from "../gen/dev/cel/expr/operator_const.js"; @@ -33,74 +32,115 @@ import { import { equals } from "../equals.js"; import type { CelMap } from "../map.js"; +// TODO: cel-go uses these instead of DYN for various functions +// const paramA = typeParamType("A") as CelType; +// const paramB = typeParamType("B") as CelType; +// const listOfA = listType(paramA); +// const mapOfAB = mapType(paramA as mapKeyType, paramB); + /** * This is not in the spec but is part of at least go,java, and cpp implementations. * * It should return true for anything exept for the literal `false`. */ -const notStrictlyFalse: CallDispatch = { - dispatch(_, args) { - const raw = args[0]; - if (isCelError(raw)) { - return true; - } - return raw !== false; - }, -}; +const notStrictlyFalse = celFunc(opc.NOT_STRICTLY_FALSE, [ + celOverload( + olc.NOT_STRICTLY_FALSE, + [CelScalar.BOOL], + CelScalar.BOOL, + (x) => true, // Irrelevant because we overwrite dispatch below + ) +]) +notStrictlyFalse.dispatch = (_, args) => { + const raw = args[0]; + if (isCelError(raw)) { + return true; + } + return raw !== false; +} + const notFunc = celFunc(opc.LOGICAL_NOT, [ celOverload(olc.LOGICAL_NOT, [CelScalar.BOOL], CelScalar.BOOL, (x) => !x), ]); -const and: CallDispatch = { - dispatch(_id, args) { - let allBools = true; - const errors: CelError[] = []; - for (let i = 0; i < args.length; i++) { - let arg = args[i]; - if (typeof arg === "boolean") { - if (!arg) return false; // short-circuit - } else { - allBools = false; - if (isCelError(arg)) { - errors.push(arg); - } +const and = celFunc(opc.LOGICAL_AND, [ + celOverload( + olc.LOGICAL_AND, + [CelScalar.BOOL, CelScalar.BOOL], + CelScalar.BOOL, + (x, y) => x && y, // Irrelevant because we overwrite dispatch below + ) +]) +and.dispatch = (_id, args) => { + let allBools = true; + const errors: CelError[] = []; + for (let i = 0; i < args.length; i++) { + let arg = args[i]; + if (typeof arg === "boolean") { + if (!arg) return false; // short-circuit + } else { + allBools = false; + if (isCelError(arg)) { + errors.push(arg); } } - if (allBools) { - return true; - } - if (errors.length > 0) { - return celErrorMerge(errors[0], ...errors.slice(1)); - } - return undefined; - }, -}; + } + if (allBools) { + return true; + } + if (errors.length > 0) { + return celErrorMerge(errors[0], ...errors.slice(1)); + } + return undefined; +} -const or: CallDispatch = { - dispatch(_, args) { - let allBools = true; - const errors: CelError[] = []; - for (let i = 0; i < args.length; i++) { - let arg = args[i]; - if (typeof arg === "boolean") { - if (arg) return true; // short-circuit - } else { - allBools = false; - if (isCelError(arg)) { - errors.push(arg); - } +const or = celFunc(opc.LOGICAL_OR, [ + celOverload( + olc.LOGICAL_OR, + [CelScalar.BOOL, CelScalar.BOOL], + CelScalar.BOOL, + (x, y) => x || y, // Irrelevant because we overwrite dispatch below + ) +]) +or.dispatch = (_, args) => { + let allBools = true; + const errors: CelError[] = []; + for (let i = 0; i < args.length; i++) { + let arg = args[i]; + if (typeof arg === "boolean") { + if (arg) return true; // short-circuit + } else { + allBools = false; + if (isCelError(arg)) { + errors.push(arg); } } - if (allBools) { - return false; - } - if (errors.length > 0) { - return celErrorMerge(errors[0], ...errors.slice(1)); - } - return undefined; - }, -}; + } + if (allBools) { + return false; + } + if (errors.length > 0) { + return celErrorMerge(errors[0], ...errors.slice(1)); + } + return undefined; +} + +/** + * This is not actually used by the planner since it handles conditionals + * directly, but it is defined here for type checking. + */ +const conditional = celFunc(opc.CONDITIONAL, [ + celOverload( + olc.CONDITIONAL, + [CelScalar.BOOL, CelScalar.DYN, CelScalar.DYN], + CelScalar.DYN, + (_cond, thenBranch, elseBranch) => { + // Irrelevant because dispatch is never called by the planner for conditionals + return _cond ? thenBranch : elseBranch; + }, + ), +]); const eqFunc = celFunc(opc.EQUALS, [ celOverload(olc.EQUALS, [CelScalar.DYN, CelScalar.DYN], CelScalar.BOOL, equals), @@ -367,10 +407,11 @@ const inFunc = celFunc(opc.IN, [ ]); export function addLogic(funcs: FuncRegistry) { - funcs.add(opc.NOT_STRICTLY_FALSE, notStrictlyFalse); - funcs.add(opc.LOGICAL_AND, and); - funcs.add(opc.LOGICAL_OR, or); + funcs.add(notStrictlyFalse); + funcs.add(and); + funcs.add(or); funcs.add(notFunc); + funcs.add(conditional); funcs.add(eqFunc); funcs.add(neFunc); funcs.add(ltFunc); From 83ad79408cda93fa260d6cbe77a962e0413d2ff5 Mon Sep 17 00:00:00 2001 From: jafaircl Date: Thu, 4 Dec 2025 08:21:00 -0500 Subject: [PATCH 3/5] fix macro typing --- packages/cel/src/checker/checker.test.ts | 36 +++- packages/cel/src/checker/env.ts | 8 + packages/cel/src/checker/types.ts | 4 +- packages/cel/src/func.ts | 3 - packages/cel/src/ident.ts | 12 +- packages/cel/src/provider.ts | 199 ----------------------- packages/cel/src/std/logic.ts | 63 ++++--- packages/cel/src/std/math.ts | 6 +- packages/cel/src/std/types.ts | 32 ++++ packages/cel/src/type.ts | 2 +- 10 files changed, 126 insertions(+), 239 deletions(-) delete mode 100644 packages/cel/src/provider.ts create mode 100644 packages/cel/src/std/types.ts diff --git a/packages/cel/src/checker/checker.test.ts b/packages/cel/src/checker/checker.test.ts index b0980178..908aafd8 100644 --- a/packages/cel/src/checker/checker.test.ts +++ b/packages/cel/src/checker/checker.test.ts @@ -360,6 +360,14 @@ void suite("checker", () => { expr: parse("id + 1.0"), want: CelScalar.DOUBLE, }, + { + expr: parse("[1, 2] + [3, 4]"), + want: listType(CelScalar.INT), + }, + { + expr: parse("[1.0, 2] + [3u, 4]"), + want: listType(CelScalar.DYN), + }, { expr: parse("iz && true"), want: CelScalar.BOOL, @@ -403,6 +411,22 @@ void suite("checker", () => { { expr: parse("is.contains('str')"), want: CelScalar.BOOL, + }, + { + expr: parse('["1", "2", "3"][3]'), + want: CelScalar.STRING, + }, + { + expr: parse('[1, 2.0, 3u, "4"][3]'), + want: CelScalar.DYN, + }, + { + expr: parse('{ 1: "2", 3: "4" }[1]'), + want: CelScalar.STRING, + }, + { + expr: parse('{ "1": 2, "3": 4.0 }["1"]'), + want: CelScalar.DYN, } ]; for (const c of cases) { @@ -427,13 +451,19 @@ void suite("checker", () => { }, { expr: parse('[1, 2, 3].filter(e, e > 2)'), - // TODO: other implementations can type this correctly + want: listType(CelScalar.INT), + }, + { + expr: parse('[1, 2.0, 3u].filter(e, e > 2)'), want: listType(CelScalar.DYN), }, { expr: parse('[1, 2, 3].map(e, e + 1)'), - // TODO: other implementations can type this correctly - want: listType(CelScalar.DYN), + want: listType(CelScalar.INT), + }, + { + expr: parse('[1, 2.0, 3u].map(e, e + 1)'), + want: listType(CelScalar.INT), }, ] for (const c of cases) { diff --git a/packages/cel/src/checker/env.ts b/packages/cel/src/checker/env.ts index 8713c2df..465c0842 100644 --- a/packages/cel/src/checker/env.ts +++ b/packages/cel/src/checker/env.ts @@ -8,6 +8,7 @@ import { celConstant, type CelIdent, celVariable } from "../ident.js"; import { createRegistryWithWKT } from "../registry.js"; import { CelScalar, objectType } from "../type.js"; import { STD_FUNCS } from "../std/std.js"; +import { STD_TYPES } from "../std/types.js"; const privateSymbol = Symbol.for("@bufbuild/cel/checker/env"); @@ -150,6 +151,13 @@ export function celCheckerEnv(options?: CelCheckerEnvOptions): CelCheckerEnv { idents.set(ident.name, ident); } } + for (const ident of STD_TYPES) { + // TODO: how do other implementations handle this? Can users overwrite std types? + if (idents.has(ident.name)) { + continue; + } + idents.set(ident.name, ident); + } const funcs = new Map(); for (const func of STD_FUNCS.declarations) { funcs.set(func.name, func); diff --git a/packages/cel/src/checker/types.ts b/packages/cel/src/checker/types.ts index 61002146..bd5ac9e5 100644 --- a/packages/cel/src/checker/types.ts +++ b/packages/cel/src/checker/types.ts @@ -12,7 +12,7 @@ import { type mapKeyType, mapType, opaqueType, - typeParamTypeWithParam, + typeTypeWithParam, } from "../type.js"; import { Mapping } from "./mapping.js"; @@ -334,7 +334,7 @@ export function substitute(m: Mapping, t: CelType, typeParamToDyn: boolean): Cel ); case "type": if (t.type) { - return typeParamTypeWithParam(substitute(m, t.type, typeParamToDyn)); + return typeTypeWithParam(substitute(m, t.type, typeParamToDyn)); } return t; default: diff --git a/packages/cel/src/func.ts b/packages/cel/src/func.ts index 7ab460ea..463961b0 100644 --- a/packages/cel/src/func.ts +++ b/packages/cel/src/func.ts @@ -266,9 +266,6 @@ export class FuncRegistry implements Dispatcher { throw new Error("dispatch is required with name"); } this.addCall(nameOrFunc, call); - if (call instanceof Func) { - this.functionDeclarations.set(nameOrFunc, call); - } } /** diff --git a/packages/cel/src/ident.ts b/packages/cel/src/ident.ts index 5a06c710..2e743254 100644 --- a/packages/cel/src/ident.ts +++ b/packages/cel/src/ident.ts @@ -1,4 +1,4 @@ -import { type CelValue, type CelType, isEquivalentCelType } from "./type.js"; +import { type CelValue, type CelType, isEquivalentCelType, typeTypeWithParam } from "./type.js"; const privateIdentSymbol = Symbol.for("@bufbuild/cel/ident"); @@ -34,6 +34,16 @@ export function celVariable( return new Ident(name, type, undefined, doc); } +/** + * Creates a new type identifier + */ +export function celTypeVariable( + type: CelType, + doc?: string +): CelIdent { + return new Ident(type.name, typeTypeWithParam(type), undefined, doc); +} + /** * Creates a new CelConstant. */ diff --git a/packages/cel/src/provider.ts b/packages/cel/src/provider.ts deleted file mode 100644 index 23dcef13..00000000 --- a/packages/cel/src/provider.ts +++ /dev/null @@ -1,199 +0,0 @@ -import { - type DescFile, - type DescMessage, - type MutableRegistry, - type Registry, - createMutableRegistry, -} from "@bufbuild/protobuf"; -import { type CelIdent } from "./ident.js"; -import { - CelScalar, - type CelType, - type CelValue, - DURATION, - fieldDescToCelType, - isEquivalentCelType, - listType, - mapType, - objectType, - TIMESTAMP, -} from "./type.js"; -import { celError } from "./error.js"; - -/** - * Provider specifies functions for creating new object instances and for resolving - * enum values by name. - */ -export interface Provider { - /** - * enumValue returns the numeric value of the given enum value name. - */ - enumValue(name: string): bigint | undefined; - - /** - * findIdent takes a qualified identifier name and returns a CelIdent if one exists. - */ - findIdent(ident: string): CelValue | undefined; - - /** - * findStructType returns the Descriptor given a qualified type name. - */ - findStructType(name: string): CelType | undefined; - - /** - * findStructFieldNames returns the set of field names for the given struct type, - * if the type exists in the registry. - */ - findStructFieldNames(typeName: string): string[] | undefined; - - /** - * findStructFieldType returns the field type for a checked type value. - */ - findStructFieldType(typeName: string, fieldName: string): CelType | undefined; - - // /** - // * newValue creates a new type value from a qualified name and map of field - // * name to value. - // */ - // newValue(typeName: string, fields: Record): CelValue | undefined; -} - -// TODO: this could probably be used by the planner as well -export class CelRegistry implements Provider { - protected revTypeMap = new Map(); - protected pbdb: MutableRegistry; - - constructor(public readonly idents: CelIdent[] = [], pbRegistry?: Registry) { - this.registerType( - CelScalar.BOOL, - CelScalar.BYTES, - CelScalar.DOUBLE, - DURATION, - CelScalar.INT, - listType(CelScalar.DYN), - mapType(CelScalar.DYN, CelScalar.DYN), - CelScalar.NULL, - CelScalar.STRING, - TIMESTAMP, - CelScalar.TYPE, - CelScalar.UINT, - ) - this.pbdb = pbRegistry - ? createMutableRegistry(pbRegistry) - : createMutableRegistry(); - for (const ident of idents) { - this.registerType(ident.type); - } - } - - /** - * copy copies the current state of the registry into its own memory space. - */ - copy(): CelRegistry { - const newReg = new CelRegistry([...this.idents], this.pbdb); - for (const [k, v] of this.revTypeMap) { - newReg.revTypeMap.set(k, v); - } - return newReg; - } - - enumValue(name: string): bigint | undefined { - const lastDot = name.lastIndexOf("."); - if (lastDot < 0) { - return undefined; - } - const enumName = name.substring(0, lastDot); - const enumValue = name.substring(lastDot + 1); - const _enum = this.pbdb.getEnum(enumName); - if (!_enum) { - return undefined; - } - const enumNumber = _enum.values.find((v) => v.name === enumValue); - if (!enumNumber) { - return undefined; - } - return BigInt(enumNumber.number); - } - - findIdent(identName: string): CelValue | undefined { - if (this.revTypeMap.has(identName)) { - return this.revTypeMap.get(identName); - } - const enumValue = this.enumValue(identName); - if (enumValue !== undefined) { - return enumValue; - } - return undefined; - } - - findStructType(typeName: string): CelType | undefined { - const struct = this.pbdb.getMessage(typeName); - if (struct) { - return objectType(struct); - } - return undefined; - } - - findStructFieldNames(typeName: string): string[] | undefined { - const struct = this.pbdb.getMessage(typeName); - if (!struct) { - return undefined; - } - return struct.fields.map((f) => f.name); - } - - findStructFieldType( - typeName: string, - fieldName: string - ): CelType | undefined { - const struct = this.pbdb.getMessage(typeName); - if (!struct) { - return undefined; - } - const field = struct.fields.find((f) => f.name === fieldName); - if (!field) { - return undefined; - } - return fieldDescToCelType(field); - } - - /** - * Registers the contents of a protocol buffer `FileDescriptor` - */ - registerDescriptor(fd: DescFile): void { - return this.registerAllTypes(fd); - } - - /** - * Registers a protocol buffer message and its dependencies. - */ - registerMessage(message: DescMessage): void { - this.pbdb.add(message); - return this.registerAllTypes(message.file); - } - - /** - * Registers a type value with the provider which ensures the provider is aware of how to - * map the type to an identifier. - */ - registerType(...types: CelType[]): void { - for (const type of types) { - if (!this.revTypeMap.has(type.name)) { - this.revTypeMap.set(type.name, type); - continue; - } - const existing = this.revTypeMap.get(type.name) as CelType; - if (!isEquivalentCelType(existing, type)) { - throw celError( - `type registration conflict. found: ${existing.toString()}, input: ${type.toString()}` - ); - } - } - } - - private registerAllTypes(fd: DescFile): void { - for (const msg of fd.messages) { - this.registerType(objectType(msg)); - } - } -} diff --git a/packages/cel/src/std/logic.ts b/packages/cel/src/std/logic.ts index dbc7a52d..658eb332 100644 --- a/packages/cel/src/std/logic.ts +++ b/packages/cel/src/std/logic.ts @@ -31,12 +31,7 @@ import { } from "../type.js"; import { equals } from "../equals.js"; import type { CelMap } from "../map.js"; - -// TODO: cel-go uses these instead of DYN for various functions -// const paramA = typeParamType("A") as CelType; -// const paramB = typeParamType("B") as CelType; -// const listOfA = listType(paramA); -// const mapOfAB = mapType(paramA as mapKeyType, paramB); +import { listOfA, mapOfAB, paramA, paramB } from "./types.js"; /** * This is not in the spec but is part of at least go,java, and cpp implementations. @@ -133,8 +128,8 @@ or.dispatch = (_, args) => { const conditional = celFunc(opc.CONDITIONAL, [ celOverload( olc.CONDITIONAL, - [CelScalar.BOOL, CelScalar.DYN, CelScalar.DYN], - CelScalar.DYN, + [CelScalar.BOOL, paramA, paramA], + paramA, (_cond, thenBranch, elseBranch) => { // Irrelevant because dispatch is never called by the planner for conditionals return _cond ? thenBranch : elseBranch; @@ -142,6 +137,31 @@ const conditional = celFunc(opc.CONDITIONAL, [ ), ]); +/** + * This is not actually used by the planner since it handles indexing directly, + * but it is defined here for type checking. + */ +const index = celFunc(opc.INDEX, [ + celOverload( + olc.INDEX_LIST, + [listOfA, CelScalar.INT], + paramA, + (lst, idx) => { + // Irrelevant because dispatch is never called by the planner for indexing + return lst.get(Number(idx)) ?? null; + }, + ), + celOverload( + olc.INDEX_MAP, + [mapOfAB, paramA], + paramB, + (mp, key) => { + // Irrelevant because dispatch is never called by the planner for indexing + return mp.get(key as string) ?? null; + }, + ) +]); + const eqFunc = celFunc(opc.EQUALS, [ celOverload(olc.EQUALS, [CelScalar.DYN, CelScalar.DYN], CelScalar.BOOL, equals), ]); @@ -341,24 +361,10 @@ const sizeFunc = celFunc(olc.SIZE, [ }), celOverload(olc.SIZE_BYTES, [CelScalar.BYTES], CelScalar.INT, (x) => BigInt(x.length)), celMemberOverload(olc.SIZE_BYTES_INST, [CelScalar.BYTES], CelScalar.INT, (x) => BigInt(x.length)), - celOverload(olc.SIZE_LIST, [listType(CelScalar.DYN)], CelScalar.INT, (x) => BigInt(x.size)), - celMemberOverload(olc.SIZE_LIST_INST, [listType(CelScalar.DYN)], CelScalar.INT, (x) => BigInt(x.size)), - // TODO: this may need to be one dyn like in other implementations - celOverload(olc.SIZE_MAP + '_int_key', [mapType(CelScalar.INT, CelScalar.DYN)], CelScalar.INT, (x) => - BigInt(x.size), - ), - celOverload(olc.SIZE_MAP + '_uint_key', [mapType(CelScalar.UINT, CelScalar.DYN)], CelScalar.INT, (x) => - BigInt(x.size), - ), - celOverload(olc.SIZE_MAP + '_bool_key', [mapType(CelScalar.BOOL, CelScalar.DYN)], CelScalar.INT, (x) => - BigInt(x.size), - ), - celOverload(olc.SIZE_MAP + '_string_key', [mapType(CelScalar.STRING, CelScalar.DYN)], CelScalar.INT, (x) => - BigInt(x.size), - ), - celMemberOverload(olc.SIZE_MAP_INST, [mapType(CelScalar.DYN, CelScalar.DYN)], CelScalar.INT, (x) => - BigInt(x.size), - ), + celOverload(olc.SIZE_LIST, [listOfA], CelScalar.INT, (x) => BigInt(x.size)), + celMemberOverload(olc.SIZE_LIST_INST, [listOfA], CelScalar.INT, (x) => BigInt(x.size)), + celOverload(olc.SIZE_MAP, [mapOfAB], CelScalar.INT, (x) => BigInt(x.size)), + celMemberOverload(olc.SIZE_MAP_INST, [mapOfAB], CelScalar.INT, (x) => BigInt(x.size)), ]); function mapInOp(x: CelValue, y: CelMap) { @@ -379,7 +385,9 @@ const inFunc = celFunc(opc.IN, [ return false; }, ), - // TODO: this may need to be one dyn like in other implementations + // TODO: other implementations use listOfA/mapOfAB here instead of having a separate + // overload for each key type. Switching it out causes some conformance tests to fail + // because the planner cannot resolve the overloads correctly. celOverload( olc.IN_MAP, [CelScalar.DYN, mapType(CelScalar.STRING, CelScalar.DYN)], @@ -412,6 +420,7 @@ export function addLogic(funcs: FuncRegistry) { funcs.add(or); funcs.add(notFunc); funcs.add(conditional); + funcs.add(index); funcs.add(eqFunc); funcs.add(neFunc); funcs.add(ltFunc); diff --git a/packages/cel/src/std/math.ts b/packages/cel/src/std/math.ts index 8e0505e5..e0d8af0c 100644 --- a/packages/cel/src/std/math.ts +++ b/packages/cel/src/std/math.ts @@ -21,7 +21,6 @@ import * as olc from "../gen/dev/cel/expr/overload_const.js"; import { CelScalar, DURATION, - listType, TIMESTAMP, type CelType, type CelValue, @@ -30,6 +29,7 @@ import { celListConcat } from "../list.js"; import { celUint } from "../uint.js"; import { createDuration } from "../duration.js"; import { createTimestamp } from "../timestamp.js"; +import { listOfA } from "./types.js"; const MAX_INT = 9223372036854775807n; // biome-ignore lint/correctness/noPrecisionLoss: No symbol exists in the std. @@ -153,8 +153,8 @@ const add = celFunc(opc.ADD, [ celOverload(olc.ADD_DURATION_DURATION, [DURATION, DURATION], DURATION, addDuration), celOverload( olc.ADD_LIST, - [listType(CelScalar.DYN), listType(CelScalar.DYN)], - listType(CelScalar.DYN), + [listOfA, listOfA], + listOfA, celListConcat, ), ]); diff --git a/packages/cel/src/std/types.ts b/packages/cel/src/std/types.ts new file mode 100644 index 00000000..38abc179 --- /dev/null +++ b/packages/cel/src/std/types.ts @@ -0,0 +1,32 @@ +import { celTypeVariable } from "../ident.js"; +import { + CelScalar, + DURATION, + listType, + mapType, + TIMESTAMP, + typeParamType, + typeType, + type CelType, + type mapKeyType, +} from "../type.js"; + +export const paramA = typeParamType("A") as CelType; +export const paramB = typeParamType("B") as CelType; +export const listOfA = listType(paramA); +export const mapOfAB = mapType(paramA as mapKeyType, paramB); + +export const STD_TYPES = [ + celTypeVariable(typeType(CelScalar.BOOL)), + celTypeVariable(typeType(CelScalar.BYTES)), + celTypeVariable(typeType(CelScalar.DOUBLE)), + celTypeVariable(typeType(DURATION)), + celTypeVariable(typeType(CelScalar.INT)), + celTypeVariable(typeType(listOfA)), + celTypeVariable(typeType(mapOfAB)), + celTypeVariable(typeType(CelScalar.NULL)), + celTypeVariable(typeType(CelScalar.STRING)), + celTypeVariable(typeType(TIMESTAMP)), + celTypeVariable(typeType(CelScalar.TYPE)), + celTypeVariable(typeType(CelScalar.UINT)), +]; diff --git a/packages/cel/src/type.ts b/packages/cel/src/type.ts index c5d97fa1..28622de4 100644 --- a/packages/cel/src/type.ts +++ b/packages/cel/src/type.ts @@ -261,7 +261,7 @@ export function typeParamType( * TypeTypeWithParam creates a type with a type parameter. * Used for type-checking purposes, but equivalent to TypeType otherwise. */ -export function typeParamTypeWithParam(param: CelType): CelTypeType { +export function typeTypeWithParam(param: CelType): CelTypeType { return { [privateSymbol]: {}, kind: "type", From 355c1a9eae72a45f24d315aa2b5038f3b7b61d1e Mon Sep 17 00:00:00 2001 From: jafaircl Date: Thu, 4 Dec 2025 22:21:21 -0500 Subject: [PATCH 4/5] typemap and referenceinfo for checked expressions; fix formatting errors --- packages/cel/src/checker/checker.test.ts | 86 ++++--- packages/cel/src/checker/checker.ts | 299 +++++++++++++++-------- packages/cel/src/checker/env.ts | 37 ++- packages/cel/src/checker/mapping.ts | 16 +- packages/cel/src/checker/scopes.ts | 22 +- packages/cel/src/checker/types.ts | 174 +++++++++++-- packages/cel/src/ext/strings/strings.ts | 93 ++++--- packages/cel/src/func.ts | 6 +- packages/cel/src/ident.ts | 32 ++- packages/cel/src/namespace.ts | 6 +- packages/cel/src/referenceinfo.test.ts | 18 +- packages/cel/src/referenceinfo.ts | 17 +- packages/cel/src/std/cast.ts | 85 +++++-- packages/cel/src/std/logic.ts | 117 +++++---- packages/cel/src/std/math.ts | 254 ++++++++++++------- packages/cel/src/std/time.ts | 6 +- packages/cel/src/std/types.ts | 14 ++ packages/cel/src/type.ts | 182 ++++++++------ packages/example/src/example.ts | 8 +- 19 files changed, 1008 insertions(+), 464 deletions(-) diff --git a/packages/cel/src/checker/checker.test.ts b/packages/cel/src/checker/checker.test.ts index 908aafd8..8e8f1901 100644 --- a/packages/cel/src/checker/checker.test.ts +++ b/packages/cel/src/checker/checker.test.ts @@ -1,19 +1,28 @@ +// Copyright 2024-2025 Buf Technologies, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + import { celVariable } from "../ident.js"; -import { - type Expr, - type ParsedExpr, +import type { + Expr, + ParsedExpr, } from "@bufbuild/cel-spec/cel/expr/syntax_pb.js"; import * as assert from "node:assert/strict"; import { suite, test } from "node:test"; import { _CelChecker } from "./checker.js"; import { parse } from "../parse.js"; -import { - CelScalar, - listType, - mapType, - objectType, - type CelType, -} from "../type.js"; +import { CelScalar, listType, mapType, objectType } from "../type.js"; +import type { CelType } from "../type.js"; import { celCheckerEnv } from "./env.js"; import { createRegistry } from "@bufbuild/protobuf"; import { TestAllTypes_NestedMessageSchema } from "@bufbuild/cel-spec/cel/expr/conformance/proto3/test_all_types_pb.js"; @@ -35,7 +44,10 @@ import { } from "@bufbuild/protobuf/wkt"; import { celFunc, celMemberOverload, celOverload } from "../func.js"; -function internal__checkForTest(expr: Expr): CelType { +function internal__checkForTest(expr: Expr | undefined): CelType { + if (expr === undefined) { + throw new Error("expr is undefined"); + } const checker = new _CelChecker( celCheckerEnv({ registry: createRegistry(TestAllTypes_NestedMessageSchema), @@ -48,7 +60,7 @@ function internal__checkForTest(expr: Expr): CelType { "fi_s_s_0", [CelScalar.STRING], CelScalar.STRING, - (s) => "" + (s) => "", ), ]), ], @@ -63,12 +75,12 @@ function internal__checkForTest(expr: Expr): CelType { celVariable("b", listType(CelScalar.STRING)), celVariable("c", mapType(CelScalar.STRING, CelScalar.BOOL)), ], - }) + }), ); checker.checkExpr(expr); if (checker.errors.length > 0) { throw new Error( - `type checking failed: ${checker.errors.map((e) => e.message).join("\n")}` + `type checking failed: ${checker.errors.map((e) => e.message).join("\n")}`, ); } return checker.getType(expr) as CelType; @@ -87,7 +99,7 @@ void suite("checker", () => { { expr: parse("null"), want: CelScalar.NULL }, ]; for (const c of cases) { - const got = internal__checkForTest(c.expr.expr!); + const got = internal__checkForTest(c.expr.expr); assert.equal(got.toString(), c.want.toString(), `case ${c.expr}`); } }); @@ -105,7 +117,7 @@ void suite("checker", () => { { expr: parse("c"), want: mapType(CelScalar.STRING, CelScalar.BOOL) }, ]; for (const c of cases) { - const got = internal__checkForTest(c.expr.expr!); + const got = internal__checkForTest(c.expr.expr); assert.equal(got.toString(), c.want.toString(), `case ${c.expr}`); } }); @@ -120,7 +132,7 @@ void suite("checker", () => { { expr: parse('["a", 1, 1.0]'), want: listType(CelScalar.DYN) }, ]; for (const c of cases) { - const got = internal__checkForTest(c.expr.expr!); + const got = internal__checkForTest(c.expr.expr); assert.equal(got.toString(), c.want.toString(), `case ${c.expr}`); } }); @@ -173,7 +185,7 @@ void suite("checker", () => { }, ]; for (const c of cases) { - const got = internal__checkForTest(c.expr.expr!); + const got = internal__checkForTest(c.expr.expr); assert.equal(got.toString(), c.want.toString(), `case ${c.expr}`); } }); @@ -186,19 +198,19 @@ void suite("checker", () => { }, { expr: parse( - "cel.expr.conformance.proto3.TestAllTypes.NestedMessage{bb: 1}" + "cel.expr.conformance.proto3.TestAllTypes.NestedMessage{bb: 1}", ), want: objectType(TestAllTypes_NestedMessageSchema), }, { expr: parse( - "cel.expr.conformance.proto3.TestAllTypes.NestedMessage{}.bb" + "cel.expr.conformance.proto3.TestAllTypes.NestedMessage{}.bb", ), want: CelScalar.INT, }, ]; for (const c of cases) { - const got = internal__checkForTest(c.expr.expr!); + const got = internal__checkForTest(c.expr.expr); assert.equal(got.toString(), c.want.toString(), `case ${c.expr}`); } }); @@ -311,7 +323,7 @@ void suite("checker", () => { }, ]; for (const c of cases) { - const got = internal__checkForTest(c.expr.expr!); + const got = internal__checkForTest(c.expr.expr); assert.equal(got.toString(), c.want.toString(), `case ${c.expr}`); } }); @@ -337,7 +349,7 @@ void suite("checker", () => { // } ]; for (const c of cases) { - const got = internal__checkForTest(c.expr.expr!); + const got = internal__checkForTest(c.expr.expr); assert.equal(got.toString(), c.want.toString(), `case ${c.expr}`); } }); @@ -381,7 +393,7 @@ void suite("checker", () => { want: CelScalar.BOOL, }, { - expr: parse('fg_s()'), + expr: parse("fg_s()"), want: CelScalar.STRING, }, { @@ -427,48 +439,48 @@ void suite("checker", () => { { expr: parse('{ "1": 2, "3": 4.0 }["1"]'), want: CelScalar.DYN, - } + }, ]; for (const c of cases) { - const got = internal__checkForTest(c.expr.expr!); + const got = internal__checkForTest(c.expr.expr); assert.equal(got.toString(), c.want.toString(), `case ${c.expr}`); } }); - void test('macros', () => { + void test("macros", () => { const cases = [ { - expr: parse('[1, 2, 3].all(e, e > 0)'), + expr: parse("[1, 2, 3].all(e, e > 0)"), want: CelScalar.BOOL, }, { - expr: parse('[1, 2, 3].exists(e, e > 0)'), + expr: parse("[1, 2, 3].exists(e, e > 0)"), want: CelScalar.BOOL, }, { - expr: parse('[1, 2, 3].exists_one(e, e > 2)'), + expr: parse("[1, 2, 3].exists_one(e, e > 2)"), want: CelScalar.BOOL, }, { - expr: parse('[1, 2, 3].filter(e, e > 2)'), + expr: parse("[1, 2, 3].filter(e, e > 2)"), want: listType(CelScalar.INT), }, { - expr: parse('[1, 2.0, 3u].filter(e, e > 2)'), + expr: parse("[1, 2.0, 3u].filter(e, e > 2)"), want: listType(CelScalar.DYN), }, { - expr: parse('[1, 2, 3].map(e, e + 1)'), + expr: parse("[1, 2, 3].map(e, e + 1)"), want: listType(CelScalar.INT), }, { - expr: parse('[1, 2.0, 3u].map(e, e + 1)'), + expr: parse("[1, 2.0, 3u].map(e, e + 1)"), want: listType(CelScalar.INT), }, - ] + ]; for (const c of cases) { - const got = internal__checkForTest(c.expr.expr!); + const got = internal__checkForTest(c.expr.expr); assert.equal(got.toString(), c.want.toString(), `case ${c.expr}`); } - }) + }); }); diff --git a/packages/cel/src/checker/checker.ts b/packages/cel/src/checker/checker.ts index 0980a0ac..a3511b34 100644 --- a/packages/cel/src/checker/checker.ts +++ b/packages/cel/src/checker/checker.ts @@ -1,17 +1,39 @@ +// Copyright 2024-2025 Buf Technologies, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + import { AggregateLiteralElementType, type CelCheckerEnv } from "./env.js"; import { Mapping } from "./mapping.js"; -import type { CheckedExpr } from "@bufbuild/cel-spec/cel/expr/checked_pb.js"; -import { CheckedExprSchema } from "@bufbuild/cel-spec/cel/expr/checked_pb.js"; import { + type CheckedExpr, + type Reference, + type Type, + CheckedExprSchema, + ReferenceSchema, +} from "@bufbuild/cel-spec/cel/expr/checked_pb.js"; +import { + ConstantSchema, Expr_CallSchema, Expr_CreateStructSchema, Expr_IdentSchema, + type Constant, type Expr, type Expr_CreateStruct, } from "@bufbuild/cel-spec/cel/expr/syntax_pb.js"; import { create } from "@bufbuild/protobuf"; import { celError, type CelError } from "../error.js"; import { + celTypeToProtoType, functionType, isAssignable, isAssignableList, @@ -21,7 +43,9 @@ import { import { type CelOpaqueType, CelScalar, + celType, type CelType, + type CelValue, DURATION, errorType, fieldDescToCelType, @@ -55,19 +79,20 @@ import { import { celVariable } from "../ident.js"; export interface CelChecker { - // + check(expr: Expr): void; } /** - * TODO: this should return a CheckedExpr. We need functions to convert - * types and references to protobuf first. + * Takes a CEL expression and environment and produces a checked expression. */ export function check(expr: Expr, env: CelCheckerEnv): CheckedExpr { const checker = new _CelChecker(env); checker.check(expr); return create(CheckedExprSchema, { expr, - // TODO: typeMap, referenceMap, sourceInfo + typeMap: checker.protoTypeMap(), + referenceMap: checker.protoReferenceMap(), + // TODO: sourceInfo }); } @@ -78,7 +103,7 @@ interface OverloadResolution { function overloadResolution( type: CelType, - reference: ReferenceInfo + reference: ReferenceInfo, ): OverloadResolution { return { type, reference }; } @@ -90,8 +115,13 @@ export class _CelChecker implements CelChecker { errors: CelError[] = []; mappings: Mapping = new Mapping(); freeTypeVarCounter = 0; + #containerName = ""; - constructor(private env: CelCheckerEnv) {} + constructor(private env: CelCheckerEnv) { + if (this.env.namespace) { + this.#containerName = this.env.namespace.name(); + } + } check(expr: Expr): void { this.checkExpr(expr); @@ -105,22 +135,30 @@ export class _CelChecker implements CelChecker { checkExpr(expr: Expr): void { switch (expr.exprKind.case) { case "constExpr": - return this.checkConstExpr(expr); + this.checkConstExpr(expr); + break; case "identExpr": - return this.checkIdentExpr(expr); + this.checkIdentExpr(expr); + break; case "selectExpr": - return this.checkSelectExpr(expr); + this.checkSelectExpr(expr); + break; case "callExpr": - return this.checkCallExpr(expr); + this.checkCallExpr(expr); + break; case "listExpr": - return this.checkCreateListExpr(expr); + this.checkCreateListExpr(expr); + break; case "structExpr": if (!expr.exprKind.value.messageName) { - return this.checkCreateMapExpr(expr); + this.checkCreateMapExpr(expr); + break; } - return this.checkCreateStructExpr(expr); + this.checkCreateStructExpr(expr); + break; case "comprehensionExpr": - return this.checkComprehensionExpr(expr); + this.checkComprehensionExpr(expr); + break; default: throw new Error(`unexpected expression kind: ${expr.exprKind.case}`); } @@ -134,26 +172,35 @@ export class _CelChecker implements CelChecker { const constant = expr.exprKind.value; switch (constant.constantKind.case) { case "boolValue": - return this.setType(expr, CelScalar.BOOL); + this.setType(expr, CelScalar.BOOL); + break; case "bytesValue": - return this.setType(expr, CelScalar.BYTES); + this.setType(expr, CelScalar.BYTES); + break; case "doubleValue": - return this.setType(expr, CelScalar.DOUBLE); + this.setType(expr, CelScalar.DOUBLE); + break; case "durationValue": - return this.setType(expr, DURATION); + this.setType(expr, DURATION); + break; case "int64Value": - return this.setType(expr, CelScalar.INT); + this.setType(expr, CelScalar.INT); + break; case "nullValue": - return this.setType(expr, CelScalar.NULL); + this.setType(expr, CelScalar.NULL); + break; case "stringValue": - return this.setType(expr, CelScalar.STRING); + this.setType(expr, CelScalar.STRING); + break; case "timestampValue": - return this.setType(expr, TIMESTAMP); + this.setType(expr, TIMESTAMP); + break; case "uint64Value": - return this.setType(expr, CelScalar.UINT); + this.setType(expr, CelScalar.UINT); + break; default: throw new Error( - `unexpected constant kind: ${constant.constantKind.case}` + `unexpected constant kind: ${constant.constantKind.case}`, ); } } @@ -179,8 +226,8 @@ export class _CelChecker implements CelChecker { const error = celError( `undeclared reference to '${ ident.name - }' (in container '${this.env.namespace!.name()}')`, - expr.id + }' (in container '${this.#containerName}')`, + expr.id, ); this.setType(expr, errorType(error)); this.errors.push(error); @@ -231,7 +278,7 @@ export class _CelChecker implements CelChecker { call.target ? " member call with" : "" } argument count: ${call.args.length}`; this.errors.push( - celError(`unsupported optional field selection: ${msg}`) + celError(`unsupported optional field selection: ${msg}`), ); return; } @@ -243,7 +290,7 @@ export class _CelChecker implements CelChecker { field.exprKind.value.constantKind.case !== "stringValue" ) { this.errors.push( - celError(`unsupported optional field selection: ${field}`, field.id) + celError(`unsupported optional field selection: ${field}`, field.id), ); return; } @@ -252,7 +299,7 @@ export class _CelChecker implements CelChecker { expr, operand, field.exprKind.value.constantKind.value, - true + true, ); this.setType(expr, substitute(this.mappings, resultType, false)); this.setReference(expr, functionReference(["select_optional_field"])); @@ -262,7 +309,7 @@ export class _CelChecker implements CelChecker { expr: Expr, operand: Expr | undefined, field: string, - optional: boolean + optional: boolean, ): CelType { if (!operand) { this.errors.push(celError(`expected select operand`, expr.id)); @@ -273,7 +320,7 @@ export class _CelChecker implements CelChecker { let operandType = this.getType(operand); if (!operandType) { this.errors.push( - celError(`unable to determine type of operand`, expr.id) + celError(`unable to determine type of operand`, expr.id), ); return errorType(celError(`invalid select operand`, expr.id)); } @@ -286,8 +333,8 @@ export class _CelChecker implements CelChecker { let resultType: CelType = errorType( celError( `type '${operandType.toString()}' does not support field selection`, - expr.id - ) + expr.id, + ), ); switch (targetType.kind) { case "map": @@ -299,7 +346,7 @@ export class _CelChecker implements CelChecker { const fieldType = this.lookupFieldType( expr.id, targetType.desc.typeName, - field + field, ); if (fieldType) { resultType = fieldType; @@ -320,8 +367,8 @@ export class _CelChecker implements CelChecker { this.errors.push( celError( `type '${operandType.toString()}' does not support field selection`, - expr.id - ) + expr.id, + ), ); } resultType = CelScalar.DYN; @@ -343,7 +390,8 @@ export class _CelChecker implements CelChecker { const call = expr.exprKind.value; const fnName = call.function; if (fnName === OPT_SELECT) { - return this.checkOptSelect(expr); + this.checkOptSelect(expr); + return; } const args = call.args; @@ -358,8 +406,8 @@ export class _CelChecker implements CelChecker { const fn = this.env.lookupFunction(fnName); if (!fn) { const err = celError( - `undeclared reference to '${fnName}' (in container '${this.env.namespace!.name()}')`, - expr.id + `undeclared reference to '${fnName}' (in container '${this.#containerName}')`, + expr.id, ); this.errors.push(err); this.setType(expr, errorType(err)); @@ -413,8 +461,8 @@ export class _CelChecker implements CelChecker { } // Function name not declared, record error. const err = celError( - `undeclared reference to '${fnName}' (in container '${this.env.namespace!.name()}')`, - expr.id + `undeclared reference to '${fnName}' (in container '${this.#containerName}')`, + expr.id, ); this.errors.push(err); this.setType(expr, errorType(err)); @@ -424,7 +472,7 @@ export class _CelChecker implements CelChecker { call: Expr, fn: CelFunc, target?: Expr, - args: Expr[] = [] + args: Expr[] = [], ) { // Attempt to resolve the overload. const resolution = this.resolveOverload(call, fn, target, args); @@ -443,14 +491,14 @@ export class _CelChecker implements CelChecker { call: Expr, fn: CelFunc, target?: Expr, - args: Expr[] = [] + args: Expr[] = [], ): OverloadResolution | undefined { const argTypes: CelType[] = []; if (target) { const targetType = this.getType(target); if (!targetType) { this.errors.push( - celError(`unable to determine type of target`, call.id) + celError(`unable to determine type of target`, call.id), ); return; } @@ -460,7 +508,7 @@ export class _CelChecker implements CelChecker { const argType = this.getType(arg); if (!argType) { this.errors.push( - celError(`unable to determine type of argument`, call.id) + celError(`unable to determine type of argument`, call.id), ); return; } @@ -492,7 +540,7 @@ export class _CelChecker implements CelChecker { if (!this.isAssignable(argType, CelScalar.BOOL)) { const err = celError( `expected type 'bool' but got '${argType.toString()}'`, - call.id + call.id, ); this.errors.push(err); resultType = errorType(err); @@ -506,7 +554,7 @@ export class _CelChecker implements CelChecker { let overloadType: CelOpaqueType = functionType( overload.result, - ...overload.parameters + ...overload.parameters, ); let typeParams = overload.typeParams(); if (typeParams.length > 0) { @@ -518,7 +566,7 @@ export class _CelChecker implements CelChecker { overloadType = substitute( substitutions, overloadType, - false + false, ) as CelOpaqueType; } @@ -533,7 +581,7 @@ export class _CelChecker implements CelChecker { const fnResultType = substitute( this.mappings, overloadType.parameters[0], - false + false, ); if (!resultType) { resultType = fnResultType; @@ -555,8 +603,8 @@ export class _CelChecker implements CelChecker { `no matching overload for '${fn.name}' applied to '(${argTypes .map((t) => t.toString()) .join(", ")})'`, - call.id - ) + call.id, + ), ); return; } @@ -586,10 +634,10 @@ export class _CelChecker implements CelChecker { this.errors.push( celError( `expected type '${optionalCelType( - elemType + elemType, ).toString()}' but got '${elemType.toString()}'`, - e.id - ) + e.id, + ), ); return; } @@ -626,17 +674,17 @@ export class _CelChecker implements CelChecker { return; } this.checkExpr(val); - let valType = this.getType(val); + let valType = this.getType(val) as CelType; if (entry.optionalEntry) { - let isOptional = isOptionalCelType(valType!); - valType = maybeUnwrapOptionalCelType(valType!); - if (!isOptional && !isDynCelType(valType!)) { - const expected = optionalCelType(valType!); + let isOptional = isOptionalCelType(valType); + valType = maybeUnwrapOptionalCelType(valType); + if (!isOptional && !isDynCelType(valType)) { + const expected = optionalCelType(valType); this.errors.push( celError( - `expected type '${expected.toString()}' but got '${valType!.toString()}'`, - val.id - ) + `expected type '${expected.toString()}' but got '${valType.toString()}'`, + val.id, + ), ); } } @@ -649,7 +697,7 @@ export class _CelChecker implements CelChecker { } this.setType( expr, - mapType(mapKeyType as mapKeyType, mapValueType as CelType) + mapType(mapKeyType as mapKeyType, mapValueType as CelType), ); } @@ -661,15 +709,15 @@ export class _CelChecker implements CelChecker { let msgVal = expr.exprKind.value; // Determine the type of the message. let resultType: CelType = errorType( - celError(`'${msgVal.messageName}' is not a message type`, expr.id) + celError(`'${msgVal.messageName}' is not a message type`, expr.id), ); const ident = this.env.lookupIdent(msgVal.messageName); if (!ident) { const error = celError( `undeclared reference to '${ msgVal.messageName - }' (in container '${this.env.namespace!.name()}')`, - expr.id + }' (in container '${this.#containerName}')`, + expr.id, ); this.setType(expr, errorType(error)); this.errors.push(error); @@ -692,7 +740,7 @@ export class _CelChecker implements CelChecker { if (identKind !== "error") { if (identKind !== "object") { this.errors.push( - celError(`'${ident.type.name}' is not a type`, expr.id) + celError(`'${ident.type.name}' is not a type`, expr.id), ); } else { resultType = objectType(ident.type.desc); @@ -700,13 +748,13 @@ export class _CelChecker implements CelChecker { // In this context, the type is being instantiated by its protobuf name which // is not ideal or recommended, but some users expect this to work. if (isWellKnownType(resultType)) { - typeName = getWellKnownTypeName(resultType)!; + typeName = getWellKnownTypeName(resultType) as string; } else if (resultType.kind === "object") { typeName = resultType.desc.typeName; } else { const error = celError( `'${ident.type.name}' is not a message type`, - expr.id + expr.id, ); this.errors.push(error); resultType = errorType(error); @@ -730,33 +778,33 @@ export class _CelChecker implements CelChecker { this.checkExpr(value); let fieldType: CelType = errorType( - celError(`unable to determine type of field '${fieldName}'`, field.id) + celError(`unable to determine type of field '${fieldName}'`, field.id), ); const ft = this.lookupFieldType(field.id, typeName, fieldName); if (ft) { fieldType = ft; } - const valType = this.getType(value); + const valType = this.getType(value) as CelType; if (field.optionalEntry) { - let isOptional = isOptionalCelType(valType!); - const unwrapped = maybeUnwrapOptionalCelType(valType!); + let isOptional = isOptionalCelType(valType); + const unwrapped = maybeUnwrapOptionalCelType(valType); if (!isOptional && !isDynCelType(unwrapped)) { const expected = optionalCelType(unwrapped); this.errors.push( celError( `expected type '${expected.toString()}' but got '${unwrapped.toString()}'`, - value.id - ) + value.id, + ), ); } } - if (!this.isAssignable(fieldType, valType!)) { + if (!this.isAssignable(fieldType, valType)) { this.errors.push( celError( - `expected type '${fieldType.toString()}' but got '${valType?.toString()}'`, - value.id - ) + `expected type '${fieldType.toString()}' but got '${valType.toString()}'`, + value.id, + ), ); } } @@ -786,8 +834,8 @@ export class _CelChecker implements CelChecker { this.errors.push( celError( `unable to determine type of comprehension iter_range`, - expr.id - ) + expr.id, + ), ); return; } @@ -799,7 +847,10 @@ export class _CelChecker implements CelChecker { if (!accuType) { // This should not happen, anyway, report an error. this.errors.push( - celError(`unable to determine type of comprehension accu_init`, expr.id) + celError( + `unable to determine type of comprehension accu_init`, + expr.id, + ), ); return; } @@ -832,7 +883,7 @@ export class _CelChecker implements CelChecker { case "scalar": if (rangeType.kind === "scalar" && rangeType.scalar !== "dyn") { const err = celError( - `expression of type '${rangeType.kind}' cannot be range of a comprehension (must be list, map, or dynamic)` + `expression of type '${rangeType.kind}' cannot be range of a comprehension (must be list, map, or dynamic)`, ); this.errors.push(err); varType = errorType(err); @@ -854,7 +905,7 @@ export class _CelChecker implements CelChecker { default: const err = celError( `expression of type '${rangeType.kind}' cannot be range of a comprehension (must be list, map, or dynamic)`, - expr.id + expr.id, ); this.errors.push(err); varType = errorType(err); @@ -868,12 +919,12 @@ export class _CelChecker implements CelChecker { this.env = this.env.enterScope(); this.env.addIdents([celVariable(comp.iterVar, varType)]); if (comp.iterVar2) { - this.env.addIdents([celVariable(comp.iterVar2, var2Type!)]); + this.env.addIdents([celVariable(comp.iterVar2, var2Type as CelType)]); } // Check the variable references in the condition and step. if (!comp.loopCondition) { this.errors.push( - celError(`expected comprehension loop_condition`, expr.id) + celError(`expected comprehension loop_condition`, expr.id), ); return; } @@ -898,7 +949,7 @@ export class _CelChecker implements CelChecker { const resultType = this.getType(comp.result); if (!resultType) { this.errors.push( - celError(`unable to determine type of comprehension result`, expr.id) + celError(`unable to determine type of comprehension result`, expr.id), ); return; } @@ -908,13 +959,16 @@ export class _CelChecker implements CelChecker { joinTypes( expr: Expr, previous: CelType | undefined, - current: CelType | undefined + current: CelType | undefined, ): CelType | undefined { if (!previous) { return current; } - if (this.isAssignable(previous, current!)) { - return mostGeneral(previous, current!); + if (!current) { + return previous; + } + if (this.isAssignable(previous, current)) { + return mostGeneral(previous, current); } if ( this.env.aggregateLiteralElementType === @@ -924,7 +978,7 @@ export class _CelChecker implements CelChecker { } const err = celError( `expected type '${previous.toString()}' but got '${current?.toString()}'`, - expr.id + expr.id, ); this.errors.push(err); return errorType(err); @@ -958,7 +1012,7 @@ export class _CelChecker implements CelChecker { const found = this.typeMap.get(expr.id); if (found && found.kind !== type.kind) { this.errors.push( - celError(`incompatible type already exists for expression`, expr.id) + celError(`incompatible type already exists for expression`, expr.id), ); return; } @@ -975,8 +1029,8 @@ export class _CelChecker implements CelChecker { this.errors.push( celError( `reference already exists for expression: ${expr}(${expr.id}) old:${old}, new:${ref}`, - expr.id - ) + expr.id, + ), ); return; } @@ -987,7 +1041,7 @@ export class _CelChecker implements CelChecker { const exprType = this.getType(expr); if (!exprType) { this.errors.push( - celError(`unable to determine type of expression`, expr.id) + celError(`unable to determine type of expression`, expr.id), ); return; } @@ -995,8 +1049,8 @@ export class _CelChecker implements CelChecker { this.errors.push( celError( `expected type '${t.toString()}' but got '${exprType.toString()}'`, - expr.id - ) + expr.id, + ), ); } } @@ -1004,13 +1058,13 @@ export class _CelChecker implements CelChecker { lookupFieldType( id: bigint, structType: string, - fieldName: string + fieldName: string, ): CelType | undefined { const msg = this.env.registry.getMessage(structType); if (!msg) { // This should not happen, anyway, report an error. this.errors.push( - celError(`unexpected failed resolution of '${structType}'`, id) + celError(`unexpected failed resolution of '${structType}'`, id), ); return undefined; } @@ -1021,6 +1075,23 @@ export class _CelChecker implements CelChecker { } return fieldDescToCelType(field); } + + protoTypeMap(): Record { + const result: Record = {}; + const mappings = this.mappings.copy(); + for (const [id, t] of this.typeMap.entries()) { + result[id.toString()] = celTypeToProtoType(substitute(mappings, t, true)); + } + return result; + } + + protoReferenceMap(): Record { + const result: Record = {}; + for (const [id, ref] of this.referenceMap.entries()) { + result[id.toString()] = referenceInfoToProtoReference(ref); + } + return result; + } } function isWellKnownType(t: CelType): boolean { @@ -1098,3 +1169,29 @@ function getWellKnownTypeName(t: CelType): string | undefined { return undefined; } } + +function valToConstant(val: CelValue): Constant { + const t = celType(val); + switch (t.kind) { + case "scalar": + switch (t.scalar) { + case "bool": + return create(ConstantSchema, { + constantKind: { + case: "boolValue", + value: val as boolean, + }, + }); + } + } + throw new Error(`unsupported constant type: ${t.toString()}`); +} + +function referenceInfoToProtoReference(ref: ReferenceInfo): Reference { + const protoRef = create(ReferenceSchema, { + name: ref.name, + overloadId: Array.from(ref.overloadIds), + value: ref.value ? valToConstant(ref.value) : undefined, + }); + return protoRef; +} diff --git a/packages/cel/src/checker/env.ts b/packages/cel/src/checker/env.ts index 465c0842..8f659845 100644 --- a/packages/cel/src/checker/env.ts +++ b/packages/cel/src/checker/env.ts @@ -1,10 +1,25 @@ +// Copyright 2024-2025 Buf Technologies, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + import * as olc from "../gen/dev/cel/expr/overload_const.js"; import { _CelChecker } from "./checker.js"; -import { type Registry } from "@bufbuild/protobuf"; +import type { Registry } from "@bufbuild/protobuf"; import { Namespace } from "../namespace.js"; import { Group, Scopes } from "./scopes.js"; -import { type CelFunc } from "../func.js"; -import { celConstant, type CelIdent, celVariable } from "../ident.js"; +import type { CelFunc } from "../func.js"; +import type { CelIdent } from "../ident.js"; +import { celConstant, celVariable } from "../ident.js"; import { createRegistryWithWKT } from "../registry.js"; import { CelScalar, objectType } from "../type.js"; import { STD_FUNCS } from "../std/std.js"; @@ -186,7 +201,7 @@ export function celCheckerEnv(options?: CelCheckerEnvOptions): CelCheckerEnv { : createRegistryWithWKT(), declarations, aggLitElemType, - filteredOverloadIds + filteredOverloadIds, ); } @@ -197,7 +212,7 @@ class _CelCheckerEnv implements CelCheckerEnv { public readonly registry: Registry, public readonly declarations: Scopes, public readonly aggregateLiteralElementType: AggregateLiteralElementType, - public readonly filteredOverloadIds: Set + public readonly filteredOverloadIds: Set, ) {} /** @@ -259,7 +274,7 @@ class _CelCheckerEnv implements CelCheckerEnv { const enumType = this.registry.getEnum(enumTypeName); if (enumType) { const enumValueDesc = enumType.values.find( - (v) => v.name === enumValueName + (v) => v.name === enumValueName, ); if (enumValueDesc) { return celConstant(candidate, CelScalar.INT, enumValueDesc.number); @@ -292,14 +307,14 @@ class _CelCheckerEnv implements CelCheckerEnv { #setFunction(fn: CelFunc): string[] { const errMsgs: string[] = []; let current = this.declarations.findFunction(fn.name); - if (current) { + if (!current) { + current = fn; + } else { // TODO: merge overloads // current = current.merge(fn) return [ `function ${fn.name} already declared. merging overloads not yet supported`, ]; - } else { - current = fn; } // TODO: check macros // for (const overload of current.overloads) { @@ -358,7 +373,7 @@ class _CelCheckerEnv implements CelCheckerEnv { this.registry, this.declarations.push(), this.aggregateLiteralElementType, - this.filteredOverloadIds + this.filteredOverloadIds, ); } @@ -371,7 +386,7 @@ class _CelCheckerEnv implements CelCheckerEnv { this.registry, this.declarations.pop(), this.aggregateLiteralElementType, - this.filteredOverloadIds + this.filteredOverloadIds, ); } } diff --git a/packages/cel/src/checker/mapping.ts b/packages/cel/src/checker/mapping.ts index fc4564ae..3a8c80f9 100644 --- a/packages/cel/src/checker/mapping.ts +++ b/packages/cel/src/checker/mapping.ts @@ -1,4 +1,18 @@ -import { type CelType } from "../type.js"; +// Copyright 2024-2025 Buf Technologies, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import type { CelType } from "../type.js"; export class Mapping { #mapping: Map; diff --git a/packages/cel/src/checker/scopes.ts b/packages/cel/src/checker/scopes.ts index 304fbbb2..c94df0be 100644 --- a/packages/cel/src/checker/scopes.ts +++ b/packages/cel/src/checker/scopes.ts @@ -1,5 +1,19 @@ -import { type CelFunc } from "../func.js"; -import { type CelIdent } from "../ident.js"; +// Copyright 2024-2025 Buf Technologies, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import type { CelFunc } from "../func.js"; +import type { CelIdent } from "../ident.js"; /** * Group is a set of Decls that is pushed on or popped off a Scopes as a unit. @@ -8,7 +22,7 @@ import { type CelIdent } from "../ident.js"; export class Group { constructor( public readonly idents: Map = new Map(), - public readonly functions: Map = new Map() + public readonly functions: Map = new Map(), ) {} /** @@ -28,7 +42,7 @@ export class Group { export class Scopes { constructor( public readonly scopes = new Group(), - public readonly parent?: Scopes + public readonly parent?: Scopes, ) { this.scopes = scopes; this.parent = parent; diff --git a/packages/cel/src/checker/types.ts b/packages/cel/src/checker/types.ts index bd5ac9e5..4b210ad9 100644 --- a/packages/cel/src/checker/types.ts +++ b/packages/cel/src/checker/types.ts @@ -1,20 +1,43 @@ +// Copyright 2024-2025 Buf Technologies, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import { + Type_PrimitiveType, + TypeSchema, +} from "@bufbuild/cel-spec/cel/expr/checked_pb.js"; +import type { Type } from "@bufbuild/cel-spec/cel/expr/checked_pb.js"; import { - type CelListType, - type CelMapType, - type CelOpaqueType, CelScalar, - type CelType, isAssignableType, isDynCelType, isDynOrErrorCelType, isExactCelType, listType, - type mapKeyType, mapType, opaqueType, typeTypeWithParam, } from "../type.js"; -import { Mapping } from "./mapping.js"; +import type { + CelListType, + CelMapType, + CelOpaqueType, + CelType, + mapKeyType, +} from "../type.js"; +import type { Mapping } from "./mapping.js"; +import { create } from "@bufbuild/protobuf"; +import { NullValue } from "@bufbuild/protobuf/wkt"; /** * isEqualOrLessSpecific checks whether one type is equal or less specific than the other one. @@ -48,7 +71,7 @@ function isEqualOrLessSpecific(t1: CelType, t2: CelType): boolean { if ( !isEqualOrLessSpecific( t1.parameters[i], - (t2 as CelOpaqueType).parameters[i] + (t2 as CelOpaqueType).parameters[i], ) ) { return false; @@ -151,7 +174,7 @@ function internalIsAssignable(m: Mapping, t1: CelType, t2: CelType): boolean { function isValidTypeSubstitution( m: Mapping, t1: CelType, - t2: CelType + t2: CelType, ): [boolean, boolean] { // Early return if the t1 and t2 are the same instance. const kind1 = t1.kind; @@ -193,7 +216,7 @@ function isValidTypeSubstitution( function internalIsAssignableList( m: Mapping, l1: CelType[], - l2: CelType[] + l2: CelType[], ): boolean { if (l1.length !== l2.length) { return false; @@ -233,7 +256,7 @@ function isLegacyNullable(t: CelType): boolean { export function isAssignable( m: Mapping, t1: CelType, - t2: CelType + t2: CelType, ): Mapping | undefined { const mCopy = m.copy(); if (internalIsAssignable(mCopy, t1, t2)) { @@ -248,7 +271,7 @@ export function isAssignable( export function isAssignableList( m: Mapping, l1: CelType[], - l2: CelType[] + l2: CelType[], ): Mapping | undefined { const mCopy = m.copy(); if (internalIsAssignableList(mCopy, l1, l2)) { @@ -311,7 +334,11 @@ function notReferencedIn(m: Mapping, t: CelType, withinType: CelType): boolean { * substitute replaces all direct and indirect occurrences of bound type parameters. Unbound type * parameters are replaced by DYN if typeParamToDyn is true. */ -export function substitute(m: Mapping, t: CelType, typeParamToDyn: boolean): CelType { +export function substitute( + m: Mapping, + t: CelType, + typeParamToDyn: boolean, +): CelType { const tSub = m.find(t); if (tSub) { return substitute(m, tSub, typeParamToDyn); @@ -323,14 +350,14 @@ export function substitute(m: Mapping, t: CelType, typeParamToDyn: boolean): Cel case "opaque": return opaqueType( t.name, - substituteParams(m, t.parameters, typeParamToDyn) + substituteParams(m, t.parameters, typeParamToDyn), ); case "list": return listType(substitute(m, t.element, typeParamToDyn)); case "map": return mapType( substitute(m, t.key, typeParamToDyn) as mapKeyType, - substitute(m, t.value, typeParamToDyn) + substitute(m, t.value, typeParamToDyn), ); case "type": if (t.type) { @@ -345,7 +372,7 @@ export function substitute(m: Mapping, t: CelType, typeParamToDyn: boolean): Cel function substituteParams( m: Mapping, typeParams: CelType[], - typeParamToDyn: boolean + typeParamToDyn: boolean, ): CelType[] { const subParams: CelType[] = []; for (let i = 0; i < typeParams.length; i++) { @@ -360,3 +387,120 @@ export function functionType( ): CelOpaqueType { return opaqueType("function", [resultType, ...argTypes]); } + +/** + * Converts a primitive CelType to its protobuf Type representation. + */ +function primitiveProtoType(type: Type_PrimitiveType): Type { + return create(TypeSchema, { + typeKind: { + case: "primitive", + value: type, + }, + }); +} + +/** + * Converts a CelType to its protobuf Type representation. + */ +export function celTypeToProtoType(t: CelType): Type { + switch (t.kind) { + case "error": + return create(TypeSchema, { + typeKind: { + case: "error", + // TODO: should we include more info here? + value: {}, + }, + }); + case "list": + return create(TypeSchema, { + typeKind: { + case: "listType", + value: { + elemType: celTypeToProtoType(t.element), + }, + }, + }); + case "map": + return create(TypeSchema, { + typeKind: { + case: "mapType", + value: { + keyType: celTypeToProtoType(t.key), + valueType: celTypeToProtoType(t.value), + }, + }, + }); + case "object": + return create(TypeSchema, { + typeKind: { + case: "messageType", + value: t.name, + }, + }); + case "opaque": + return create(TypeSchema, { + typeKind: { + case: "abstractType", + value: { + name: t.name, + parameterTypes: t.parameters.map((pt) => celTypeToProtoType(pt)), + }, + }, + }); + case "scalar": + switch (t.scalar) { + case "bool": + return primitiveProtoType(Type_PrimitiveType.BOOL); + case "bytes": + return primitiveProtoType(Type_PrimitiveType.BYTES); + case "double": + return primitiveProtoType(Type_PrimitiveType.DOUBLE); + case "int": + return primitiveProtoType(Type_PrimitiveType.INT64); + case "string": + return primitiveProtoType(Type_PrimitiveType.STRING); + case "uint": + return primitiveProtoType(Type_PrimitiveType.UINT64); + case "null_type": + return create(TypeSchema, { + typeKind: { + case: "null", + value: NullValue.NULL_VALUE, + }, + }); + case "dyn": + return create(TypeSchema, { + typeKind: { + case: "dyn", + value: {}, + }, + }); + case "type": + // TODO: is this right? + return create(TypeSchema, { + typeKind: { + case: "typeParam", + value: t.name, + }, + }); + } + case "type": { + return create(TypeSchema, { + typeKind: { + case: "type", + value: celTypeToProtoType(t.type), + }, + }); + } + case "type_param": { + return create(TypeSchema, { + typeKind: { + case: "typeParam", + value: t.name, + }, + }); + } + } +} diff --git a/packages/cel/src/ext/strings/strings.ts b/packages/cel/src/ext/strings/strings.ts index 45131a2c..228ad24b 100644 --- a/packages/cel/src/ext/strings/strings.ts +++ b/packages/cel/src/ext/strings/strings.ts @@ -29,7 +29,7 @@ import { isReflectMessage } from "@bufbuild/protobuf/reflect"; const charAt = celFunc("charAt", [ celOverload( - 'string_char_at_int', + "string_char_at_int", [CelScalar.STRING, CelScalar.INT], CelScalar.STRING, (str, index) => { @@ -44,13 +44,13 @@ const charAt = celFunc("charAt", [ const indexOf = celFunc("indexOf", [ celOverload( - 'string_index_of_string', + "string_index_of_string", [CelScalar.STRING, CelScalar.STRING], CelScalar.INT, (str, substr) => BigInt(str.indexOf(substr)), ), celOverload( - 'string_index_of_string_int', + "string_index_of_string_int", [CelScalar.STRING, CelScalar.STRING, CelScalar.INT], CelScalar.INT, (str, substr, startN) => { @@ -65,13 +65,13 @@ const indexOf = celFunc("indexOf", [ const lastIndexOf = celFunc("lastIndexOf", [ celOverload( - 'string_last_index_of_string', + "string_last_index_of_string", [CelScalar.STRING, CelScalar.STRING], CelScalar.INT, (str, substr) => BigInt(str.lastIndexOf(substr)), ), celOverload( - 'string_last_index_of_string_int', + "string_last_index_of_string_int", [CelScalar.STRING, CelScalar.STRING, CelScalar.INT], CelScalar.INT, (str, substr, startN) => { @@ -85,34 +85,44 @@ const lastIndexOf = celFunc("lastIndexOf", [ ]); const lowerAscii = celFunc("lowerAscii", [ - celOverload('string_lower_ascii', [CelScalar.STRING], CelScalar.STRING, (str) => { - // Only lower case ascii characters. - let result = ""; - for (let i = 0; i < str.length; i++) { - const code = str.charCodeAt(i); - if (code >= 65 && code <= 90) { - result += String.fromCharCode(code + 32); - } else { - result += str.charAt(i); + celOverload( + "string_lower_ascii", + [CelScalar.STRING], + CelScalar.STRING, + (str) => { + // Only lower case ascii characters. + let result = ""; + for (let i = 0; i < str.length; i++) { + const code = str.charCodeAt(i); + if (code >= 65 && code <= 90) { + result += String.fromCharCode(code + 32); + } else { + result += str.charAt(i); + } } - } - return result; - }), + return result; + }, + ), ]); const upperAscii = celFunc("upperAscii", [ - celOverload('string_upper_ascii', [CelScalar.STRING], CelScalar.STRING, (str) => { - let result = ""; - for (let i = 0; i < str.length; i++) { - const c = str.charCodeAt(i); - if (c >= 97 && c <= 122) { - result += String.fromCharCode(c - 32); - } else { - result += str.charAt(i); + celOverload( + "string_upper_ascii", + [CelScalar.STRING], + CelScalar.STRING, + (str) => { + let result = ""; + for (let i = 0; i < str.length; i++) { + const c = str.charCodeAt(i); + if (c >= 97 && c <= 122) { + result += String.fromCharCode(c - 32); + } else { + result += str.charAt(i); + } } - } - return result; - }), + return result; + }, + ), ]); function replaceOp(str: string, substr: string, repl: string, num: number) { @@ -134,13 +144,13 @@ function replaceOp(str: string, substr: string, repl: string, num: number) { const replace = celFunc("replace", [ celOverload( - 'string_replace_string_string', + "string_replace_string_string", [CelScalar.STRING, CelScalar.STRING, CelScalar.STRING], CelScalar.STRING, (str, substr, repl) => replaceOp(str, substr, repl, str.length), ), celOverload( - 'string_replace_string_string_int', + "string_replace_string_string_int", [CelScalar.STRING, CelScalar.STRING, CelScalar.STRING, CelScalar.INT], CelScalar.STRING, (str, substr, repl, num) => replaceOp(str, substr, repl, Number(num)), @@ -156,13 +166,13 @@ function splitOp(str: string, sep: string, num?: number) { const split = celFunc("split", [ celOverload( - 'string_split_string', + "string_split_string", [CelScalar.STRING, CelScalar.STRING], listType(CelScalar.STRING), splitOp, ), celOverload( - 'string_split_string_int', + "string_split_string_int", [CelScalar.STRING, CelScalar.STRING, CelScalar.INT], listType(CelScalar.STRING), (str, sep, num) => splitOp(str, sep, Number(num)), @@ -192,9 +202,14 @@ function substringOp(str: string, start: bigint, end?: bigint) { } const substring = celFunc("substring", [ - celOverload('string_substring_int', [CelScalar.STRING, CelScalar.INT], CelScalar.STRING, substringOp), celOverload( - 'string_substring_int_int', + "string_substring_int", + [CelScalar.STRING, CelScalar.INT], + CelScalar.STRING, + substringOp, + ), + celOverload( + "string_substring_int_int", [CelScalar.STRING, CelScalar.INT, CelScalar.INT], CelScalar.STRING, substringOp, @@ -209,7 +224,7 @@ const WHITE_SPACE = new Set([ ]); const trim = celFunc("trim", [ - celOverload('string_trim', [CelScalar.STRING], CelScalar.STRING, (str) => { + celOverload("string_trim", [CelScalar.STRING], CelScalar.STRING, (str) => { // Trim using the unicode white space definition. let start = 0; let end = str.length - 1; @@ -239,9 +254,9 @@ function joinOp(list: CelList, sep = "") { } const join = celFunc("join", [ - celOverload('list_join', [listType(CelScalar.DYN)], CelScalar.STRING, joinOp), + celOverload("list_join", [listType(CelScalar.DYN)], CelScalar.STRING, joinOp), celOverload( - 'list_join_string', + "list_join_string", [listType(CelScalar.DYN), CelScalar.STRING], CelScalar.STRING, joinOp, @@ -262,7 +277,7 @@ const QUOTE_MAP: Map = new Map([ ]); const quote = celFunc("strings.quote", [ - celOverload('strings_quote', [CelScalar.STRING], CelScalar.STRING, (str) => { + celOverload("strings_quote", [CelScalar.STRING], CelScalar.STRING, (str) => { let result = '"'; for (let i = 0; i < str.length; i++) { const c = str.charCodeAt(i); @@ -574,7 +589,7 @@ function formatImpl(format: string, args: CelList) { const format = celFunc("format", [ celOverload( - 'string_format', + "string_format", [CelScalar.STRING, listType(CelScalar.DYN)], CelScalar.STRING, formatImpl, diff --git a/packages/cel/src/func.ts b/packages/cel/src/func.ts index 463961b0..8220b2f4 100644 --- a/packages/cel/src/func.ts +++ b/packages/cel/src/func.ts @@ -205,13 +205,13 @@ class FuncOverload typeParams(): string[] { function collectParamNames(paramNames: string[], arg: CelType) { switch (arg.kind) { - case 'type_param': + case "type_param": paramNames.push(arg.name); break; - case 'list': + case "list": collectParamNames(paramNames, arg.element); break; - case 'map': + case "map": collectParamNames(paramNames, arg.key); collectParamNames(paramNames, arg.value); break; diff --git a/packages/cel/src/ident.ts b/packages/cel/src/ident.ts index 2e743254..eecfd195 100644 --- a/packages/cel/src/ident.ts +++ b/packages/cel/src/ident.ts @@ -1,4 +1,23 @@ -import { type CelValue, type CelType, isEquivalentCelType, typeTypeWithParam } from "./type.js"; +// Copyright 2024-2025 Buf Technologies, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import { + type CelValue, + type CelType, + isEquivalentCelType, + typeTypeWithParam, +} from "./type.js"; const privateIdentSymbol = Symbol.for("@bufbuild/cel/ident"); @@ -29,7 +48,7 @@ export interface CelIdent { export function celVariable( name: string, type: CelType, - doc?: string + doc?: string, ): CelIdent { return new Ident(name, type, undefined, doc); } @@ -37,10 +56,7 @@ export function celVariable( /** * Creates a new type identifier */ -export function celTypeVariable( - type: CelType, - doc?: string -): CelIdent { +export function celTypeVariable(type: CelType, doc?: string): CelIdent { return new Ident(type.name, typeTypeWithParam(type), undefined, doc); } @@ -51,7 +67,7 @@ export function celConstant( name: string, type: CelType, value: CelValue, - doc?: string + doc?: string, ): CelIdent { return new Ident(name, type, value, doc); } @@ -63,7 +79,7 @@ class Ident implements CelIdent { public readonly name: string, public readonly type: CelType, public readonly value?: CelValue, - public readonly doc?: string + public readonly doc?: string, ) {} declarationIsEquivalent(other: CelIdent): boolean { diff --git a/packages/cel/src/namespace.ts b/packages/cel/src/namespace.ts index 369f03e0..72e7b968 100644 --- a/packages/cel/src/namespace.ts +++ b/packages/cel/src/namespace.ts @@ -85,9 +85,9 @@ export class Namespace { */ export function toQualifiedName(e: Expr): [string, boolean] { switch (e.exprKind.case) { - case 'identExpr': + case "identExpr": return [e.exprKind.value.name, true]; - case 'selectExpr': + case "selectExpr": const sel = e.exprKind.value; // Test only expressions are not valid as qualified names. if (sel.testOnly || !sel.operand) { @@ -100,4 +100,4 @@ export function toQualifiedName(e: Expr): [string, boolean] { break; } return ["", false]; -} \ No newline at end of file +} diff --git a/packages/cel/src/referenceinfo.test.ts b/packages/cel/src/referenceinfo.test.ts index d795bb59..7b790e7b 100644 --- a/packages/cel/src/referenceinfo.test.ts +++ b/packages/cel/src/referenceinfo.test.ts @@ -1,3 +1,17 @@ +// Copyright 2024-2025 Buf Technologies, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + import * as assert from "node:assert/strict"; import { suite, test } from "node:test"; import { ADD_BYTES, ADD_DOUBLE } from "./gen/dev/cel/expr/overload_const.js"; @@ -60,7 +74,7 @@ void suite("ReferenceInfo", () => { a: identReference("BYTES", toCel(new TextEncoder().encode("bytes"))), b: identReference( "BYTES", - toCel(new TextEncoder().encode("bytes-other")) + toCel(new TextEncoder().encode("bytes-other")), ), equal: false, }, @@ -75,7 +89,7 @@ void suite("ReferenceInfo", () => { assert.equal( tc.a.equals(tc.b), tc.equal, - `unexpected equality for ${tc.name}` + `unexpected equality for ${tc.name}`, ); } }); diff --git a/packages/cel/src/referenceinfo.ts b/packages/cel/src/referenceinfo.ts index f0e029bf..bb6161b8 100644 --- a/packages/cel/src/referenceinfo.ts +++ b/packages/cel/src/referenceinfo.ts @@ -1,3 +1,17 @@ +// Copyright 2024-2025 Buf Technologies, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + import { equals } from "./equals.js"; import type { CelValue } from "./type.js"; @@ -10,7 +24,7 @@ export class ReferenceInfo { constructor( public readonly name?: string, public readonly value?: CelValue, - public readonly overloadIds: Set = new Set() + public readonly overloadIds: Set = new Set(), ) {} /** @@ -66,4 +80,3 @@ export function functionReference( ): ReferenceInfo { return new ReferenceInfo("", undefined, new Set(overloadIds)); } - diff --git a/packages/cel/src/std/cast.ts b/packages/cel/src/std/cast.ts index cff253aa..7d6a92d9 100644 --- a/packages/cel/src/std/cast.ts +++ b/packages/cel/src/std/cast.ts @@ -114,10 +114,21 @@ const uintFunc = celFunc(UINT, [ ]); const doubleFunc = celFunc(DOUBLE, [ - celOverload(olc.DOUBLE_TO_DOUBLE, [CelScalar.DOUBLE], CelScalar.DOUBLE, (x) => x), - celOverload(olc.INT_TO_DOUBLE, [CelScalar.INT], CelScalar.DOUBLE, (x) => Number(x)), - celOverload(olc.DOUBLE_TO_UINT, [CelScalar.UINT], CelScalar.DOUBLE, (x) => Number(x.value)), - celOverload(olc.STRING_TO_DOUBLE, [CelScalar.STRING], CelScalar.DOUBLE, (x) => Number(x)), + celOverload( + olc.DOUBLE_TO_DOUBLE, + [CelScalar.DOUBLE], + CelScalar.DOUBLE, + (x) => x, + ), + celOverload(olc.INT_TO_DOUBLE, [CelScalar.INT], CelScalar.DOUBLE, (x) => + Number(x), + ), + celOverload(olc.DOUBLE_TO_UINT, [CelScalar.UINT], CelScalar.DOUBLE, (x) => + Number(x.value), + ), + celOverload(olc.STRING_TO_DOUBLE, [CelScalar.STRING], CelScalar.DOUBLE, (x) => + Number(x), + ), ]); const boolFunc = celFunc(BOOL, [ @@ -148,13 +159,24 @@ const bytesFunc = celFunc(BYTES, [ ]); const stringFunc = celFunc(STRING, [ - celOverload(olc.STRING_TO_STRING, [CelScalar.STRING], CelScalar.STRING, (x) => x), + celOverload( + olc.STRING_TO_STRING, + [CelScalar.STRING], + CelScalar.STRING, + (x) => x, + ), celOverload(olc.BOOL_TO_STRING, [CelScalar.BOOL], CelScalar.STRING, (x) => x ? "true" : "false", ), - celOverload(olc.INT_TO_STRING, [CelScalar.INT], CelScalar.STRING, (x) => x.toString()), - celOverload(olc.UINT_TO_STRING, [CelScalar.UINT], CelScalar.STRING, (x) => x.value.toString()), - celOverload(olc.DOUBLE_TO_STRING, [CelScalar.DOUBLE], CelScalar.STRING, (x) => x.toString()), + celOverload(olc.INT_TO_STRING, [CelScalar.INT], CelScalar.STRING, (x) => + x.toString(), + ), + celOverload(olc.UINT_TO_STRING, [CelScalar.UINT], CelScalar.STRING, (x) => + x.value.toString(), + ), + celOverload(olc.DOUBLE_TO_STRING, [CelScalar.DOUBLE], CelScalar.STRING, (x) => + x.toString(), + ), celOverload(olc.BYTES_TO_STRING, [CelScalar.BYTES], CelScalar.STRING, (x) => { const coder = new TextDecoder(undefined, { fatal: true }); try { @@ -163,8 +185,11 @@ const stringFunc = celFunc(STRING, [ throw new Error(`Failed to decode bytes as string: ${e}`); } }), - celOverload(olc.TIMESTAMP_TO_STRING, [TIMESTAMP_TYPE], CelScalar.STRING, (x) => - toJson(TimestampSchema, x.message), + celOverload( + olc.TIMESTAMP_TO_STRING, + [TIMESTAMP_TYPE], + CelScalar.STRING, + (x) => toJson(TimestampSchema, x.message), ), celOverload(olc.DURATION_TO_STRING, [DURATION_TYPE], CelScalar.STRING, (x) => toJson(DurationSchema, x.message), @@ -172,22 +197,42 @@ const stringFunc = celFunc(STRING, [ ]); const timestampFunc = celFunc(TIMESTAMP, [ - celOverload(olc.TIMESTAMP_TO_TIMESTAMP, [TIMESTAMP_TYPE], TIMESTAMP_TYPE, (x) => x), - celOverload(olc.STRING_TO_TIMESTAMP, [CelScalar.STRING], TIMESTAMP_TYPE, (x) => { - try { - return fromJson(TimestampSchema, x); - } catch (e) { - throw new Error(`Failed to parse timestamp: ${e}`); - } - }), + celOverload( + olc.TIMESTAMP_TO_TIMESTAMP, + [TIMESTAMP_TYPE], + TIMESTAMP_TYPE, + (x) => x, + ), + celOverload( + olc.STRING_TO_TIMESTAMP, + [CelScalar.STRING], + TIMESTAMP_TYPE, + (x) => { + try { + return fromJson(TimestampSchema, x); + } catch (e) { + throw new Error(`Failed to parse timestamp: ${e}`); + } + }, + ), celOverload(olc.INT_TO_TIMESTAMP, [CelScalar.INT], TIMESTAMP_TYPE, (x) => timestampFromMs(Number(x)), ), ]); const durationFunc = celFunc(DURATION, [ - celOverload(olc.DURATION_TO_DURATION, [DURATION_TYPE], DURATION_TYPE, (x) => x), - celOverload(olc.STRING_TO_DURATION, [CelScalar.STRING], DURATION_TYPE, parseDuration), + celOverload( + olc.DURATION_TO_DURATION, + [DURATION_TYPE], + DURATION_TYPE, + (x) => x, + ), + celOverload( + olc.STRING_TO_DURATION, + [CelScalar.STRING], + DURATION_TYPE, + parseDuration, + ), celOverload(olc.INT_TO_DURATION, [CelScalar.INT], DURATION_TYPE, (x) => create(DurationSchema, { seconds: x }), ), diff --git a/packages/cel/src/std/logic.ts b/packages/cel/src/std/logic.ts index 658eb332..699ec261 100644 --- a/packages/cel/src/std/logic.ts +++ b/packages/cel/src/std/logic.ts @@ -44,16 +44,15 @@ const notStrictlyFalse = celFunc(opc.NOT_STRICTLY_FALSE, [ [CelScalar.BOOL], CelScalar.BOOL, (x) => true, // Irrelevant because we overwrite dispatch below - ) -]) + ), +]); notStrictlyFalse.dispatch = (_, args) => { const raw = args[0]; if (isCelError(raw)) { return true; } return raw !== false; -} - +}; const notFunc = celFunc(opc.LOGICAL_NOT, [ celOverload(olc.LOGICAL_NOT, [CelScalar.BOOL], CelScalar.BOOL, (x) => !x), @@ -65,8 +64,8 @@ const and = celFunc(opc.LOGICAL_AND, [ [CelScalar.BOOL, CelScalar.BOOL], CelScalar.BOOL, (x, y) => x && y, // Irrelevant because we overwrite dispatch below - ) -]) + ), +]); and.dispatch = (_id, args) => { let allBools = true; const errors: CelError[] = []; @@ -88,7 +87,7 @@ and.dispatch = (_id, args) => { return celErrorMerge(errors[0], ...errors.slice(1)); } return undefined; -} +}; const or = celFunc(opc.LOGICAL_OR, [ celOverload( @@ -96,8 +95,8 @@ const or = celFunc(opc.LOGICAL_OR, [ [CelScalar.BOOL, CelScalar.BOOL], CelScalar.BOOL, (x, y) => x || y, // Irrelevant because we overwrite dispatch below - ) -]) + ), +]); or.dispatch = (_, args) => { let allBools = true; const errors: CelError[] = []; @@ -119,7 +118,7 @@ or.dispatch = (_, args) => { return celErrorMerge(errors[0], ...errors.slice(1)); } return undefined; -} +}; /** * This is not actually used by the planner since it handles conditionals @@ -142,28 +141,23 @@ const conditional = celFunc(opc.CONDITIONAL, [ * but it is defined here for type checking. */ const index = celFunc(opc.INDEX, [ - celOverload( - olc.INDEX_LIST, - [listOfA, CelScalar.INT], - paramA, - (lst, idx) => { - // Irrelevant because dispatch is never called by the planner for indexing - return lst.get(Number(idx)) ?? null; - }, - ), - celOverload( - olc.INDEX_MAP, - [mapOfAB, paramA], - paramB, - (mp, key) => { - // Irrelevant because dispatch is never called by the planner for indexing - return mp.get(key as string) ?? null; - }, - ) + celOverload(olc.INDEX_LIST, [listOfA, CelScalar.INT], paramA, (lst, idx) => { + // Irrelevant because dispatch is never called by the planner for indexing + return lst.get(Number(idx)) ?? null; + }), + celOverload(olc.INDEX_MAP, [mapOfAB, paramA], paramB, (mp, key) => { + // Irrelevant because dispatch is never called by the planner for indexing + return mp.get(key as string) ?? null; + }), ]); const eqFunc = celFunc(opc.EQUALS, [ - celOverload(olc.EQUALS, [CelScalar.DYN, CelScalar.DYN], CelScalar.BOOL, equals), + celOverload( + olc.EQUALS, + [CelScalar.DYN, CelScalar.DYN], + CelScalar.BOOL, + equals, + ), ]); const neFunc = celFunc(opc.NOT_EQUALS, [ @@ -262,20 +256,29 @@ const geFunc = celFunc(opc.GREATER_EQUALS, [ ]); const containsFunc = celFunc(olc.CONTAINS, [ - celMemberOverload(olc.CONTAINS_STRING, [CelScalar.STRING, CelScalar.STRING], CelScalar.BOOL, (x, y) => - x.includes(y), + celMemberOverload( + olc.CONTAINS_STRING, + [CelScalar.STRING, CelScalar.STRING], + CelScalar.BOOL, + (x, y) => x.includes(y), ), ]); const endsWithFunc = celFunc(olc.ENDS_WITH, [ - celMemberOverload(olc.ENDS_WITH_STRING, [CelScalar.STRING, CelScalar.STRING], CelScalar.BOOL, (x, y) => - x.endsWith(y), + celMemberOverload( + olc.ENDS_WITH_STRING, + [CelScalar.STRING, CelScalar.STRING], + CelScalar.BOOL, + (x, y) => x.endsWith(y), ), ]); const startsWithFunc = celFunc(olc.STARTS_WITH, [ - celMemberOverload(olc.STARTS_WITH_STRING, [CelScalar.STRING, CelScalar.STRING], CelScalar.BOOL, (x, y) => - x.startsWith(y), + celMemberOverload( + olc.STARTS_WITH_STRING, + [CelScalar.STRING, CelScalar.STRING], + CelScalar.BOOL, + (x, y) => x.startsWith(y), ), ]); @@ -352,19 +355,35 @@ const sizeFunc = celFunc(olc.SIZE, [ } return BigInt(size); }), - celMemberOverload(olc.SIZE_STRING_INST, [CelScalar.STRING], CelScalar.INT, (x) => { - let size = 0; - for (const _ of x) { - size++; - } - return BigInt(size); - }), - celOverload(olc.SIZE_BYTES, [CelScalar.BYTES], CelScalar.INT, (x) => BigInt(x.length)), - celMemberOverload(olc.SIZE_BYTES_INST, [CelScalar.BYTES], CelScalar.INT, (x) => BigInt(x.length)), + celMemberOverload( + olc.SIZE_STRING_INST, + [CelScalar.STRING], + CelScalar.INT, + (x) => { + let size = 0; + for (const _ of x) { + size++; + } + return BigInt(size); + }, + ), + celOverload(olc.SIZE_BYTES, [CelScalar.BYTES], CelScalar.INT, (x) => + BigInt(x.length), + ), + celMemberOverload( + olc.SIZE_BYTES_INST, + [CelScalar.BYTES], + CelScalar.INT, + (x) => BigInt(x.length), + ), celOverload(olc.SIZE_LIST, [listOfA], CelScalar.INT, (x) => BigInt(x.size)), - celMemberOverload(olc.SIZE_LIST_INST, [listOfA], CelScalar.INT, (x) => BigInt(x.size)), + celMemberOverload(olc.SIZE_LIST_INST, [listOfA], CelScalar.INT, (x) => + BigInt(x.size), + ), celOverload(olc.SIZE_MAP, [mapOfAB], CelScalar.INT, (x) => BigInt(x.size)), - celMemberOverload(olc.SIZE_MAP_INST, [mapOfAB], CelScalar.INT, (x) => BigInt(x.size)), + celMemberOverload(olc.SIZE_MAP_INST, [mapOfAB], CelScalar.INT, (x) => + BigInt(x.size), + ), ]); function mapInOp(x: CelValue, y: CelMap) { @@ -395,19 +414,19 @@ const inFunc = celFunc(opc.IN, [ mapInOp, ), celOverload( - olc.IN_MAP + '_int_key', + olc.IN_MAP + "_int_key", [CelScalar.DYN, mapType(CelScalar.INT, CelScalar.DYN)], CelScalar.BOOL, mapInOp, ), celOverload( - olc.IN_MAP + '_uint_key', + olc.IN_MAP + "_uint_key", [CelScalar.DYN, mapType(CelScalar.UINT, CelScalar.DYN)], CelScalar.BOOL, mapInOp, ), celOverload( - olc.IN_MAP + '_bool_key', + olc.IN_MAP + "_bool_key", [CelScalar.DYN, mapType(CelScalar.BOOL, CelScalar.DYN)], CelScalar.BOOL, mapInOp, diff --git a/packages/cel/src/std/math.ts b/packages/cel/src/std/math.ts index e0d8af0c..be0fd8c4 100644 --- a/packages/cel/src/std/math.ts +++ b/packages/cel/src/std/math.ts @@ -108,20 +108,30 @@ function subtractDurationOrTimestamp< } const add = celFunc(opc.ADD, [ - celOverload(olc.ADD_INT64, [CelScalar.INT, CelScalar.INT], CelScalar.INT, (lhs, rhs) => { - const val = lhs + rhs; - if (isOverflowInt(val)) { - throw overflow(opc.SUBTRACT, CelScalar.INT); - } - return val; - }), - celOverload(olc.ADD_UINT64, [CelScalar.UINT, CelScalar.UINT], CelScalar.UINT, (lhs, rhs) => { - const val = lhs.value + rhs.value; - if (isOverflowUint(val)) { - throw overflow(opc.SUBTRACT, CelScalar.UINT); - } - return celUint(val); - }), + celOverload( + olc.ADD_INT64, + [CelScalar.INT, CelScalar.INT], + CelScalar.INT, + (lhs, rhs) => { + const val = lhs + rhs; + if (isOverflowInt(val)) { + throw overflow(opc.SUBTRACT, CelScalar.INT); + } + return val; + }, + ), + celOverload( + olc.ADD_UINT64, + [CelScalar.UINT, CelScalar.UINT], + CelScalar.UINT, + (lhs, rhs) => { + const val = lhs.value + rhs.value; + if (isOverflowUint(val)) { + throw overflow(opc.SUBTRACT, CelScalar.UINT); + } + return celUint(val); + }, + ), celOverload( olc.ADD_DOUBLE, [CelScalar.DOUBLE, CelScalar.DOUBLE], @@ -145,66 +155,113 @@ const add = celFunc(opc.ADD, [ return val; }, ), - celOverload('add_timestamp_timestamp', [TIMESTAMP, TIMESTAMP], TIMESTAMP, addTimestamp), - celOverload(olc.ADD_TIMESTAMP_DURATION, [TIMESTAMP, DURATION], TIMESTAMP, addTimestamp), - celOverload(olc.ADD_DURATION_TIMESTAMP, [DURATION, TIMESTAMP], TIMESTAMP, (lhs, rhs) => - addTimestamp(rhs, lhs), + celOverload( + "add_timestamp_timestamp", + [TIMESTAMP, TIMESTAMP], + TIMESTAMP, + addTimestamp, + ), + celOverload( + olc.ADD_TIMESTAMP_DURATION, + [TIMESTAMP, DURATION], + TIMESTAMP, + addTimestamp, + ), + celOverload( + olc.ADD_DURATION_TIMESTAMP, + [DURATION, TIMESTAMP], + TIMESTAMP, + (lhs, rhs) => addTimestamp(rhs, lhs), ), - celOverload(olc.ADD_DURATION_DURATION, [DURATION, DURATION], DURATION, addDuration), celOverload( - olc.ADD_LIST, - [listOfA, listOfA], - listOfA, - celListConcat, + olc.ADD_DURATION_DURATION, + [DURATION, DURATION], + DURATION, + addDuration, ), + celOverload(olc.ADD_LIST, [listOfA, listOfA], listOfA, celListConcat), ]); const subtract = celFunc(opc.SUBTRACT, [ - celOverload(olc.SUBTRACT_INT64, [CelScalar.INT, CelScalar.INT], CelScalar.INT, (lhs, rhs) => { - const val = lhs - rhs; - if (isOverflowInt(val)) { - throw overflow(opc.SUBTRACT, CelScalar.INT); - } - return val; - }), - celOverload(olc.SUBTRACT_UINT64, [CelScalar.UINT, CelScalar.UINT], CelScalar.UINT, (lhs, rhs) => { - const val = lhs.value - rhs.value; - if (isOverflowUint(val)) { - throw overflow(opc.SUBTRACT, CelScalar.UINT); - } - return celUint(val); - }), + celOverload( + olc.SUBTRACT_INT64, + [CelScalar.INT, CelScalar.INT], + CelScalar.INT, + (lhs, rhs) => { + const val = lhs - rhs; + if (isOverflowInt(val)) { + throw overflow(opc.SUBTRACT, CelScalar.INT); + } + return val; + }, + ), + celOverload( + olc.SUBTRACT_UINT64, + [CelScalar.UINT, CelScalar.UINT], + CelScalar.UINT, + (lhs, rhs) => { + const val = lhs.value - rhs.value; + if (isOverflowUint(val)) { + throw overflow(opc.SUBTRACT, CelScalar.UINT); + } + return celUint(val); + }, + ), celOverload( olc.SUBTRACT_DOUBLE, [CelScalar.DOUBLE, CelScalar.DOUBLE], CelScalar.DOUBLE, (lhs, rhs) => lhs - rhs, ), - celOverload(olc.SUBTRACT_TIMESTAMP_TIMESTAMP, [TIMESTAMP, TIMESTAMP], DURATION, subtractDurationOrTimestamp), - celOverload(olc.SUBTRACT_DURATION_DURATION, [DURATION, DURATION], DURATION, subtractDurationOrTimestamp), - celOverload(olc.SUBTRACT_TIMESTAMP_DURATION, [TIMESTAMP, DURATION], TIMESTAMP, (lhs, rhs) => - createTimestamp( - lhs.message.seconds - rhs.message.seconds, - lhs.message.nanos - rhs.message.nanos, - ), + celOverload( + olc.SUBTRACT_TIMESTAMP_TIMESTAMP, + [TIMESTAMP, TIMESTAMP], + DURATION, + subtractDurationOrTimestamp, + ), + celOverload( + olc.SUBTRACT_DURATION_DURATION, + [DURATION, DURATION], + DURATION, + subtractDurationOrTimestamp, + ), + celOverload( + olc.SUBTRACT_TIMESTAMP_DURATION, + [TIMESTAMP, DURATION], + TIMESTAMP, + (lhs, rhs) => + createTimestamp( + lhs.message.seconds - rhs.message.seconds, + lhs.message.nanos - rhs.message.nanos, + ), ), ]); const multiply = celFunc(opc.MULTIPLY, [ - celOverload(olc.MULTIPLY_INT64, [CelScalar.INT, CelScalar.INT], CelScalar.INT, (lhs, rhs) => { - const product = lhs * rhs; - if (isOverflowInt(product)) { - throw overflow(opc.MULTIPLY, CelScalar.INT); - } - return product; - }), - celOverload(olc.MULTIPLY_UINT64, [CelScalar.UINT, CelScalar.UINT], CelScalar.UINT, (lhs, rhs) => { - const product = lhs.value * rhs.value; - if (isOverflowUint(product)) { - throw overflow(opc.MULTIPLY, CelScalar.UINT); - } - return celUint(product); - }), + celOverload( + olc.MULTIPLY_INT64, + [CelScalar.INT, CelScalar.INT], + CelScalar.INT, + (lhs, rhs) => { + const product = lhs * rhs; + if (isOverflowInt(product)) { + throw overflow(opc.MULTIPLY, CelScalar.INT); + } + return product; + }, + ), + celOverload( + olc.MULTIPLY_UINT64, + [CelScalar.UINT, CelScalar.UINT], + CelScalar.UINT, + (lhs, rhs) => { + const product = lhs.value * rhs.value; + if (isOverflowUint(product)) { + throw overflow(opc.MULTIPLY, CelScalar.UINT); + } + return celUint(product); + }, + ), celOverload( olc.MULTIPLY_DOUBLE, [CelScalar.DOUBLE, CelScalar.DOUBLE], @@ -214,42 +271,62 @@ const multiply = celFunc(opc.MULTIPLY, [ ]); const divide = celFunc(opc.DIVIDE, [ - celOverload(olc.DIVIDE_INT64, [CelScalar.INT, CelScalar.INT], CelScalar.INT, (lhs, rhs) => { - if (rhs === 0n) { - throw divisionByZero(CelScalar.INT); - } - if (lhs === MIN_INT && rhs === -1n) { - throw overflow(opc.DIVIDE, CelScalar.INT); - } - return lhs / rhs; - }), + celOverload( + olc.DIVIDE_INT64, + [CelScalar.INT, CelScalar.INT], + CelScalar.INT, + (lhs, rhs) => { + if (rhs === 0n) { + throw divisionByZero(CelScalar.INT); + } + if (lhs === MIN_INT && rhs === -1n) { + throw overflow(opc.DIVIDE, CelScalar.INT); + } + return lhs / rhs; + }, + ), celOverload( olc.DIVIDE_DOUBLE, [CelScalar.DOUBLE, CelScalar.DOUBLE], CelScalar.DOUBLE, (lhs, rhs) => lhs / rhs, ), - celOverload(olc.DIVIDE_UINT64, [CelScalar.UINT, CelScalar.UINT], CelScalar.UINT, (lhs, rhs) => { - if (rhs.value === 0n) { - throw divisionByZero(CelScalar.UINT); - } - return celUint(lhs.value / rhs.value); - }), + celOverload( + olc.DIVIDE_UINT64, + [CelScalar.UINT, CelScalar.UINT], + CelScalar.UINT, + (lhs, rhs) => { + if (rhs.value === 0n) { + throw divisionByZero(CelScalar.UINT); + } + return celUint(lhs.value / rhs.value); + }, + ), ]); const modulo = celFunc(opc.MODULO, [ - celOverload(olc.MODULO_INT64, [CelScalar.INT, CelScalar.INT], CelScalar.INT, (lhs, rhs) => { - if (rhs === 0n) { - throw moduloByZero(CelScalar.INT); - } - return lhs % rhs; - }), - celOverload(olc.MODULO_UINT64, [CelScalar.UINT, CelScalar.UINT], CelScalar.UINT, (lhs, rhs) => { - if (rhs.value === 0n) { - throw moduloByZero(CelScalar.UINT); - } - return celUint(lhs.value % rhs.value); - }), + celOverload( + olc.MODULO_INT64, + [CelScalar.INT, CelScalar.INT], + CelScalar.INT, + (lhs, rhs) => { + if (rhs === 0n) { + throw moduloByZero(CelScalar.INT); + } + return lhs % rhs; + }, + ), + celOverload( + olc.MODULO_UINT64, + [CelScalar.UINT, CelScalar.UINT], + CelScalar.UINT, + (lhs, rhs) => { + if (rhs.value === 0n) { + throw moduloByZero(CelScalar.UINT); + } + return celUint(lhs.value % rhs.value); + }, + ), ]); const negate = celFunc(opc.NEGATE, [ @@ -260,7 +337,12 @@ const negate = celFunc(opc.NEGATE, [ } return val; }), - celOverload(olc.NEGATE_DOUBLE, [CelScalar.DOUBLE], CelScalar.DOUBLE, (arg) => -arg), + celOverload( + olc.NEGATE_DOUBLE, + [CelScalar.DOUBLE], + CelScalar.DOUBLE, + (arg) => -arg, + ), ]); function overflow(op: string, type: CelType) { diff --git a/packages/cel/src/std/time.ts b/packages/cel/src/std/time.ts index e612cc82..611e7a35 100644 --- a/packages/cel/src/std/time.ts +++ b/packages/cel/src/std/time.ts @@ -264,7 +264,7 @@ const getSecondsFunc = celFunc(olc.TIME_GET_SECONDS, [ olc.DURATION_TO_SECONDS, [DURATION], CelScalar.INT, - (dur) => dur.message.seconds + (dur) => dur.message.seconds, ), ]); @@ -285,7 +285,7 @@ const getMinutesFunc = celFunc(olc.TIME_GET_MINUTES, [ olc.DURATION_TO_MINUTES, [DURATION], CelScalar.INT, - (dur) => dur.message.seconds / 60n + (dur) => dur.message.seconds / 60n, ), ]); @@ -306,7 +306,7 @@ const getHoursFunc = celFunc(olc.TIME_GET_HOURS, [ olc.DURATION_TO_HOURS, [DURATION], CelScalar.INT, - (dur) => dur.message.seconds / 3600n + (dur) => dur.message.seconds / 3600n, ), ]); diff --git a/packages/cel/src/std/types.ts b/packages/cel/src/std/types.ts index 38abc179..0ed8cab9 100644 --- a/packages/cel/src/std/types.ts +++ b/packages/cel/src/std/types.ts @@ -1,3 +1,17 @@ +// Copyright 2024-2025 Buf Technologies, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + import { celTypeVariable } from "../ident.js"; import { CelScalar, diff --git a/packages/cel/src/type.ts b/packages/cel/src/type.ts index 28622de4..2e5b4a43 100644 --- a/packages/cel/src/type.ts +++ b/packages/cel/src/type.ts @@ -12,7 +12,13 @@ // See the License for the specific language governing permissions and // limitations under the License. -import { ScalarType, type DescField, type DescMessage, type Message, type MessageShape } from "@bufbuild/protobuf"; +import { + ScalarType, + type DescField, + type DescMessage, + type Message, + type MessageShape, +} from "@bufbuild/protobuf"; import { isCelList, type CelList } from "./list.js"; import { isCelMap, type CelMap } from "./map.js"; import { isCelUint, type CelUint } from "./uint.js"; @@ -22,7 +28,11 @@ import { type ReflectMap, type ReflectMessage, } from "@bufbuild/protobuf/reflect"; -import { TimestampSchema, DurationSchema, AnySchema } from "@bufbuild/protobuf/wkt"; +import { + TimestampSchema, + DurationSchema, + AnySchema, +} from "@bufbuild/protobuf/wkt"; import { isCelError, type CelError } from "./error.js"; const privateSymbol = Symbol.for("@bufbuild/cel/type"); @@ -44,7 +54,7 @@ export type CelType = export type CelNullableType = T & { readonly wrapped: T; -} +}; /** * Scalar CEL value types. @@ -117,7 +127,7 @@ export interface CelOpaqueType extends celTypeShared { readonly kind: "opaque"; readonly name: string; - readonly parameters: T[] + readonly parameters: T[]; } export interface CelTypeParamType extends celTypeShared { @@ -224,9 +234,7 @@ export function errorType(error: CelError): CelErrorType { }; } -export function opaqueType< - const T extends CelType = CelType, ->( +export function opaqueType( name: string, parameters: T[], ): CelOpaqueType { @@ -244,17 +252,15 @@ export function opaqueType< }; } -export function typeParamType( - name: string, -): CelTypeParamType { +export function typeParamType(name: string): CelTypeParamType { return { [privateSymbol]: {}, kind: "type_param", name, toString() { return name; - } - } + }, + }; } /** @@ -401,11 +407,13 @@ export function isObjectCelType(v: NonNullable): v is CelType { return privateSymbol in v; } -export function isDynCelType(v: CelType): v is typeof CelScalar.DYN | CelObjectType { +export function isDynCelType( + v: CelType, +): v is typeof CelScalar.DYN | CelObjectType { switch (v.kind) { - case 'scalar': - return v.scalar === 'dyn'; - case 'object': + case "scalar": + return v.scalar === "dyn"; + case "object": return v.desc.typeName === AnySchema.typeName; default: return false; @@ -413,21 +421,23 @@ export function isDynCelType(v: CelType): v is typeof CelScalar.DYN | CelObjectT } export function isErrorCelType(v: CelType): v is CelErrorType { - return isCelType(v) && v.kind === 'error'; + return isCelType(v) && v.kind === "error"; } -export function isDynOrErrorCelType(v: CelType): v is CelErrorType | typeof CelScalar.DYN | CelObjectType { +export function isDynOrErrorCelType( + v: CelType, +): v is CelErrorType | typeof CelScalar.DYN | CelObjectType { return isErrorCelType(v) || isDynCelType(v); } export function optionalCelType(paramType: CelType): CelOpaqueType { - return opaqueType('optional_type', [paramType]); + return opaqueType("optional_type", [paramType]); } export function isOptionalCelType(v: CelType): v is CelOpaqueType { switch (v.kind) { - case 'opaque': - return v.name === 'optional_type' + case "opaque": + return v.name === "optional_type"; default: return false; } @@ -442,7 +452,7 @@ export function maybeUnwrapOptionalCelType(v: CelType): CelType { /** * Creates an instance of a nullable type with the provided wrapped type. - * + * * Note: only primitive types are supported as wrapped types. */ export function nullableType(paramType: CelType): CelNullableType { @@ -453,7 +463,7 @@ export function nullableType(paramType: CelType): CelNullableType { } export function isNullableCelType(v: CelType): v is CelNullableType { - return isCelType(v) && 'wrapped' in v && isCelType(v.wrapped); + return isCelType(v) && "wrapped" in v && isCelType(v.wrapped); } /** @@ -472,30 +482,49 @@ export function isEquivalentCelType(self: CelType, other: CelType): boolean { return _isTypeInternal(self, other, false); } -function _isTypeInternal(self: CelType, other: CelType, checkTypeParamName: boolean): boolean { +function _isTypeInternal( + self: CelType, + other: CelType, + checkTypeParamName: boolean, +): boolean { if (self === other) { return true; } if (self.kind !== other.kind) { return false; } - if ( - (checkTypeParamName || self.kind != 'type') && - self.name != other.name - ) { + if ((checkTypeParamName || self.kind != "type") && self.name != other.name) { return false; } switch (self.kind) { - case 'list': - return _isTypeInternal(self.element, (other as CelListType).element, checkTypeParamName); - case 'map': - return _isTypeInternal(self.key, (other as CelMapType).key, checkTypeParamName) && - _isTypeInternal(self.value, (other as CelMapType).value, checkTypeParamName); - case 'type': - return _isTypeInternal(self.type, (other as CelTypeType).type, checkTypeParamName); - case 'object': + case "list": + return _isTypeInternal( + self.element, + (other as CelListType).element, + checkTypeParamName, + ); + case "map": + return ( + _isTypeInternal( + self.key, + (other as CelMapType).key, + checkTypeParamName, + ) && + _isTypeInternal( + self.value, + (other as CelMapType).value, + checkTypeParamName, + ) + ); + case "type": + return _isTypeInternal( + self.type, + (other as CelTypeType).type, + checkTypeParamName, + ); + case "object": return self.desc.typeName === (other as CelObjectType).desc.typeName; - case 'scalar': + case "scalar": return (self as CelScalarType).scalar === (other as CelScalarType).scalar; default: return false; @@ -526,54 +555,54 @@ export const ProtoScalarCELPrimitives = { export const CheckedWellKnownCELTypes: Record = { // Wrapper types. - "google.protobuf.BoolValue": nullableType(CelScalar.BOOL), - "google.protobuf.BytesValue": nullableType(CelScalar.BYTES), + "google.protobuf.BoolValue": nullableType(CelScalar.BOOL), + "google.protobuf.BytesValue": nullableType(CelScalar.BYTES), "google.protobuf.DoubleValue": nullableType(CelScalar.DOUBLE), - "google.protobuf.FloatValue": nullableType(CelScalar.DOUBLE), - "google.protobuf.Int64Value": nullableType(CelScalar.INT), - "google.protobuf.Int32Value": nullableType(CelScalar.INT), + "google.protobuf.FloatValue": nullableType(CelScalar.DOUBLE), + "google.protobuf.Int64Value": nullableType(CelScalar.INT), + "google.protobuf.Int32Value": nullableType(CelScalar.INT), "google.protobuf.UInt64Value": nullableType(CelScalar.UINT), "google.protobuf.UInt32Value": nullableType(CelScalar.UINT), "google.protobuf.StringValue": nullableType(CelScalar.STRING), // Well-known types. - 'google.protobuf.Any': objectType(AnySchema), - 'google.protobuf.Timestamp': TIMESTAMP, - 'google.protobuf.Duration': DURATION, + "google.protobuf.Any": objectType(AnySchema), + "google.protobuf.Timestamp": TIMESTAMP, + "google.protobuf.Duration": DURATION, // Json types. - "google.protobuf.ListValue": listType(CelScalar.DYN), - "google.protobuf.NullValue": CelScalar.NULL, - "google.protobuf.Struct": mapType(CelScalar.STRING, CelScalar.DYN), - "google.protobuf.Value": CelScalar.DYN, + "google.protobuf.ListValue": listType(CelScalar.DYN), + "google.protobuf.NullValue": CelScalar.NULL, + "google.protobuf.Struct": mapType(CelScalar.STRING, CelScalar.DYN), + "google.protobuf.Value": CelScalar.DYN, } as const; export function fieldDescToCelType(field: DescField) { switch (field.fieldKind) { - case 'scalar': - return ProtoScalarCELPrimitives[field.scalar] - case 'enum': + case "scalar": + return ProtoScalarCELPrimitives[field.scalar]; + case "enum": return CelScalar.INT; - case 'message': + case "message": if (CheckedWellKnownCELTypes[field.message.typeName]) { return CheckedWellKnownCELTypes[field.message.typeName]; } return objectType(field.message); - case 'list': + case "list": switch (field.listKind) { - case 'enum': - return listType(CelScalar.INT); - case 'message': - return listType(objectType(field.message)); - case 'scalar': - return listType(ProtoScalarCELPrimitives[field.scalar]); + case "enum": + return listType(CelScalar.INT); + case "message": + return listType(objectType(field.message)); + case "scalar": + return listType(ProtoScalarCELPrimitives[field.scalar]); } - case 'map': + case "map": const keyType = ProtoScalarCELPrimitives[field.mapKey]; switch (field.mapKind) { - case 'enum': + case "enum": return mapType(keyType, CelScalar.INT); - case 'message': + case "message": return mapType(keyType, objectType(field.message)); - case 'scalar': + case "scalar": return mapType(keyType, ProtoScalarCELPrimitives[field.scalar]); } } @@ -592,7 +621,6 @@ export function isAssignableType(t: CelType, fromType: CelType): boolean { return defaultIsAssignableType(t, fromType); } - /** * defaultIsAssignableType provides the standard definition of what it means for one type to be assignable to another * where any of the following may return a true result: @@ -609,24 +637,26 @@ function defaultIsAssignableType(t: CelType, fromType: CelType): boolean { return false; } switch (t.kind) { - case 'list': - if (fromType.kind !== 'list') { + case "list": + if (fromType.kind !== "list") { return false; } return isAssignableType(t.element, fromType.element); - case 'map': - if (fromType.kind !== 'map') { + case "map": + if (fromType.kind !== "map") { return false; } - return isAssignableType(t.key, fromType.key) && - isAssignableType(t.value, fromType.value); - case 'type': - if (fromType.kind !== 'type') { + return ( + isAssignableType(t.key, fromType.key) && + isAssignableType(t.value, fromType.value) + ); + case "type": + if (fromType.kind !== "type") { return false; } return isAssignableType(t.type, fromType.type); - case 'opaque': - if (fromType.kind !== 'opaque') { + case "opaque": + if (fromType.kind !== "opaque") { return false; } if (t.parameters.length !== fromType.parameters.length) { @@ -641,4 +671,4 @@ function defaultIsAssignableType(t: CelType, fromType: CelType): boolean { default: return true; } -} \ No newline at end of file +} diff --git a/packages/example/src/example.ts b/packages/example/src/example.ts index 8a446ebc..3d00f6b3 100644 --- a/packages/example/src/example.ts +++ b/packages/example/src/example.ts @@ -16,7 +16,7 @@ import { celEnv, CelScalar, celFunc, - celMemberOverload, + celOverload, parse, plan, run, @@ -51,9 +51,9 @@ console.log(result); // true // Provide a new function: const similar = celFunc("similar", [ - celMemberOverload( - // Overload ID. - 'similar_string', + celOverload( + // Overload name. + "similar_string_string", // Parameter types. [CelScalar.STRING, CelScalar.STRING], // Return type. From 53b0e8bc150899123e70c3f48e8a30bd1cda8388 Mon Sep 17 00:00:00 2001 From: jafaircl Date: Sat, 17 Jan 2026 21:57:16 -0500 Subject: [PATCH 5/5] add mergeFuncs --- packages/cel/src/checker/checker.ts | 4 +- packages/cel/src/checker/env.ts | 15 +-- packages/cel/src/func.ts | 179 +++++++++++++++++++++++----- packages/cel/src/ident.ts | 35 +++--- packages/cel/src/std/cast.ts | 4 +- 5 files changed, 183 insertions(+), 54 deletions(-) diff --git a/packages/cel/src/checker/checker.ts b/packages/cel/src/checker/checker.ts index a3511b34..99f64c6b 100644 --- a/packages/cel/src/checker/checker.ts +++ b/packages/cel/src/checker/checker.ts @@ -70,7 +70,7 @@ import { identReference, type ReferenceInfo, } from "../referenceinfo.js"; -import type { CelFunc } from "../func.js"; +import { overloadTypeParams, type CelFunc } from "../func.js"; import { LOGICAL_AND, LOGICAL_OR, @@ -556,7 +556,7 @@ export class _CelChecker implements CelChecker { overload.result, ...overload.parameters, ); - let typeParams = overload.typeParams(); + let typeParams = overloadTypeParams(overload); if (typeParams.length > 0) { // Instantiate overload's type with fresh type variables. const substitutions = new Mapping(); diff --git a/packages/cel/src/checker/env.ts b/packages/cel/src/checker/env.ts index 8f659845..6a4d208c 100644 --- a/packages/cel/src/checker/env.ts +++ b/packages/cel/src/checker/env.ts @@ -18,8 +18,13 @@ import type { Registry } from "@bufbuild/protobuf"; import { Namespace } from "../namespace.js"; import { Group, Scopes } from "./scopes.js"; import type { CelFunc } from "../func.js"; +import { mergeFuncs } from "../func.js"; import type { CelIdent } from "../ident.js"; -import { celConstant, celVariable } from "../ident.js"; +import { + celConstant, + celVariable, + identDeclarationIsEquivalent, +} from "../ident.js"; import { createRegistryWithWKT } from "../registry.js"; import { CelScalar, objectType } from "../type.js"; import { STD_FUNCS } from "../std/std.js"; @@ -310,11 +315,7 @@ class _CelCheckerEnv implements CelCheckerEnv { if (!current) { current = fn; } else { - // TODO: merge overloads - // current = current.merge(fn) - return [ - `function ${fn.name} already declared. merging overloads not yet supported`, - ]; + current = mergeFuncs(current, fn); } // TODO: check macros // for (const overload of current.overloads) { @@ -340,7 +341,7 @@ class _CelCheckerEnv implements CelCheckerEnv { #addIdent(ident: CelIdent): string | null { const current = this.declarations.findIdentInScope(ident.name); if (current) { - if (current.declarationIsEquivalent(ident)) { + if (identDeclarationIsEquivalent(current, ident)) { return null; } return `overlapping identifier for name '${ident.name}'`; diff --git a/packages/cel/src/func.ts b/packages/cel/src/func.ts index 8220b2f4..fd115efa 100644 --- a/packages/cel/src/func.ts +++ b/packages/cel/src/func.ts @@ -20,6 +20,8 @@ import { isCelType, type CelValue, type CelInput, + isAssignableType, + isEquivalentCelType, } from "./type.js"; import { type CelResult, @@ -94,10 +96,6 @@ export interface CelOverload

{ * Whether this is a member function overload. */ readonly isMemberFunction: boolean; - /** - * TypeParams returns the type parameter names associated with the overload. - */ - typeParams(): string[]; } /** @@ -202,32 +200,40 @@ class FuncOverload get isMemberFunction() { return this._isMemberFunction; } - typeParams(): string[] { - function collectParamNames(paramNames: string[], arg: CelType) { - switch (arg.kind) { - case "type_param": - paramNames.push(arg.name); - break; - case "list": - collectParamNames(paramNames, arg.element); - break; - case "map": - collectParamNames(paramNames, arg.key); - collectParamNames(paramNames, arg.value); - break; - default: - break; - } - } - const typeNames: string[] = []; - collectParamNames(typeNames, this._result); - for (const paramType of this._parameters) { - collectParamNames(typeNames, paramType); - } - return typeNames; +} + +function collectParamNames(paramNames: string[], arg: CelType) { + switch (arg.kind) { + case "type_param": + paramNames.push(arg.name); + break; + case "list": + collectParamNames(paramNames, arg.element); + break; + case "map": + collectParamNames(paramNames, arg.key); + collectParamNames(paramNames, arg.value); + break; + default: + break; } } +/** + * TypeParams returns the type parameter names associated with the overload. + */ +export function overloadTypeParams< + P extends readonly CelType[], + R extends CelType, +>(func: CelOverload): string[] { + const typeNames: string[] = []; + collectParamNames(typeNames, func.result); + for (const paramType of func.parameters) { + collectParamNames(typeNames, paramType); + } + return typeNames; +} + /** * Set of functions uniquely identified by their name. */ @@ -357,3 +363,122 @@ function unwrapResults(args: CelResult[]) { } return vals; } + +/** + * SignatureOverlaps indicates whether two functions have non-equal, but overloapping function signatures. + * + * For example, list(dyn) collides with list(string) since the 'dyn' type can contain a 'string' type. + */ +export function overloadSignatureOverlaps< + P extends readonly CelType[], + R extends CelType, +>(overload: CelOverload, other: CelOverload): boolean { + if ( + overload.isMemberFunction !== other.isMemberFunction || + overload.parameters.length !== other.parameters.length + ) { + return false; + } + let argsOverlap = true; + for (let i = 0; i < overload.parameters.length; i++) { + const argType = overload.parameters[i]; + const otherArgType = other.parameters[i]; + argsOverlap = + argsOverlap && + (isAssignableType(argType, otherArgType) || + isAssignableType(otherArgType, argType)); + } + return argsOverlap; +} + +/** + * SignatureEquals determines whether the incoming overload declaration signature is equal to the current signature. + * + * Result type, operand trait, and strict-ness are not considered as part of signature equality. + */ +export function overloadSignatureEquals< + P extends readonly CelType[], + R extends CelType, +>(overload: CelOverload, other: CelOverload): boolean { + if (overload === other) { + return true; + } + if ( + overload.id !== other.id || + overload.isMemberFunction !== other.isMemberFunction || + overload.parameters.length !== other.parameters.length + ) { + return false; + } + for (let i = 0; i < overload.parameters.length; i++) { + const argType = overload.parameters[i]; + const otherArgType = other.parameters[i]; + if (!isEquivalentCelType(argType, otherArgType)) { + return false; + } + } + return isEquivalentCelType(overload.result, other.result); +} + +/** + * AddOverload ensures that the new overload does not collide with an existing overload signature; + * however, if the function signatures are identical, the implementation may be rewritten as its + * difficult to compare functions by object identity. + */ +export function addOverloadToFunc( + func: CelFunc, + overload: CelOverload, +): void { + for (let i = 0; i < func.overloads.length; i += 1) { + const o = func.overloads[i]; + if (o.id !== overload.id && overloadSignatureOverlaps(o, overload)) { + throw new Error( + `overload signature collision in function ${func.name}: ${o.id} collides with ${overload.id}`, + ); + } + if (o.id === overload.id) { + if (overloadSignatureEquals(o, overload)) { + // Allow redefinition of an overload implementation so long as the signatures match. + func.overloads[i] = overload; + // TODO: Allow redefinition of the doc string. + // if len(overload.doc) != 0 && o.doc != overload.doc { + // o.doc = overload.doc + // } + return; + } + throw new Error( + `overload redefinition in function. ${func.name}: ${o.id} has multiple definitions`, + ); + } + } + func.overloads.push(overload); +} + +/** + * Merge combines an existing function declaration with another. + * + * If a function is extended, by say adding new overloads to an existing function, then it is merged with the + * prior definition of the function at which point its overloads must not collide with pre-existing overloads + * and its bindings (singleton, or per-overload) must not conflict with previous definitions either. + */ +export function mergeFuncs(f: CelFunc, other: CelFunc): CelFunc { + if (f === other) { + return f; + } + if (f.name !== other.name) { + throw new Error( + `cannot merge unrelated functions. ${f.name} and ${other.name}`, + ); + } + const merged = celFunc(f.name, []); + + // baseline copy of the overloads + for (const overload of f.overloads) { + merged.overloads.push(overload); + } + // add new overloads + for (const overload of other.overloads) { + addOverloadToFunc(merged, overload); + } + return merged; +} diff --git a/packages/cel/src/ident.ts b/packages/cel/src/ident.ts index eecfd195..83287bda 100644 --- a/packages/cel/src/ident.ts +++ b/packages/cel/src/ident.ts @@ -34,12 +34,6 @@ export interface CelIdent { readonly value?: CelValue; readonly doc?: string; - - /** - * DeclarationIsEquivalent returns true if one variable declaration has the - * same name and same type as the input. - */ - declarationIsEquivalent(other: CelIdent): boolean; } /** @@ -81,17 +75,24 @@ class Ident implements CelIdent { public readonly value?: CelValue, public readonly doc?: string, ) {} +} - declarationIsEquivalent(other: CelIdent): boolean { - if (this === other) { - return true; - } - // If either type is undefined, we cannot be equivalent. - if (!this.type || !other.type) { - return false; - } - return ( - this.name === other.name && isEquivalentCelType(this.type, other.type) - ); +/** + * DeclarationIsEquivalent returns true if one variable declaration has the + * same name and same type as the input. + */ +export function identDeclarationIsEquivalent( + ident: CelIdent, + other: CelIdent, +): boolean { + if (ident === other) { + return true; + } + // If either type is undefined, we cannot be equivalent. + if (!ident.type || !other.type) { + return false; } + return ( + ident.name === other.name && isEquivalentCelType(ident.type, other.type) + ); } diff --git a/packages/cel/src/std/cast.ts b/packages/cel/src/std/cast.ts index 7d6a92d9..27d824b6 100644 --- a/packages/cel/src/std/cast.ts +++ b/packages/cel/src/std/cast.ts @@ -155,7 +155,9 @@ const boolFunc = celFunc(BOOL, [ const bytesFunc = celFunc(BYTES, [ celOverload(olc.BYTES_TO_BYTES, [CelScalar.BYTES], CelScalar.BYTES, (x) => x), - celOverload(olc.STRING_TO_BYTES, [CelScalar.STRING], CelScalar.BYTES, (x) => encoder.encode(x)), + celOverload(olc.STRING_TO_BYTES, [CelScalar.STRING], CelScalar.BYTES, (x) => + encoder.encode(x), + ), ]); const stringFunc = celFunc(STRING, [