diff --git a/common/src/main/java/dev/cel/common/CelOverloadDecl.java b/common/src/main/java/dev/cel/common/CelOverloadDecl.java index c6a7ab1c3..f17dd87b1 100644 --- a/common/src/main/java/dev/cel/common/CelOverloadDecl.java +++ b/common/src/main/java/dev/cel/common/CelOverloadDecl.java @@ -36,6 +36,8 @@ *

An overload is declared in either a global function `Ex: f(x, ...)` or a method call style * `Ex: x.f(...)`. */ +// TODO: Port https://github.com/cel-expr/cel-go/pull/1483 to support first-class +// properties on declarations and bindings for late-bound and asynchronous functions. @AutoValue @Immutable public abstract class CelOverloadDecl { diff --git a/runtime/planner/BUILD.bazel b/runtime/planner/BUILD.bazel index 4c482337d..78d73885a 100644 --- a/runtime/planner/BUILD.bazel +++ b/runtime/planner/BUILD.bazel @@ -22,6 +22,12 @@ java_library( exports = ["//runtime/src/main/java/dev/cel/runtime/planner:planned_program"], ) +cel_android_library( + name = "planned_program_android", + visibility = ["//:internal"], + exports = ["//runtime/src/main/java/dev/cel/runtime/planner:planned_program_android"], +) + java_library( name = "async_gate", testonly = 1, @@ -29,6 +35,13 @@ java_library( exports = ["//runtime/src/main/java/dev/cel/runtime/planner:async_gate"], ) +cel_android_library( + name = "async_gate_android", + testonly = 1, + visibility = ["//:internal"], + exports = ["//runtime/src/main/java/dev/cel/runtime/planner:async_gate_android"], +) + java_library( name = "async_completion_coordinator", testonly = 1, @@ -36,8 +49,21 @@ java_library( exports = ["//runtime/src/main/java/dev/cel/runtime/planner:async_completion_coordinator"], ) +cel_android_library( + name = "async_completion_coordinator_android", + testonly = 1, + visibility = ["//:internal"], + exports = ["//runtime/src/main/java/dev/cel/runtime/planner:async_completion_coordinator_android"], +) + java_library( name = "async_call_state_tracker", visibility = ["//:internal"], exports = ["//runtime/src/main/java/dev/cel/runtime/planner:async_call_state_tracker"], ) + +cel_android_library( + name = "async_call_state_tracker_android", + visibility = ["//:internal"], + exports = ["//runtime/src/main/java/dev/cel/runtime/planner:async_call_state_tracker_android"], +) diff --git a/runtime/src/main/java/dev/cel/runtime/BUILD.bazel b/runtime/src/main/java/dev/cel/runtime/BUILD.bazel index 9518e1601..158c932c7 100644 --- a/runtime/src/main/java/dev/cel/runtime/BUILD.bazel +++ b/runtime/src/main/java/dev/cel/runtime/BUILD.bazel @@ -829,7 +829,6 @@ java_library( ":function_binding", ":function_resolver", ":partial_vars", - ":program", ":proto_message_runtime_equality", ":runtime", ":runtime_equality", @@ -855,7 +854,6 @@ java_library( "//runtime:activation", "//runtime:interpretable", "//runtime:proto_message_activation_factory", - "//runtime:resolved_overload", "//runtime/planner:planned_program", "//runtime/planner:program_planner", "//runtime/standard:type", @@ -997,6 +995,7 @@ java_library( "//common/types:type_providers", "//common/values", "//common/values:cel_value_provider", + "//runtime:async_options", "//runtime:evaluation_exception", "//runtime/planner:program_planner", "//runtime/standard:standard_function", @@ -1025,6 +1024,7 @@ cel_android_library( "//common/types:type_providers_android", "//common/values:cel_value_provider_android", "//common/values:values_android", + "//runtime:async_options_android", "//runtime:evaluation_exception", "//runtime/planner:program_planner_android", "//runtime/standard:standard_function_android", diff --git a/runtime/src/main/java/dev/cel/runtime/CelFunctionBinding.java b/runtime/src/main/java/dev/cel/runtime/CelFunctionBinding.java index 3b0084394..4597622f3 100644 --- a/runtime/src/main/java/dev/cel/runtime/CelFunctionBinding.java +++ b/runtime/src/main/java/dev/cel/runtime/CelFunctionBinding.java @@ -188,9 +188,7 @@ static ImmutableSet fromOverloads( String functionName, Collection overloadBindings) { checkArgument(!Strings.isNullOrEmpty(functionName), "Function name cannot be null or empty"); checkArgument(!overloadBindings.isEmpty(), "You must provide at least one binding."); - // TODO: Dynamic dispatch grouping does not currently support asynchronous - // function overloads. In parsed-only mode, overloaded async functions must be resolved - // at runtime via CelFunctionResolver. + // TODO: Support dynamic overload resolution for asynchronous function overloads. for (CelFunctionBinding binding : overloadBindings) { checkArgument( !(binding.getDefinition() instanceof CelAsyncFunctionOverload), diff --git a/runtime/src/main/java/dev/cel/runtime/CelRuntimeImpl.java b/runtime/src/main/java/dev/cel/runtime/CelRuntimeImpl.java index 857434ba2..c3bff8dfd 100644 --- a/runtime/src/main/java/dev/cel/runtime/CelRuntimeImpl.java +++ b/runtime/src/main/java/dev/cel/runtime/CelRuntimeImpl.java @@ -112,22 +112,7 @@ public Program createProgram(CelAbstractSyntaxTree ast) throws CelEvaluationExce return toRuntimeProgram(planner().plan(ast)); } - private static final CelFunctionResolver EMPTY_FUNCTION_RESOLVER = - new CelFunctionResolver() { - @Override - public Optional findOverloadMatchingArgs( - String functionName, Collection overloadIds, Object[] args) { - return Optional.empty(); - } - - @Override - public Optional findOverloadMatchingArgs( - String functionName, Object[] args) { - return Optional.empty(); - } - }; - - public Program toRuntimeProgram(dev.cel.runtime.Program program) { + private Program toRuntimeProgram(PlannedProgram program) { return new Program() { @Override @@ -148,11 +133,9 @@ public Object eval(Map mapValue, CelFunctionResolver lateBoundFunctio @Override public Object eval(Message message) throws CelEvaluationException { - PlannedProgram plannedProgram = (PlannedProgram) program; - return plannedProgram.evalOrThrow( - plannedProgram.interpretable(), - ProtoMessageActivationFactory.fromProto(message, plannedProgram.options()), - EMPTY_FUNCTION_RESOLVER, + return program.evalOrThrow( + ProtoMessageActivationFactory.fromProto(message, program.options()), + CelFunctionResolver.EMPTY, /* partialVars= */ null, /* listener= */ null); } @@ -190,12 +173,6 @@ public ListenableFuture evalAsync( return program.evalAsync(mapValue, lateBoundFunctionResolver); } - @Override - public ListenableFuture evalAsync(Message message) { - throw new UnsupportedOperationException( - "evalAsync is not supported by this Program implementation."); - } - @Override public ListenableFuture evalAsync(CelVariableResolver resolver) { return program.evalAsync(resolver); @@ -212,27 +189,30 @@ public ListenableFuture evalAsync(PartialVars partialVars) { return program.evalAsync(partialVars); } + @Override + public ListenableFuture evalAsync(Message message) { + throw new UnsupportedOperationException( + "evalAsync is not supported by this Program implementation."); + } + @Override public Object trace(CelEvaluationListener listener) throws CelEvaluationException { - return ((PlannedProgram) program) - .trace(GlobalResolver.EMPTY, EMPTY_FUNCTION_RESOLVER, null, listener); + return program.trace(GlobalResolver.EMPTY, CelFunctionResolver.EMPTY, null, listener); } @Override public Object trace(Map mapValue, CelEvaluationListener listener) throws CelEvaluationException { - return ((PlannedProgram) program) - .trace(Activation.copyOf(mapValue), EMPTY_FUNCTION_RESOLVER, null, listener); + return program.trace( + Activation.copyOf(mapValue), CelFunctionResolver.EMPTY, null, listener); } @Override public Object trace(Message message, CelEvaluationListener listener) throws CelEvaluationException { - PlannedProgram plannedProgram = (PlannedProgram) program; - return plannedProgram.evalOrThrow( - plannedProgram.interpretable(), - ProtoMessageActivationFactory.fromProto(message, plannedProgram.options()), - EMPTY_FUNCTION_RESOLVER, + return program.evalOrThrow( + ProtoMessageActivationFactory.fromProto(message, program.options()), + CelFunctionResolver.EMPTY, /* partialVars= */ null, listener); } @@ -240,12 +220,8 @@ public Object trace(Message message, CelEvaluationListener listener) @Override public Object trace(CelVariableResolver resolver, CelEvaluationListener listener) throws CelEvaluationException { - return ((PlannedProgram) program) - .trace( - (name) -> resolver.find(name).orElse(null), - EMPTY_FUNCTION_RESOLVER, - null, - listener); + return program.trace( + (name) -> resolver.find(name).orElse(null), CelFunctionResolver.EMPTY, null, listener); } @Override @@ -254,12 +230,8 @@ public Object trace( CelFunctionResolver lateBoundFunctionResolver, CelEvaluationListener listener) throws CelEvaluationException { - return ((PlannedProgram) program) - .trace( - (name) -> resolver.find(name).orElse(null), - lateBoundFunctionResolver, - null, - listener); + return program.trace( + (name) -> resolver.find(name).orElse(null), lateBoundFunctionResolver, null, listener); } @Override @@ -268,23 +240,22 @@ public Object trace( CelFunctionResolver lateBoundFunctionResolver, CelEvaluationListener listener) throws CelEvaluationException { - return ((PlannedProgram) program) - .trace(Activation.copyOf(mapValue), lateBoundFunctionResolver, null, listener); + return program.trace( + Activation.copyOf(mapValue), lateBoundFunctionResolver, null, listener); } @Override public Object trace(PartialVars partialVars, CelEvaluationListener listener) throws CelEvaluationException { - return ((PlannedProgram) program) - .trace( - (name) -> partialVars.resolver().find(name).orElse(null), - EMPTY_FUNCTION_RESOLVER, - partialVars, - listener); + return program.trace( + (name) -> partialVars.resolver().find(name).orElse(null), + CelFunctionResolver.EMPTY, + partialVars, + listener); } @Override - public Object advanceEvaluation(UnknownContext context) throws CelEvaluationException { + public Object advanceEvaluation(UnknownContext context) { throw new UnsupportedOperationException("Unsupported operation."); } }; @@ -347,6 +318,10 @@ public abstract Builder setAsyncEvaluationOptions( @Override public abstract CelValueProvider valueProvider(); + abstract CelAsyncEvaluationOptions asyncEvaluationOptions(); + + abstract Optional asyncExecutor(); + abstract CelStandardFunctions standardFunctions(); abstract ExtensionRegistry extensionRegistry(); @@ -604,7 +579,9 @@ public CelRuntime build() { celValueConverter, container(), options(), - lateBoundFunctionNamesBuilder().build()); + lateBoundFunctionNamesBuilder().build(), + asyncEvaluationOptions(), + asyncExecutor().orElse(null)); setPlanner(planner); setFunctionBindings(ImmutableMap.copyOf(mutableFunctionBindings)); diff --git a/runtime/src/main/java/dev/cel/runtime/LiteRuntimeImpl.java b/runtime/src/main/java/dev/cel/runtime/LiteRuntimeImpl.java index 6572621a6..875626e81 100644 --- a/runtime/src/main/java/dev/cel/runtime/LiteRuntimeImpl.java +++ b/runtime/src/main/java/dev/cel/runtime/LiteRuntimeImpl.java @@ -229,7 +229,10 @@ public CelLiteRuntime build() { celValueProvider.celValueConverter(), container, celOptions, - lateBoundFunctionNamesBuilder.build()); + lateBoundFunctionNamesBuilder.build(), + // TODO: Support async eval in lite runtime. + CelAsyncEvaluationOptions.defaultOptions(), + /* asyncExecutor= */ null); return new LiteRuntimeImpl( planner, diff --git a/runtime/src/main/java/dev/cel/runtime/planner/BUILD.bazel b/runtime/src/main/java/dev/cel/runtime/planner/BUILD.bazel index 0ab4f1beb..a8882f539 100644 --- a/runtime/src/main/java/dev/cel/runtime/planner/BUILD.bazel +++ b/runtime/src/main/java/dev/cel/runtime/planner/BUILD.bazel @@ -17,6 +17,7 @@ java_library( ":attribute", ":error_metadata", ":eval_and", + ":eval_async_call", ":eval_attribute", ":eval_binary", ":eval_block", @@ -54,10 +55,11 @@ java_library( "//common/types:type_providers", "//common/values", "//common/values:cel_value_provider", + "//runtime:async_options", "//runtime:dispatcher", "//runtime:evaluation_exception", "//runtime:evaluation_exception_builder", - "//runtime:program", + "//runtime:function_overload", "//runtime:resolved_overload", "@maven//:com_google_code_findbugs_annotations", "@maven//:com_google_errorprone_error_prone_annotations", @@ -81,6 +83,7 @@ java_library( "//common/exceptions:runtime_exception", "//common/values", "//runtime:activation", + "//runtime:async_options", "//runtime:evaluation_exception", "//runtime:evaluation_exception_builder", "//runtime:evaluation_listener", @@ -89,7 +92,6 @@ java_library( "//runtime:interpreter_util", "//runtime:partial_vars", "//runtime:program", - "//runtime:resolved_overload", "//runtime:variable_resolver", "@maven//:com_google_errorprone_error_prone_annotations", "@maven//:com_google_guava_guava", @@ -244,6 +246,19 @@ java_library( ], ) +java_library( + name = "eval_async_call", + srcs = ["EvalAsyncCall.java"], + deps = [ + ":planned_interpretable", + "//common/ast", + "//runtime:evaluation_exception", + "//runtime:interpretable", + "@maven//:com_google_errorprone_error_prone_annotations", + "@maven//:com_google_guava_guava", + ], +) + java_library( name = "activation_wrapper", srcs = ["ActivationWrapper.java"], @@ -607,6 +622,7 @@ cel_android_library( ":attribute_android", ":error_metadata_android", ":eval_and_android", + ":eval_async_call_android", ":eval_attribute_android", ":eval_binary_android", ":eval_block_android", @@ -644,11 +660,12 @@ cel_android_library( "//common/types:types_android", "//common/values:cel_value_provider_android", "//common/values:values_android", + "//runtime:async_options_android", "//runtime:dispatcher_android", "//runtime:evaluation_exception", "//runtime:evaluation_exception_builder", + "//runtime:function_overload_android", "//runtime:resolved_overload_android", - "//runtime/src/main/java/dev/cel/runtime:program_android", "@maven//:com_google_errorprone_error_prone_annotations", "@maven//:org_jspecify_jspecify", "@maven_android//:com_google_guava_guava", @@ -658,6 +675,8 @@ cel_android_library( cel_android_library( name = "planned_program_android", srcs = ["PlannedProgram.java"], + tags = [ + ], deps = [ ":error_metadata_android", ":localized_evaluation_exception_android", @@ -668,10 +687,10 @@ cel_android_library( "//common/exceptions:runtime_exception", "//common/values:values_android", "//runtime:activation_android", + "//runtime:async_options_android", "//runtime:evaluation_exception", "//runtime:evaluation_exception_builder", "//runtime:interpretable_android", - "//runtime:resolved_overload_android", "//runtime:variable_resolver", "//runtime/src/main/java/dev/cel/runtime:evaluation_listener_android", "//runtime/src/main/java/dev/cel/runtime:function_resolver_android", @@ -774,6 +793,78 @@ cel_android_library( ], ) +cel_android_library( + name = "async_gate_android", + srcs = ["AsyncGate.java"], + tags = [ + ], + deps = [ + "@maven//:com_google_code_findbugs_annotations", + "@maven//:com_google_errorprone_error_prone_annotations", + "@maven//:org_jspecify_jspecify", + "@maven_android//:com_google_guava_guava", + ], +) + +cel_android_library( + name = "async_completion_coordinator_android", + srcs = ["AsyncCompletionCoordinator.java"], + tags = [ + ], + deps = [ + ":async_gate_android", + "//runtime:async_call_android", + "//runtime:async_drain_strategy_android", + "//runtime:async_options_android", + "@maven//:com_google_code_findbugs_annotations", + "@maven//:com_google_errorprone_error_prone_annotations", + "@maven//:com_google_guava_guava", + "@maven//:org_jspecify_jspecify", + "@maven_android//:com_google_guava_guava", + ], +) + +cel_android_library( + name = "async_call_state_tracker_android", + srcs = [ + "AsyncCallRecord.java", + "AsyncCallStateTracker.java", + ], + tags = [ + ], + deps = [ + ":async_completion_coordinator_android", + ":async_gate_android", + "//common/exceptions:runtime_exception", + "//common/values:values_android", + "//runtime:async_call_android", + "//runtime:async_observer_android", + "//runtime:evaluation_exception", + "//runtime:function_overload_android", + "//runtime:interpreter_util_android", + "//runtime:runtime_equality_android", + "//runtime/src/main/java/dev/cel/runtime:accumulated_unknowns_android", + "@maven//:com_google_code_findbugs_annotations", + "@maven//:com_google_errorprone_error_prone_annotations", + "@maven//:com_google_guava_guava", + "@maven//:org_jspecify_jspecify", + "@maven_android//:com_google_guava_guava", + ], +) + +cel_android_library( + name = "eval_async_call_android", + srcs = ["EvalAsyncCall.java"], + deps = [ + ":planned_interpretable_android", + "//common/ast:ast_android", + "//runtime:evaluation_exception", + "//runtime:interpretable_android", + "@maven//:com_google_errorprone_error_prone_annotations", + "@maven_android//:com_google_guava_guava", + ], +) + cel_android_library( name = "activation_wrapper_android", srcs = ["ActivationWrapper.java"], diff --git a/runtime/src/main/java/dev/cel/runtime/planner/EvalAsyncCall.java b/runtime/src/main/java/dev/cel/runtime/planner/EvalAsyncCall.java new file mode 100644 index 000000000..3b88564aa --- /dev/null +++ b/runtime/src/main/java/dev/cel/runtime/planner/EvalAsyncCall.java @@ -0,0 +1,47 @@ +// 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 com.google.common.base.Preconditions.checkNotNull; + +import com.google.errorprone.annotations.Immutable; +import dev.cel.common.ast.CelExpr; +import dev.cel.runtime.CelEvaluationException; +import dev.cel.runtime.GlobalResolver; + +/** Evaluates an asynchronous function call within a planned program. */ +@Immutable +final class EvalAsyncCall extends PlannedInterpretable { + + private final String functionName; + + static EvalAsyncCall create(CelExpr expr, String functionName) { + return new EvalAsyncCall(expr, functionName); + } + + @Override + Object evalInternal(GlobalResolver resolver, ExecutionFrame frame) throws CelEvaluationException { + throw new CelEvaluationException( + String.format( + "Async function '%s' evaluated in synchronous mode. Asynchronous functions are only" + + " supported via evalAsync.", + functionName)); + } + + private EvalAsyncCall(CelExpr expr, String functionName) { + super(expr); + this.functionName = checkNotNull(functionName); + } +} diff --git a/runtime/src/main/java/dev/cel/runtime/planner/PlannedProgram.java b/runtime/src/main/java/dev/cel/runtime/planner/PlannedProgram.java index f7f3d7f01..2f007923e 100644 --- a/runtime/src/main/java/dev/cel/runtime/planner/PlannedProgram.java +++ b/runtime/src/main/java/dev/cel/runtime/planner/PlannedProgram.java @@ -16,23 +16,23 @@ import com.google.auto.value.AutoValue; import com.google.common.util.concurrent.ListenableFuture; +import com.google.common.util.concurrent.ListeningExecutorService; import com.google.errorprone.annotations.Immutable; import dev.cel.common.CelOptions; import dev.cel.common.annotations.Internal; import dev.cel.common.exceptions.CelRuntimeException; import dev.cel.common.values.ErrorValue; import dev.cel.runtime.Activation; +import dev.cel.runtime.CelAsyncEvaluationOptions; import dev.cel.runtime.CelEvaluationException; import dev.cel.runtime.CelEvaluationExceptionBuilder; import dev.cel.runtime.CelEvaluationListener; import dev.cel.runtime.CelFunctionResolver; -import dev.cel.runtime.CelResolvedOverload; import dev.cel.runtime.CelVariableResolver; import dev.cel.runtime.GlobalResolver; import dev.cel.runtime.InterpreterUtil; import dev.cel.runtime.PartialVars; import dev.cel.runtime.Program; -import java.util.Collection; import java.util.Map; import java.util.Optional; import org.jspecify.annotations.Nullable; @@ -47,33 +47,37 @@ @AutoValue public abstract class PlannedProgram implements Program { - private static final CelFunctionResolver EMPTY_FUNCTION_RESOLVER = - new CelFunctionResolver() { - @Override - public Optional findOverloadMatchingArgs( - String functionName, Collection overloadIds, Object[] args) { - return Optional.empty(); - } - - @Override - public Optional findOverloadMatchingArgs( - String functionName, Object[] args) { - return Optional.empty(); - } - }; - - public abstract PlannedInterpretable interpretable(); + abstract PlannedInterpretable interpretable(); abstract ErrorMetadata metadata(); public abstract CelOptions options(); + // CelAsyncEvaluationOptions is an immutable value object. + @SuppressWarnings("Immutable") + @AutoValue.CopyAnnotations + abstract CelAsyncEvaluationOptions asyncOptions(); + + // The executor service is an externally managed, thread-safe asynchronous execution pool. + @SuppressWarnings("Immutable") + @AutoValue.CopyAnnotations + abstract Optional asyncExecutor(); + + static PlannedProgram create( + PlannedInterpretable interpretable, + ErrorMetadata metadata, + CelOptions options, + CelAsyncEvaluationOptions asyncOptions, + @Nullable ListeningExecutorService asyncExecutor) { + return new AutoValue_PlannedProgram( + interpretable, metadata, options, asyncOptions, Optional.ofNullable(asyncExecutor)); + } + @Override public Object eval() throws CelEvaluationException { return evalOrThrow( - interpretable(), GlobalResolver.EMPTY, - EMPTY_FUNCTION_RESOLVER, + CelFunctionResolver.EMPTY, /* partialVars= */ null, /* listener= */ null); } @@ -81,9 +85,8 @@ public Object eval() throws CelEvaluationException { @Override public Object eval(Map mapValue) throws CelEvaluationException { return evalOrThrow( - interpretable(), Activation.copyOf(mapValue), - EMPTY_FUNCTION_RESOLVER, + CelFunctionResolver.EMPTY, /* partialVars= */ null, /* listener= */ null); } @@ -92,7 +95,6 @@ public Object eval(Map mapValue) throws CelEvaluationException { public Object eval(Map mapValue, CelFunctionResolver lateBoundFunctionResolver) throws CelEvaluationException { return evalOrThrow( - interpretable(), Activation.copyOf(mapValue), lateBoundFunctionResolver, /* partialVars= */ null, @@ -102,9 +104,8 @@ public Object eval(Map mapValue, CelFunctionResolver lateBoundFunctio @Override public Object eval(CelVariableResolver resolver) throws CelEvaluationException { return evalOrThrow( - interpretable(), (name) -> resolver.find(name).orElse(null), - EMPTY_FUNCTION_RESOLVER, + CelFunctionResolver.EMPTY, /* partialVars= */ null, /* listener= */ null); } @@ -113,7 +114,6 @@ public Object eval(CelVariableResolver resolver) throws CelEvaluationException { public Object eval(CelVariableResolver resolver, CelFunctionResolver lateBoundFunctionResolver) throws CelEvaluationException { return evalOrThrow( - interpretable(), (name) -> resolver.find(name).orElse(null), lateBoundFunctionResolver, /* partialVars= */ null, @@ -123,9 +123,8 @@ public Object eval(CelVariableResolver resolver, CelFunctionResolver lateBoundFu @Override public Object eval(PartialVars partialVars) throws CelEvaluationException { return evalOrThrow( - interpretable(), (name) -> partialVars.resolver().find(name).orElse(null), - EMPTY_FUNCTION_RESOLVER, + CelFunctionResolver.EMPTY, partialVars, /* listener= */ null); } @@ -163,7 +162,6 @@ public ListenableFuture evalAsync(PartialVars partialVars) { } public Object evalOrThrow( - PlannedInterpretable interpretable, GlobalResolver resolver, CelFunctionResolver functionResolver, @Nullable PartialVars partialVars, @@ -172,7 +170,7 @@ public Object evalOrThrow( try { ExecutionFrame frame = ExecutionFrame.create(functionResolver, options(), partialVars, listener); - Object evalResult = interpretable.eval(resolver, frame); + Object evalResult = interpretable().eval(resolver, frame); if (evalResult instanceof ErrorValue) { ErrorValue errorValue = (ErrorValue) evalResult; throw newCelEvaluationException(errorValue.exprId(), errorValue.value()); @@ -180,20 +178,23 @@ public Object evalOrThrow( return InterpreterUtil.maybeAdaptToCelUnknownSet(evalResult); } catch (RuntimeException e) { - throw newCelEvaluationException(interpretable.expr().id(), e); + throw newCelEvaluationException(interpretable().expr().id(), e); } } public Object trace( GlobalResolver resolver, CelFunctionResolver functionResolver, - PartialVars partialVars, - CelEvaluationListener listener) + @Nullable PartialVars partialVars, + @Nullable CelEvaluationListener listener) throws CelEvaluationException { - return evalOrThrow(interpretable(), resolver, functionResolver, partialVars, listener); + return evalOrThrow(resolver, functionResolver, partialVars, listener); } - private CelEvaluationException newCelEvaluationException(long exprId, Exception e) { + private CelEvaluationException newCelEvaluationException(long exprId, Throwable e) { + if (e instanceof CelEvaluationException) { + return (CelEvaluationException) e; + } CelEvaluationExceptionBuilder builder; if (e instanceof LocalizedEvaluationException) { // Use the localized expr ID (most specific error location) @@ -201,8 +202,7 @@ private CelEvaluationException newCelEvaluationException(long exprId, Exception exprId = localized.exprId(); Throwable cause = localized.getCause(); if (cause instanceof CelRuntimeException) { - builder = - CelEvaluationExceptionBuilder.newBuilder((CelRuntimeException) localized.getCause()); + builder = CelEvaluationExceptionBuilder.newBuilder((CelRuntimeException) cause); } else { builder = CelEvaluationExceptionBuilder.newBuilder(cause.getMessage()).setCause(cause); } @@ -220,8 +220,5 @@ private CelEvaluationException newCelEvaluationException(long exprId, Exception return builder.setMetadata(metadata(), exprId).build(); } - static Program create( - PlannedInterpretable interpretable, ErrorMetadata metadata, CelOptions options) { - return new AutoValue_PlannedProgram(interpretable, metadata, options); - } + PlannedProgram() {} } diff --git a/runtime/src/main/java/dev/cel/runtime/planner/ProgramPlanner.java b/runtime/src/main/java/dev/cel/runtime/planner/ProgramPlanner.java index 23a6e5dec..47b7cf552 100644 --- a/runtime/src/main/java/dev/cel/runtime/planner/ProgramPlanner.java +++ b/runtime/src/main/java/dev/cel/runtime/planner/ProgramPlanner.java @@ -21,6 +21,7 @@ import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableMap; import com.google.common.collect.ImmutableSet; +import com.google.common.util.concurrent.ListeningExecutorService; import com.google.errorprone.annotations.CheckReturnValue; import com.google.errorprone.annotations.Immutable; import dev.cel.common.CelAbstractSyntaxTree; @@ -47,11 +48,13 @@ import dev.cel.common.types.TypeType; import dev.cel.common.values.CelValueConverter; import dev.cel.common.values.CelValueProvider; +import dev.cel.runtime.CelAsyncEvaluationOptions; +import dev.cel.runtime.CelAsyncFunctionOverload; import dev.cel.runtime.CelEvaluationException; import dev.cel.runtime.CelEvaluationExceptionBuilder; import dev.cel.runtime.CelResolvedOverload; import dev.cel.runtime.DefaultDispatcher; -import dev.cel.runtime.Program; +import java.util.Arrays; import java.util.HashMap; import java.util.NoSuchElementException; import java.util.Optional; @@ -73,11 +76,19 @@ public final class ProgramPlanner { private final CelValueConverter celValueConverter; private final ImmutableSet lateBoundFunctionNames; + // CelAsyncEvaluationOptions is an immutable value object. + @SuppressWarnings("Immutable") + private final CelAsyncEvaluationOptions asyncOptions; + + // The executor service is an externally managed, thread-safe asynchronous execution pool. + @SuppressWarnings("Immutable") + private final @Nullable ListeningExecutorService asyncExecutor; + /** - * Plans a {@link Program} from the provided parsed-only or type-checked {@link + * Plans a {@link PlannedProgram} from the provided parsed-only or type-checked {@link * CelAbstractSyntaxTree}. */ - public Program plan(CelAbstractSyntaxTree ast) throws CelEvaluationException { + public PlannedProgram plan(CelAbstractSyntaxTree ast) throws CelEvaluationException { PlannedInterpretable plannedInterpretable; ErrorMetadata errorMetadata = ErrorMetadata.create(ast.getSource().getPositionsMap(), ast.getSource().getDescription()); @@ -94,7 +105,8 @@ public Program plan(CelAbstractSyntaxTree ast) throws CelEvaluationException { .build(); } - return PlannedProgram.create(plannedInterpretable, errorMetadata, options); + return PlannedProgram.create( + plannedInterpretable, errorMetadata, options, asyncOptions, asyncExecutor); } private PlannedInterpretable plan(CelExpr celExpr, PlannerContext ctx) { @@ -117,9 +129,8 @@ private PlannedInterpretable plan(CelExpr celExpr, PlannerContext ctx) { return planComprehension(celExpr, ctx); case NOT_SET: throw new UnsupportedOperationException("Unsupported kind: " + celExpr.getKind()); - default: - throw new UnsupportedOperationException("Unexpected kind: " + celExpr.getKind()); } + throw new UnsupportedOperationException("Unexpected kind: " + celExpr.getKind()); } private PlannedInterpretable planSelect(CelExpr celExpr, PlannerContext ctx) { @@ -295,6 +306,8 @@ private PlannedInterpretable planCall(CelExpr expr, PlannerContext ctx) { } if (resolvedOverload == null) { + // TODO: Port https://github.com/cel-expr/cel-go/pull/1483 to support first-class + // properties on declarations and bindings for late-bound and asynchronous functions. boolean isLateBound = lateBoundFunctionNames.contains(functionName); // For type-checked ASTs, functions that are not explicitly registered as late-bound // must be resolved at plan time. @@ -320,6 +333,10 @@ private PlannedInterpretable planCall(CelExpr expr, PlannerContext ctx) { expr, functionName, overloadIds, evaluatedArgs, celValueConverter); } + if (resolvedOverload.getDefinition() instanceof CelAsyncFunctionOverload) { + return EvalAsyncCall.create(expr, functionName); + } + switch (argCount) { case 0: return EvalZeroArity.create(expr, functionName, resolvedOverload, celValueConverter); @@ -353,9 +370,7 @@ private PlannedInterpretable planBlock(CelBlock celBlock, PlannerContext ctx) { ImmutableList indices = celBlock.indices(); PlannedInterpretable[] slotExprs = new PlannedInterpretable[indices.size()]; - for (int i = 0; i < slotExprs.length; i++) { - slotExprs[i] = plan(indices.get(i), ctx); - } + Arrays.setAll(slotExprs, i -> plan(indices.get(i), ctx)); PlannedInterpretable resultExpr = plan(celBlock.result(), ctx); return EvalBlock.create(celBlock.expr(), slotExprs, resultExpr); } @@ -695,15 +710,18 @@ private boolean isLocalVar(String name) { return localVars.containsKey(name); } - private PlannerContext(CelAbstractSyntaxTree ast) { - this.ast = checkNotNull(ast); - } - static PlannerContext create(CelAbstractSyntaxTree ast) { return new PlannerContext(ast); } + + private PlannerContext(CelAbstractSyntaxTree ast) { + this.ast = checkNotNull(ast); + } } + // Internal API: ProgramPlanner is marked @Internal for the CEL runtime engine and requires all + // engine dependencies for planning. + @SuppressWarnings("TooManyParameters") public static ProgramPlanner newPlanner( CelTypeProvider typeProvider, CelValueProvider valueProvider, @@ -711,7 +729,9 @@ public static ProgramPlanner newPlanner( CelValueConverter celValueConverter, CelContainer container, CelOptions options, - ImmutableSet lateBoundFunctionNames) { + ImmutableSet lateBoundFunctionNames, + CelAsyncEvaluationOptions asyncOptions, + @Nullable ListeningExecutorService asyncExecutor) { return new ProgramPlanner( typeProvider, valueProvider, @@ -719,7 +739,9 @@ public static ProgramPlanner newPlanner( celValueConverter, container, options, - lateBoundFunctionNames); + lateBoundFunctionNames, + asyncOptions, + asyncExecutor); } private ProgramPlanner( @@ -729,7 +751,9 @@ private ProgramPlanner( CelValueConverter celValueConverter, CelContainer container, CelOptions options, - ImmutableSet lateBoundFunctionNames) { + ImmutableSet lateBoundFunctionNames, + CelAsyncEvaluationOptions asyncOptions, + @Nullable ListeningExecutorService asyncExecutor) { this.typeProvider = typeProvider; this.valueProvider = valueProvider; this.dispatcher = dispatcher; @@ -737,6 +761,8 @@ private ProgramPlanner( this.container = container; this.options = options; this.lateBoundFunctionNames = lateBoundFunctionNames; + this.asyncOptions = checkNotNull(asyncOptions); + this.asyncExecutor = asyncExecutor; this.attributeFactory = AttributeFactory.newAttributeFactory(container, typeProvider, celValueConverter); } diff --git a/runtime/src/test/java/dev/cel/runtime/planner/BUILD.bazel b/runtime/src/test/java/dev/cel/runtime/planner/BUILD.bazel index 02ca90662..53240ff87 100644 --- a/runtime/src/test/java/dev/cel/runtime/planner/BUILD.bazel +++ b/runtime/src/test/java/dev/cel/runtime/planner/BUILD.bazel @@ -55,6 +55,7 @@ java_library( "//runtime/planner:async_call_state_tracker", "//runtime/planner:async_completion_coordinator", "//runtime/planner:async_gate", + "//runtime/planner:planned_program", "//runtime/planner:program_planner", "//runtime/standard:type", "@cel_spec//proto/cel/expr/conformance/proto3:test_all_types_java_proto", diff --git a/runtime/src/test/java/dev/cel/runtime/planner/ProgramPlannerTest.java b/runtime/src/test/java/dev/cel/runtime/planner/ProgramPlannerTest.java index a3b1e3596..ebf8e1cdb 100644 --- a/runtime/src/test/java/dev/cel/runtime/planner/ProgramPlannerTest.java +++ b/runtime/src/test/java/dev/cel/runtime/planner/ProgramPlannerTest.java @@ -14,7 +14,9 @@ package dev.cel.runtime.planner; +import static com.google.common.base.Preconditions.checkArgument; import static com.google.common.truth.Truth.assertThat; +import static com.google.common.util.concurrent.MoreExecutors.newDirectExecutorService; import static dev.cel.common.CelFunctionDecl.newFunctionDeclaration; import static dev.cel.common.CelOverloadDecl.newGlobalOverload; import static dev.cel.common.CelOverloadDecl.newMemberOverload; @@ -26,6 +28,8 @@ import com.google.common.collect.ImmutableMap; import com.google.common.collect.ImmutableSet; import com.google.common.primitives.UnsignedLong; +import com.google.common.util.concurrent.Futures; +import com.google.common.util.concurrent.ListeningExecutorService; import com.google.testing.junit.testparameterinjector.TestParameter; import com.google.testing.junit.testparameterinjector.TestParameterInjector; import com.google.testing.junit.testparameterinjector.TestParameters; @@ -66,6 +70,7 @@ import dev.cel.expr.conformance.proto3.TestAllTypes.NestedMessage; import dev.cel.extensions.CelExtensions; import dev.cel.parser.CelStandardMacro; +import dev.cel.runtime.CelAsyncEvaluationOptions; import dev.cel.runtime.CelAttribute; import dev.cel.runtime.CelAttributePattern; import dev.cel.runtime.CelEvaluationException; @@ -92,7 +97,9 @@ public final class ProgramPlannerTest { private static final CelTypeProvider TYPE_PROVIDER = new CombinedCelTypeProvider( DefaultTypeProvider.getInstance(), - new ProtoMessageTypeProvider(ImmutableSet.of(TestAllTypes.getDescriptor()))); + ProtoMessageTypeProvider.newBuilder() + .addDescriptors(ImmutableSet.of(TestAllTypes.getDescriptor())) + .build()); private static final RuntimeEquality RUNTIME_EQUALITY = RuntimeEquality.create(RuntimeHelpers.create(), CEL_OPTIONS); private static final CelDescriptorPool DESCRIPTOR_POOL = @@ -119,7 +126,9 @@ public final class ProgramPlannerTest { CEL_VALUE_CONVERTER, CEL_CONTAINER, CEL_OPTIONS, - ImmutableSet.of("late_bound_func")); + ImmutableSet.of("late_bound_func"), + CelAsyncEvaluationOptions.defaultOptions(), + /* asyncExecutor= */ null); private static final CelCompiler CEL_COMPILER = CelCompilerFactory.standardCelCompilerBuilder() @@ -255,9 +264,7 @@ private static DefaultDispatcher newDispatcher() { private static void addBindingsToDispatcher( DefaultDispatcher.Builder builder, ImmutableCollection overloadBindings) { - if (overloadBindings.isEmpty()) { - throw new IllegalArgumentException("Invalid bindings"); - } + checkArgument(!overloadBindings.isEmpty(), "Invalid bindings"); overloadBindings.forEach( overload -> @@ -320,7 +327,9 @@ public void plan_ident_enumContainer() throws Exception { CEL_VALUE_CONVERTER, container, CEL_OPTIONS, - ImmutableSet.of()); + ImmutableSet.of(), + CelAsyncEvaluationOptions.defaultOptions(), + /* asyncExecutor= */ null); Program program = planner.plan(ast); @@ -519,7 +528,7 @@ public void plan_call_throws() throws Exception { .hasMessageThat() .contains("evaluation error at :5: Function 'error' failed with arg(s) ''"); assertThat(e).hasCauseThat().isInstanceOf(IllegalArgumentException.class); - assertThat(e.getCause()).hasMessageThat().contains("Intentional error"); + assertThat(e).hasCauseThat().hasMessageThat().contains("Intentional error"); } @Test @@ -1023,7 +1032,9 @@ public void plan_comprehension_iterationLimit_throws(String expression) throws E CEL_VALUE_CONVERTER, CEL_CONTAINER, options, - ImmutableSet.of()); + ImmutableSet.of(), + CelAsyncEvaluationOptions.defaultOptions(), + /* asyncExecutor= */ null); CelAbstractSyntaxTree ast = compile(expression); Program program = planner.plan(ast); @@ -1044,7 +1055,9 @@ public void plan_comprehension_iterationLimit_success() throws Exception { CEL_VALUE_CONVERTER, CEL_CONTAINER, options, - /* lateBoundFunctionNames= */ ImmutableSet.of()); + /* lateBoundFunctionNames= */ ImmutableSet.of(), + CelAsyncEvaluationOptions.defaultOptions(), + /* asyncExecutor= */ null); CelAbstractSyntaxTree ast = compile("[1, 2, 3].map(x, [1, 2].map(y, x + y))"); Program program = planner.plan(ast); @@ -1202,6 +1215,90 @@ public void plan_foldMap_withUnknownLoopCondition_earlyReturn() throws Exception CelUnknownSet.create(ImmutableSet.of(CelAttribute.create("unk")), ImmutableSet.of(7L))); } + @Test + public void newPlanner_withAsyncOptionsAndExecutor_plansSuccessfully() throws Exception { + ListeningExecutorService executor = newDirectExecutorService(); + try { + CelAsyncEvaluationOptions asyncOptions = + CelAsyncEvaluationOptions.builder().setMaxIterations(5).build(); + ProgramPlanner planner = + ProgramPlanner.newPlanner( + TYPE_PROVIDER, + VALUE_PROVIDER, + newDispatcher(), + CEL_VALUE_CONVERTER, + CEL_CONTAINER, + CEL_OPTIONS, + ImmutableSet.of(), + asyncOptions, + executor); + CelAbstractSyntaxTree ast = compile("1 + 2"); + + PlannedProgram program = planner.plan(ast); + + assertThat(program.eval()).isEqualTo(3L); + assertThat(program.asyncOptions()).isEqualTo(asyncOptions); + assertThat(program.asyncExecutor()).hasValue(executor); + } finally { + executor.shutdownNow(); + } + } + + @Test + public void newPlanner_nullAsyncOptions_throwsNullPointerException() { + DefaultDispatcher dispatcher = newDispatcher(); + + assertThrows( + NullPointerException.class, + () -> + ProgramPlanner.newPlanner( + TYPE_PROVIDER, + VALUE_PROVIDER, + dispatcher, + CEL_VALUE_CONVERTER, + CEL_CONTAINER, + CEL_OPTIONS, + ImmutableSet.of(), + /* asyncOptions= */ null, + /* asyncExecutor= */ null)); + } + + @Test + public void plan_asyncFunction_evalSynchronously_throwsCelEvaluationException() throws Exception { + CelCompiler compiler = + CelCompilerFactory.standardCelCompilerBuilder() + .addFunctionDeclarations( + newFunctionDeclaration( + "asyncSquare", + newGlobalOverload("asyncSquare_int", SimpleType.INT, SimpleType.INT))) + .build(); + CelAbstractSyntaxTree ast = compiler.compile("asyncSquare(5)").getAst(); + DefaultDispatcher.Builder dispatcher = DefaultDispatcher.newBuilder(); + addBindingsToDispatcher( + dispatcher, + ImmutableList.of( + CelFunctionBinding.fromAsync( + "asyncSquare_int", Long.class, (Long arg) -> Futures.immediateFuture(arg * arg)))); + ProgramPlanner planner = + ProgramPlanner.newPlanner( + TYPE_PROVIDER, + VALUE_PROVIDER, + dispatcher.build(), + CEL_VALUE_CONVERTER, + CEL_CONTAINER, + CEL_OPTIONS, + ImmutableSet.of(), + CelAsyncEvaluationOptions.defaultOptions(), + /* asyncExecutor= */ null); + Program program = planner.plan(ast); + + CelEvaluationException e = assertThrows(CelEvaluationException.class, program::eval); + + assertThat(e) + .hasMessageThat() + .contains("Async function 'asyncSquare' evaluated in synchronous mode."); + } + @Test public void plan_binaryFunction_withUnknownArg() throws Exception { CelCompiler compiler = @@ -1264,7 +1361,9 @@ public void localShadowIdentifier_inSelect() throws Exception { CEL_VALUE_CONVERTER, CelContainer.ofName("cel.example"), CEL_OPTIONS, - /* lateBoundFunctionNames= */ ImmutableSet.of()); + /* lateBoundFunctionNames= */ ImmutableSet.of(), + CelAsyncEvaluationOptions.defaultOptions(), + /* asyncExecutor= */ null); CelAbstractSyntaxTree ast = compile(celCompiler, "[{'z': 0}].exists(y, y.z == 0)"); Program program = planner.plan(ast); @@ -1289,7 +1388,9 @@ public void localShadowIdentifier_inSelect_globalDisambiguation() throws Excepti CEL_VALUE_CONVERTER, CelContainer.ofName("y"), CEL_OPTIONS, - /* lateBoundFunctionNames= */ ImmutableSet.of()); + /* lateBoundFunctionNames= */ ImmutableSet.of(), + CelAsyncEvaluationOptions.defaultOptions(), + /* asyncExecutor= */ null); CelAbstractSyntaxTree ast = compile(celCompiler, "[{'z': 0}].exists(y, y.z == 0 && .y.z == 1)"); Program program = planner.plan(ast); @@ -1313,7 +1414,9 @@ public void localShadowIdentifier_withGlobalDisambiguation() throws Exception { CEL_VALUE_CONVERTER, CelContainer.newBuilder().build(), CEL_OPTIONS, - /* lateBoundFunctionNames= */ ImmutableSet.of()); + /* lateBoundFunctionNames= */ ImmutableSet.of(), + CelAsyncEvaluationOptions.defaultOptions(), + /* asyncExecutor= */ null); CelAbstractSyntaxTree ast = compile(celCompiler, "[0].exists(x, x == 0 && .x == 1)"); Program program = planner.plan(ast); @@ -1337,7 +1440,9 @@ public void localDoubleShadowIdentifier_withGlobalDisambiguation() throws Except CEL_VALUE_CONVERTER, CelContainer.newBuilder().build(), CEL_OPTIONS, - /* lateBoundFunctionNames= */ ImmutableSet.of()); + /* lateBoundFunctionNames= */ ImmutableSet.of(), + CelAsyncEvaluationOptions.defaultOptions(), + /* asyncExecutor= */ null); CelAbstractSyntaxTree ast = compile(celCompiler, "[0].exists(x, [x+1].exists(x, x == .x))"); Program program = planner.plan(ast); @@ -1377,7 +1482,9 @@ public void plan_customFunctionReturningUnknown_fieldSelection() throws Exceptio CEL_VALUE_CONVERTER, CEL_CONTAINER, CEL_OPTIONS, - ImmutableSet.of()); + ImmutableSet.of(), + CelAsyncEvaluationOptions.defaultOptions(), + /* asyncExecutor= */ null); Program program = planner.plan(ast); @@ -1417,7 +1524,9 @@ public void plan_customFunctionReturningUnknown_binaryOperation() throws Excepti CEL_VALUE_CONVERTER, CEL_CONTAINER, CEL_OPTIONS, - ImmutableSet.of()); + ImmutableSet.of(), + CelAsyncEvaluationOptions.defaultOptions(), + /* asyncExecutor= */ null); Program program = planner.plan(ast); @@ -1450,7 +1559,9 @@ public void plan_variableAsCelUnknownSet_propagatesUnknown() throws Exception { CEL_VALUE_CONVERTER, CEL_CONTAINER, CEL_OPTIONS, - ImmutableSet.of()); + ImmutableSet.of(), + CelAsyncEvaluationOptions.defaultOptions(), + /* asyncExecutor= */ null); ImmutableMap vars = ImmutableMap.of(