Skip to content

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

@xjasonli

Description

@xjasonli

Area: Runtime, Function Overloading, Type System

Description:

Currently, it appears impossible to register function overloads into the cel::FunctionRegistry that differ only in the specific parameter types of container arguments (like list, map, message, or opaque types identified by cel::Kind::kList, cel::Kind::kMap, cel::Kind::kStruct, cel::Kind::kOpaque), while having the same function name, receiver style, arity, and argument cel::Kind. Support for types like optional might also be affected depending on their internal representation.

Example Scenario:

Consider defining two functions in the type checker (OverloadDecl) with signatures like:

  1. my_func(list<int>)
  2. my_func(list<string>)

Both are non-member functions with one argument.

When attempting to register corresponding implementations using FunctionRegistry::Register, the second registration fails.

Root Cause:

The FunctionRegistry::Register method checks for existing overloads using FunctionRegistry::DescriptorRegistered. This function, in turn, relies on cel::FunctionDescriptor::ShapeMatches to compare the new descriptor with existing ones.

ShapeMatches compares:

  • receiver_style()
  • Argument count (types().size())
  • Argument cel::Kind for each parameter (e.g., kList, kMap, kOpaque).

In the example above, both list<int> and list<string> have the cel::Kind::kList. Therefore, ShapeMatches considers them identical in shape, DescriptorRegistered returns true for the second registration attempt, and Register returns absl::StatusCode::kAlreadyExists.

This limitation prevents users from defining and registering runtime function overloads that leverage the more precise type information available in cel::Type (used during type checking) for container types.

Impact:

Users cannot implement fine-grained function overloading based on specific container element/key/value types at the runtime registration level using the standard FunctionRegistry.

The "Empty Container" Problem:

A potential workaround might involve registering a single "dispatcher" function implementation (overload_x) for the generic Kind (e.g., my_func(list) where the descriptor uses Kind::kList). Inside this function, one could inspect the runtime cel::Value's actual type using value->GetRuntimeType() to dispatch to the correct underlying implementation (e.g., the one for list<int> or list<string>).

However, this approach faces a significant challenge with empty containers:

  • If a non-empty list<int> is passed, value->GetRuntimeType() would likely return a ListType whose element_type() corresponds to int64, allowing correct dispatch.
  • If an empty list ([]) is passed, the resulting cel::Value's runtime type (value->GetRuntimeType()) might return a ListType whose element_type() is DynType (corresponding to TypeKind::kDyn), as suggested by the default construction of ListType and the existence of DynType. In this case, the dispatcher function lacks the necessary information to determine whether the caller intended this empty list to be notionally a list<int> or a list<string>, making correct dispatch impossible based solely on the runtime value's potentially dynamic type information. A similar issue arises for empty maps where key/value types might resolve to DynType.

Potential Solution Idea & Challenges (As discussed):

One theoretical approach to resolve the empty container ambiguity could involve utilizing the type information generated by the type checker, which is often stored in maps like reference_map or type_map within the AstImpl. This map contains the precise cel::Type inferred for expressions, including empty literal containers.

However, pursuing this faces hurdles:

  1. Information Availability: The current cel::runtime::Program evaluation interface does not seem to provide a standard mechanism to pass this detailed AST type map information (type_map/reference_map) from the type checking phase into the runtime execution environment (e.g., to the cel::vm::ExecutionFrame or ActivationInterface).
  2. Dispatch Mechanism: Even if this type information were available at runtime, the FunctionRegistry's current lookup and the cel::vm::FunctionStep's execution logic are primarily driven by the FunctionDescriptor (based on cel::Kind). The registry itself, having accepted only one entry based on Kind, has lost the direct link between the specific overload_id (from the type checking phase) and the runtime function implementation. Modifying FunctionStep to use the external AST type map for dispatch based on overload ID would require significant changes to the evaluation core and the function call mechanism.

Affected Components:

  • cel::FunctionRegistry (Registration logic using Kind)
  • cel::FunctionDescriptor::ShapeMatches (Comparison logic based on Kind)
  • Runtime evaluation (cel::runtime::Program, cel::vm::FunctionStep, cel::Value::GetRuntimeType()) and its handling of empty containers (potentially yielding DynType).
  • Interaction between Type Checking (cel::Type, OverloadDecl) and Runtime (cel::Kind, cel::Value).

Request:

We request consideration of this limitation. Ideally, the runtime function registration and dispatch mechanism should allow for distinguishing overloads based on the precise cel::Type of container parameters, possibly by enhancing FunctionDescriptor or the registration/lookup process, and addressing the challenge of dispatching correctly even for empty containers potentially represented with DynType at runtime.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions