Skip to content

Conversation

@jafaircl
Copy link
Contributor

@jafaircl jafaircl commented Dec 2, 2025

No description provided.

const P extends readonly CelType[],
const R extends CelType,
>(
id: string,
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@jafaircl why was it necessary to add the ID back? We had it previously, but didn't find it useful.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It’s used when resolving overloads to determine if the overload should be disabled (i.e. cross type numeric comparisons or user defined disabled overloads). See the resolveOverload method on line 522 in checker.ts. It’s also needed for reference info which is part of the cel spec (https://github.com/google/cel-spec/blob/121e265b0c5e1d4c7d5c140b33d6048fec754c77/proto/cel/expr/checked.proto#L387)

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The latter may be unavoidable, but I was already inclined to question the need for configurability on cross-type numeric comparisons. The spec itself is unambiguous, and I assume the Google implementations only provide it for historical reasons. What do you think?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think there may be valid reasons in some environments to disable cross type numeric comparisons. Since it's part of the spec, I'm inclined to believe there's a reason it hasn't been removed or marked as deprecated. Maybe that's historical. But, it could also be used again in the future. Since every other reference implementation includes it, I would be hesitant to remove it.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I removed the id as it was not used at the time. Because we have all the types, can we derive the id?

* The environment defines the functions and types that are available
* during CEL expression checking.
*/
export interface CelCheckerEnv {
Copy link

@hudlow hudlow Dec 9, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I recognize this may be painful, so feel free to push back (and I'd like to hear @timostamm and/or @srikrsna-buf's thoughts) but I'd like to avoid having multiple "environment" implementations. cel-go is really hard to work with, in part because for any given noun there are seven different abstractions all related to each other in some convoluted way.

For cel-es if we could follow the rule that "an environment is an environment" I think we'd be better for it.

The ontological relationships of "declarations," "scopes," "groups," "namespaces," and "registries" is also going to be a very steep learning curve for the uninitiated. Let's try to narrow the terminology and use the terms very crisply if possible.

/**
* Idents available in this environment.
*/
idents?: CelIdent[];
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why not just "variables"?

Comment on lines +126 to +129
/**
* Whether to allow cross-type numeric comparisons.
*/
crossTypeNumericComparisons?: boolean;
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As mentioned on the other thread, I'm not convinced we need this flexibility.

Comment on lines +218 to +249
/**
* 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"));
}
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let's eliminate mutation if possible, and require variables and functions to be provided when an environment or scope is created.

* LookupIdent returns an identifier in the Env.
* Returns undefined if no such identifier is found in the Env.
*/
lookupIdent(name: string): CelIdent | undefined {
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The compound word lookup is a noun, only — this should be lookUpIdent (or lookUpVariable)

Comment on lines +36 to +49
/**
* 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;
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we call this a Scope, singular, and eliminate the Group abstraction altogether? Also, can we eliminate mutation from this class?

/**
* A CEL ident definition.
*/
export interface CelIdent {
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we just make this CelVariable?

public readonly doc?: string,
) {}

declarationIsEquivalent(other: CelIdent): boolean {
Copy link

@hudlow hudlow Dec 9, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Eliminate the term "declaration" here — it's not clearly defined.

@hudlow
Copy link

hudlow commented Dec 9, 2025

@jafaircl Thanks for all the hard work on this — I tried to give some useful feedback, but please let me know if anything isn't making sense to you. Or, if you'd like me to take a swing at any of my suggestions.

@hudlow
Copy link

hudlow commented Dec 10, 2025

@jafaircl For visibility, here's the POC TypeScript API I wrote:

import { CelResult } from "./error.ts";
import { CelType, CelInput, CelScalar } from "./type.ts";

interface Variable<N extends string = string, T extends CelType = CelType> {
  readonly name: N;
  readonly type: T;
}

type CelVariableTuple<T extends readonly Variable[]> =
  T extends readonly [
    Variable<infer Name extends string, infer Type extends CelType>,
    ...infer Rest extends Variable[],
  ]
    ? [Variable<Name, Type>, ...CelVariableTuple<Rest>]
    : Variable[] extends T ? any[] : [];

type VariableInputEntry<V extends Variable> = {
  [key in V['name']]: CelInput<V['type']>
}

type CelInputMap<T extends CelVariableTuple<readonly Variable[]>> =
  T extends readonly [
    infer V extends Variable,
    ...infer Rest extends Variable[],
  ] ? VariableInputEntry<V> & CelInputMap<Rest> : {};

function plan<
  const T extends CelVariableTuple<readonly Variable[]>,
>(variables: T): ((bindings: CelInputMap<T>) => CelResult) {
  return () => variables.length; // fake program
}

const program = plan([
  { name: "foo", type: CelScalar.STRING },
  { name: "bar", type: CelScalar.INT },
]);

program({ foo: "string", bar: 1n });

EDIT: @srikrsna-buf suggests changing the declarations to an object to match the bindings, which makes this a lot simpler:

import { CelScalar, type CelType, mapType, type CelInput } from "./type.js";

export type BindingDeclaration = {
  [key: string]: CelType;
};

export type Binding<T extends BindingDeclaration> = {
  [P in keyof T]: CelInput<T[P]>;
};

function plan<const T extends BindingDeclaration>(
  binding: T,
): (params: Binding<T>) => void {
  return () => {};
}

const program = plan({
  foo: CelScalar.STRING,
  bar: CelScalar.INT,
});

program({ foo: "string", bar: 1n });

@CLAassistant
Copy link

CLA assistant check
Thank you for your submission! We really appreciate it. Like many open source projects, we ask that you all sign our Contributor License Agreement before we can accept your contribution.
1 out of 2 committers have signed the CLA.

✅ jafaircl
❌ Dan Hudlow


Dan Hudlow seems not to be a GitHub user. You need a GitHub account to be able to sign the CLA. If you have already a GitHub account, please add the email address used for this commit to your account.
You have signed the CLA already but the status is still pending? Let us recheck it.

@jafaircl
Copy link
Contributor Author

Since there have been so many breaking changes, this would have to be completely rewritten. I'm not even sure if some of the changes you have made are compatible with the way all other type checking implementations work. Surely something in this PR is salvageable. It's just not immediately clear what that is. Is this something you as a team would prefer to do? I don't want to waste time if it's something you are going to work on internally or not something you are interested in implementing anymore.

@srikrsna-buf
Copy link
Member

Is this something you as a team would prefer to do? I don't want to waste time if it's something you are going to work on internally or not something you are interested in implementing anymore.

We are very much interested in adding a type checker. We are not working on it internally at this point, but will be happy to review and accept a contribution. I understand if you don't want to work on this anymore, but the recent changes were done in response to this PR to trim this down to just the type checker implementation.

If you do decide to pick this up again, I think it is best if we start by agreeing on how the public API is going to end up looking and on a general approach to the implementation. Then, implementing this over several PRs with unit tests (when relevant) will make it significantly easier to review and integrate.

Since there have been so many breaking changes, this would have to be completely rewritten. I'm not even sure if some of the changes you have made are compatible with the way all other type checking implementations work.

The intent was to make it more idiomatic for the TS/JS ecosystem. We are already quite different from other implementations where we don't always box values and function implementations are type-safe. If, however, there is a limitation in the current implementation that makes it impossible or difficult to implement a type checker, please let us know and we'd be happy to address it.

Thank you for your interest and contributions so far!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants