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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
29 changes: 27 additions & 2 deletions runtime/src/main/java/dev/cel/runtime/CelFunctionBinding.java
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,20 @@ public interface CelFunctionBinding {
@SuppressWarnings("unchecked")
static <T> CelFunctionBinding from(
String overloadId, Class<T> arg, CelFunctionOverload.Unary<T> impl) {
return from(overloadId, ImmutableList.of(arg), (args) -> impl.apply((T) args[0]));
return from(
overloadId,
ImmutableList.of(arg),
new CelFunctionOverload() {
@Override
public Object apply(Object[] args) throws CelEvaluationException {
return impl.apply((T) args[0]);
}

@Override
public Object apply(Object arg1) throws CelEvaluationException {
return impl.apply((T) arg1);
}
});
}

/**
Expand All @@ -65,7 +78,19 @@ static <T> CelFunctionBinding from(
static <T1, T2> CelFunctionBinding from(
String overloadId, Class<T1> arg1, Class<T2> arg2, CelFunctionOverload.Binary<T1, T2> impl) {
return from(
overloadId, ImmutableList.of(arg1, arg2), (args) -> impl.apply((T1) args[0], (T2) args[1]));
overloadId,
ImmutableList.of(arg1, arg2),
new CelFunctionOverload() {
@Override
public Object apply(Object[] args) throws CelEvaluationException {
return impl.apply((T1) args[0], (T2) args[1]);
}

@Override
public Object apply(Object arg1, Object arg2) throws CelEvaluationException {
return impl.apply((T1) arg1, (T2) arg2);
}
});
}

/** Create a function binding from the {@code overloadId}, {@code argTypes}, and {@code impl}. */
Expand Down
61 changes: 45 additions & 16 deletions runtime/src/main/java/dev/cel/runtime/CelFunctionOverload.java
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,16 @@ public interface CelFunctionOverload {
/** Evaluate a set of arguments throwing a {@code CelException} on error. */
Object apply(Object[] args) throws CelEvaluationException;

/** Fast-path for unary function execution to avoid Object[] allocation. */
default Object apply(Object arg) throws CelEvaluationException {
return apply(new Object[] {arg});
}

/** Fast-path for binary function execution to avoid Object[] allocation. */
default Object apply(Object arg1, Object arg2) throws CelEvaluationException {
return apply(new Object[] {arg1, arg2});
}

/**
* Helper interface for describing unary functions where the type-parameter is used to improve
* compile-time correctness of function bindings.
Expand Down Expand Up @@ -57,27 +67,46 @@ static boolean canHandle(
for (int i = 0; i < parameterTypes.size(); i++) {
Class<?> paramType = parameterTypes.get(i);
Object arg = arguments[i];
if (arg == null) {
// null can be assigned to messages, maps, and to objects.
// TODO: Remove null special casing
if (paramType != Object.class && !Map.class.isAssignableFrom(paramType)) {
return false;
}
continue;
boolean result = canHandleArg(arg, paramType, isStrict);
if (!result) {
return false;
}
}
return true;
}

if (arg instanceof Exception || arg instanceof CelUnknownSet) {
// Only non-strict functions can accept errors/unknowns as arguments to a function
if (!isStrict) {
// Skip assignability check below, but continue to validate remaining args
continue;
}
}
static boolean canHandle(Object arg, ImmutableList<Class<?>> parameterTypes, boolean isStrict) {
if (parameterTypes.size() != 1) {
return false;
}
return canHandleArg(arg, parameterTypes.get(0), isStrict);
}

static boolean canHandle(
Object arg1, Object arg2, ImmutableList<Class<?>> parameterTypes, boolean isStrict) {
if (parameterTypes.size() != 2) {
return false;
}
return canHandleArg(arg1, parameterTypes.get(0), isStrict)
&& canHandleArg(arg2, parameterTypes.get(1), isStrict);
}

if (!paramType.isAssignableFrom(arg.getClass())) {
static boolean canHandleArg(Object arg, Class<?> paramType, boolean isStrict) {
// null can be assigned to messages, maps, and to objects.
// TODO: Remove null special casing
if (arg == null) {
if (paramType != Object.class && !Map.class.isAssignableFrom(paramType)) {
return false;
}
return true;
}
return true;

if (arg instanceof Exception || arg instanceof CelUnknownSet) {
if (!isStrict) {
return true;
}
}

return paramType.isAssignableFrom(arg.getClass());
}
}
66 changes: 39 additions & 27 deletions runtime/src/main/java/dev/cel/runtime/DefaultDispatcher.java
Original file line number Diff line number Diff line change
Expand Up @@ -200,19 +200,48 @@ public DefaultDispatcher build() {
for (Map.Entry<String, OverloadEntry> entry : overloads.entrySet()) {
String overloadId = entry.getKey();
OverloadEntry overloadEntry = entry.getValue();
CelFunctionOverload overloadImpl = overloadEntry.overload();

CelFunctionOverload guardedApply;
if (overloadImpl instanceof DynamicDispatchOverload) {
// Dynamic dispatcher already does its own internal canHandle checks
guardedApply = overloadImpl;
} else {
boolean isStrict = overloadEntry.isStrict();
ImmutableList<Class<?>> argTypes = overloadEntry.argTypes();

guardedApply =
new CelFunctionOverload() {
@Override
public Object apply(Object[] args) throws CelEvaluationException {
if (CelFunctionOverload.canHandle(args, argTypes, isStrict)) {
return overloadImpl.apply(args);
}
throw new CelOverloadNotFoundException(overloadId);
}

@Override
public Object apply(Object arg) throws CelEvaluationException {
if (CelFunctionOverload.canHandle(arg, argTypes, isStrict)) {
return overloadImpl.apply(arg);
}
throw new CelOverloadNotFoundException(overloadId);
}

@Override
public Object apply(Object arg1, Object arg2) throws CelEvaluationException {
if (CelFunctionOverload.canHandle(arg1, arg2, argTypes, isStrict)) {
return overloadImpl.apply(arg1, arg2);
}
throw new CelOverloadNotFoundException(overloadId);
}
};
}

resolvedOverloads.put(
overloadId,
CelResolvedOverload.of(
overloadId,
args ->
guardedOp(
overloadId,
args,
overloadEntry.argTypes(),
overloadEntry.isStrict(),
overloadEntry.overload()),
overloadEntry.isStrict(),
overloadEntry.argTypes()));
overloadId, guardedApply, overloadEntry.isStrict(), overloadEntry.argTypes()));
}

return new DefaultDispatcher(resolvedOverloads.buildOrThrow());
Expand All @@ -223,23 +252,6 @@ private Builder() {
}
}

/** Creates an invocation guard around the overload definition. */
private static Object guardedOp(
String functionName,
Object[] args,
ImmutableList<Class<?>> argTypes,
boolean isStrict,
CelFunctionOverload overload)
throws CelEvaluationException {
// Argument checking for DynamicDispatch is handled inside the overload's apply method itself.
if (overload instanceof DynamicDispatchOverload
|| CelFunctionOverload.canHandle(args, argTypes, isStrict)) {
return overload.apply(args);
}

throw new CelOverloadNotFoundException(functionName);
}

DefaultDispatcher(ImmutableMap<String, CelResolvedOverload> overloads) {
this.overloads = overloads;
}
Expand Down
29 changes: 29 additions & 0 deletions runtime/src/main/java/dev/cel/runtime/FunctionBindingImpl.java
Original file line number Diff line number Diff line change
Expand Up @@ -145,6 +145,35 @@ public Object apply(Object[] args) throws CelEvaluationException {
.collect(toImmutableList()));
}

@Override
public Object apply(Object arg) throws CelEvaluationException {
for (CelFunctionBinding overload : overloadBindings) {
if (CelFunctionOverload.canHandle(arg, overload.getArgTypes(), overload.isStrict())) {
return overload.getDefinition().apply(arg);
}
}
throw new CelOverloadNotFoundException(
functionName,
overloadBindings.stream()
.map(CelFunctionBinding::getOverloadId)
.collect(toImmutableList()));
}

@Override
public Object apply(Object arg1, Object arg2) throws CelEvaluationException {
for (CelFunctionBinding overload : overloadBindings) {
if (CelFunctionOverload.canHandle(
arg1, arg2, overload.getArgTypes(), overload.isStrict())) {
return overload.getDefinition().apply(arg1, arg2);
}
}
throw new CelOverloadNotFoundException(
functionName,
overloadBindings.stream()
.map(CelFunctionBinding::getOverloadId)
.collect(toImmutableList()));
}

ImmutableSet<CelFunctionBinding> getOverloadBindings() {
return overloadBindings;
}
Expand Down
16 changes: 16 additions & 0 deletions runtime/src/main/java/dev/cel/runtime/planner/BUILD.bazel
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ java_library(
":error_metadata",
":eval_and",
":eval_attribute",
":eval_binary",
":eval_conditional",
":eval_const",
":eval_create_list",
Expand Down Expand Up @@ -232,6 +233,21 @@ java_library(
],
)

java_library(
name = "eval_binary",
srcs = ["EvalBinary.java"],
deps = [
":eval_helpers",
":execution_frame",
":planned_interpretable",
"//common/values",
"//runtime:accumulated_unknowns",
"//runtime:evaluation_exception",
"//runtime:interpretable",
"//runtime:resolved_overload",
],
)

java_library(
name = "eval_var_args_call",
srcs = ["EvalVarArgsCall.java"],
Expand Down
75 changes: 75 additions & 0 deletions runtime/src/main/java/dev/cel/runtime/planner/EvalBinary.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
// Copyright 2026 Google LLC
//
// 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
//
// https://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.

package dev.cel.runtime.planner;

import static dev.cel.runtime.planner.EvalHelpers.evalNonstrictly;
import static dev.cel.runtime.planner.EvalHelpers.evalStrictly;

import dev.cel.common.values.CelValueConverter;
import dev.cel.runtime.AccumulatedUnknowns;
import dev.cel.runtime.CelEvaluationException;
import dev.cel.runtime.CelResolvedOverload;
import dev.cel.runtime.GlobalResolver;

final class EvalBinary extends PlannedInterpretable {

private final CelResolvedOverload resolvedOverload;
private final PlannedInterpretable arg1;
private final PlannedInterpretable arg2;
private final CelValueConverter celValueConverter;

@Override
public Object eval(GlobalResolver resolver, ExecutionFrame frame) throws CelEvaluationException {
Object argVal1 =
resolvedOverload.isStrict()
? evalStrictly(arg1, resolver, frame)
: evalNonstrictly(arg1, resolver, frame);
Object argVal2 =
resolvedOverload.isStrict()
? evalStrictly(arg2, resolver, frame)
: evalNonstrictly(arg2, resolver, frame);

AccumulatedUnknowns unknowns = AccumulatedUnknowns.maybeMerge(null, argVal1);
unknowns = AccumulatedUnknowns.maybeMerge(unknowns, argVal2);

if (unknowns != null) {
return unknowns;
}

return EvalHelpers.dispatch(resolvedOverload, celValueConverter, argVal1, argVal2);
}

static EvalBinary create(
long exprId,
CelResolvedOverload resolvedOverload,
PlannedInterpretable arg1,
PlannedInterpretable arg2,
CelValueConverter celValueConverter) {
return new EvalBinary(exprId, resolvedOverload, arg1, arg2, celValueConverter);
}

private EvalBinary(
long exprId,
CelResolvedOverload resolvedOverload,
PlannedInterpretable arg1,
PlannedInterpretable arg2,
CelValueConverter celValueConverter) {
super(exprId);
this.resolvedOverload = resolvedOverload;
this.arg1 = arg1;
this.arg2 = arg2;
this.celValueConverter = celValueConverter;
}
}
Loading
Loading