Skip to content

Use overload_id for checked function overload resolution - #2115

Open
xjasonli wants to merge 2 commits into
cel-expr:masterfrom
xjasonli:feat/runtime-function-overload-resolution
Open

xjasonli wants to merge 2 commits into
cel-expr:masterfrom
xjasonli:feat/runtime-function-overload-resolution

Conversation

@xjasonli

Copy link
Copy Markdown

Fixes #1484.

This is a follow-up to #1530, rewritten around the approach suggested in that discussion.

The previous version tried to pass overload information through the runtime function invocation path. That made the overridable function interface harder to keep compatible. This version takes a different route: FlatExprBuilder uses the checker’s overload_id information to narrow the candidate set ahead of time, and the runtime registry records which overload ID each implementation handles.

This PR is a bit large because the registry, planner, runtime dispatch, standard function registrations, and tests need to move together to keep the tree green. If this is too large to review or sync comfortably, I am happy to discuss how to split it into smaller reviewable pieces.

What changed

This PR adds overload_id-aware function overload resolution for checked expressions:

  • FunctionDescriptor can carry an overload_id and full cel::Type information.
  • FunctionRegistry can look up static and lazy overloads by overload ID.
  • ActivationInterface can look up activation-provided overloads by overload ID.
  • FlatExprBuilder reads overload IDs from the checked expression reference_map and prefers the overloads selected by the checker.
  • Runtime matching still validates actual argument values and keeps fallback candidates for dyn/runtime mismatch cases.

This PR also annotates standard library, optional types, and extension runtime function registrations with the overload IDs declared by the checker.

Why

Today, checked expressions can contain more precise overload information than the runtime normally uses during dispatch. For example, the checker may distinguish overloads such as list<int> and list<string>, while the default runtime matching path only sees both arguments as cel::Kind::kList.

This change uses a two-part approach:

  • At plan time, checked-expression overload_id data narrows the candidate set to the overloads selected by the checker.
  • At runtime, enable_type_level_overload can enable type-level verification for cases where precise container element/key/value types matter.

The overload_id path also helps with empty containers, where runtime values may not carry enough element/key/value type information to disambiguate overloads on their own.

Compatibility

The main compatibility note is FunctionDescriptor:

  • FunctionDescriptor::types() now returns const std::vector<cel::Type>&.
  • FunctionDescriptor::kinds() returns the previous kind-level view: const std::vector<cel::Kind>&.

Apart from that API adjustment, the runtime behavior is intended to stay compatible by default:

  • overload_id-based candidate filtering is used for checked expressions when reference_map overload information is available.
  • Kind-level runtime matching remains the default because RuntimeOptions::enable_type_level_overload defaults to false.
  • Type-level runtime matching is only enabled when RuntimeOptions::enable_type_level_overload is set to true.
  • Functions without overload IDs continue to use the existing arity/kind fallback matching path.

@xjasonli
xjasonli force-pushed the feat/runtime-function-overload-resolution branch from ff56407 to c84e883 Compare September 17, 2026 10:30
@xjasonli

Copy link
Copy Markdown
Author

Hi @jnthntatum, cc @jcking — I’ve rebased this PR onto the latest master and force-pushed the updated commits.

The changes remain separated into two commits:

  1. Overload ID resolution infrastructure and optional type-level runtime matching.
  2. Runtime overload ID annotations for standard library, optional type, and extension functions.

All tests pass locally with bazel test //....

The Cloud Build check currently reports “Needs /gcbrun from a collaborator,” and the workflow security scan is awaiting maintainer approval. Could a collaborator help trigger these checks?

Following your guidance in #1530, this implementation uses plan-time overload selection and registry metadata without changing the function invocation interface. I’d appreciate feedback on whether the current approach and commit boundaries are suitable for review and Piper synchronization, or whether smaller dependent PRs would be preferable.

Two compatibility points I’d particularly like to confirm:

  • Checked-expression overload ID selection is currently enabled by default; only type-level runtime matching is opt-in. Should plan-time selection also be gated by an interpreter option?
  • FunctionDescriptor::types() now returns cel::Type values, with kinds() exposing the previous view. Would you prefer preserving the existing accessor and adding a separate accessor for full types?

I’m happy to coordinate the integration steps here or in #1484. Thanks for your time!

@jcking

jcking commented Oct 15, 2026

Copy link
Copy Markdown
Collaborator

Unfortunately we are unable to take such a large PR. This is due to a lot of inflight changes that will conflict. We are going to take a more gradual approach by adding overload_id to FunctionDescriptor, using it for type-checked expressions where type checking was able to pick a single overload, and fallback to the existing matching. In the future we will likely implement the full overload matching but not for now.

There is also the question of Type vs TypeSpec, that we ourselves have not decided on. We are likely to smash the two together, and do not want to bake in a bunch of logic until that decision is made.

…nctions

Prefer checked overload IDs while validating runtime arguments, then retain
ordered arity fallbacks for parse-only, unknown-ID, and legacy registrations.
Kind matching remains the default. The opt-in type mode validates concrete
containers and solves independently scoped registration/implementation types
with per-occurrence evidence for shared generic parameters.

Share runtime acceptance between static and lazy calls. Filter actual values
and implementation strictness before provider ambiguity, preserve operational
statuses and CEL exceptions, and avoid unnecessary container observations.
Keep nonoptional opaque metadata atomic and preserve nominal struct identity.

Enforce per-name overload ID uniqueness in both activations, retain old provider
subclasses, and disambiguate legacy descriptor/adapter calls through Nary arities.
Test the fixed runtime/API matrix and deterministic independent overlap oracle.

CEL reference implementations guide overload identity, scoped generic variables,
and strict evaluation. Full runtime generic/container unification is a C++
extension behind enable_type_level_overload, not reference-language equivalence.
Bind standard, optional, and extension registrations to the checker-declared
overload IDs, retaining dynamic lookup where checker/runtime overload shapes
do not map one to one. Add the bool-to-int and optional unwrap declarations
needed for those annotations.

Return the selected OptionalValue from optional.or without unwrapping it, reject
nonoptional opaque fallback values when selected, and retain present-receiver
short circuiting. Cover direct or/orValue boundaries and evaluator error
fallbacks in both matching modes.
@xjasonli
xjasonli force-pushed the feat/runtime-function-overload-resolution branch from c84e883 to e9c7376 Compare October 20, 2026 09:46
@xjasonli

Copy link
Copy Markdown
Author

Thanks, @jcking. I understand the concern about the PR’s size and the other changes in progress. I’m happy to split the work into smaller PRs. We can first agree on their scope and order.

I’ve rebased the branch onto master and force-pushed it. This includes your FunctionDescriptor::overload_id change. Regression and conformance tests pass locally on both commits. The updated branch can serve as a reference.

For the first smaller PR, we could follow your suggestion. If the type checker selects exactly one overload, use its overload_id to find the implementation. Keep the existing matching as the fallback.

Later PRs could add more overload IDs to standard-library, optional, and extension functions.

I understand you may combine Type and TypeSpec. The design is still undecided. I agree that full type-level matching can wait until that design is clearer.

I also checked the current TypeSpec code. Two points may be useful when combining the types:

  • There is no TypeSpec variant for UnknownType.
  • ConvertTypeToTypeSpec() rejects non-message struct types, such as BasicStructType. This branch supports those types.

Do you already have plans for any of these steps? I’d be happy to coordinate the scope and order of the smaller PRs with you.

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.

FunctionRegistry cannot distinguish overloads differing only by container parameter types (e.g., list<int> vs list<string>)

2 participants