From 4cf0ad71bd6cddebf6fadf502293909c764346c1 Mon Sep 17 00:00:00 2001 From: Sokwhan Huh Date: Mon, 2 Oct 2023 14:22:00 -0700 Subject: [PATCH] Add evaluation listener PiperOrigin-RevId: 570177910 --- .../dev/cel/common/CelDescriptorUtil.java | 2 - runtime/BUILD.bazel | 5 + .../src/main/java/dev/cel/runtime/BUILD.bazel | 18 +- .../cel/runtime/CelEvaluationListener.java | 40 +++ .../main/java/dev/cel/runtime/CelRuntime.java | 52 +++- .../dev/cel/runtime/CelRuntimeLegacyImpl.java | 12 - .../dev/cel/runtime/DefaultInterpreter.java | 54 ++-- .../java/dev/cel/runtime/Interpretable.java | 2 + .../runtime/UnknownTrackingInterpretable.java | 3 +- .../src/test/java/dev/cel/runtime/BUILD.bazel | 8 +- .../java/dev/cel/runtime/CelRuntimeTest.java | 233 ++++++++++++++++-- 11 files changed, 372 insertions(+), 57 deletions(-) create mode 100644 runtime/src/main/java/dev/cel/runtime/CelEvaluationListener.java diff --git a/common/src/main/java/dev/cel/common/CelDescriptorUtil.java b/common/src/main/java/dev/cel/common/CelDescriptorUtil.java index 65fcc937c..28aff8291 100644 --- a/common/src/main/java/dev/cel/common/CelDescriptorUtil.java +++ b/common/src/main/java/dev/cel/common/CelDescriptorUtil.java @@ -14,7 +14,6 @@ package dev.cel.common; -import com.google.common.annotations.VisibleForTesting; import com.google.common.collect.ImmutableMap; import com.google.common.collect.ImmutableSet; import com.google.protobuf.DescriptorProtos.FileDescriptorSet; @@ -63,7 +62,6 @@ public static ImmutableSet getFileDescriptorsForDescriptors( *

Warning: This will produce unique FileDescriptor instances. Use with care especially in * hermetic environments. */ - @VisibleForTesting public static ImmutableSet getFileDescriptorsFromFileDescriptorSet( FileDescriptorSet fileDescriptorSet) { return FileDescriptorSetConverter.convert(fileDescriptorSet); diff --git a/runtime/BUILD.bazel b/runtime/BUILD.bazel index d1dd206b5..d9f3020e1 100644 --- a/runtime/BUILD.bazel +++ b/runtime/BUILD.bazel @@ -40,3 +40,8 @@ java_library( name = "interpreter_util", exports = ["//runtime/src/main/java/dev/cel/runtime:interpreter_util"], ) + +java_library( + name = "evaluation_listener", + exports = ["//runtime/src/main/java/dev/cel/runtime:evaluation_listener"], +) diff --git a/runtime/src/main/java/dev/cel/runtime/BUILD.bazel b/runtime/src/main/java/dev/cel/runtime/BUILD.bazel index 9d652dd5a..1c89d0996 100644 --- a/runtime/src/main/java/dev/cel/runtime/BUILD.bazel +++ b/runtime/src/main/java/dev/cel/runtime/BUILD.bazel @@ -73,6 +73,7 @@ java_library( exports = [":base"], deps = [ ":base", + ":evaluation_listener", ":runtime_helper", ":unknown_attributes", "//:auto_value", @@ -85,7 +86,6 @@ java_library( "//common/ast", "//common/internal:dynamic_proto", "//common/types:type_providers", - "//runtime:unknown_attributes", "@cel_spec//proto/cel/expr:expr_java_proto", "@maven//:com_google_code_findbugs_annotations", "@maven//:com_google_errorprone_error_prone_annotations", @@ -142,6 +142,7 @@ java_library( ], deps = [ ":base", + ":evaluation_listener", ":interpreter", ":unknown_attributes", "//:auto_value", @@ -150,7 +151,6 @@ java_library( "//common:options", "//common/annotations", "//common/internal:dynamic_proto", - "//runtime:unknown_attributes", "@maven//:com_google_code_findbugs_annotations", "@maven//:com_google_errorprone_error_prone_annotations", "@maven//:com_google_guava_guava", @@ -178,12 +178,12 @@ java_library( tags = [ ], deps = [ + ":unknown_attributes", "//common", "//common:compiler_common", "//parser", "//parser:operator", "//parser:parser_builder", - "//runtime:unknown_attributes", "@cel_spec//proto/cel/expr:expr_java_proto", "@maven//:com_google_guava_guava", ], @@ -216,3 +216,15 @@ java_library( "@maven//:org_jspecify_jspecify", ], ) + +java_library( + name = "evaluation_listener", + srcs = ["CelEvaluationListener.java"], + tags = [ + ], + deps = [ + "//common/ast", + "@maven//:com_google_code_findbugs_annotations", + "@maven//:com_google_errorprone_error_prone_annotations", + ], +) diff --git a/runtime/src/main/java/dev/cel/runtime/CelEvaluationListener.java b/runtime/src/main/java/dev/cel/runtime/CelEvaluationListener.java new file mode 100644 index 000000000..6c00390cc --- /dev/null +++ b/runtime/src/main/java/dev/cel/runtime/CelEvaluationListener.java @@ -0,0 +1,40 @@ +// Copyright 2023 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; + +import javax.annotation.concurrent.ThreadSafe; +import dev.cel.common.ast.CelExpr; + +/** + * Functional interface for a callback method invoked by the runtime. Implementations must ensure + * that its instances are unconditionally thread-safe. + */ +@FunctionalInterface +@ThreadSafe +public interface CelEvaluationListener { + + /** + * Callback method invoked by the CEL runtime as evaluation progresses through the AST. + * + * @param expr CelExpr that was evaluated to produce the evaluated result. + * @param evaluatedResult Evaluated result. + */ + void callback(CelExpr expr, Object evaluatedResult); + + /** Construct a listener that does nothing. */ + static CelEvaluationListener noOpListener() { + return (arg1, arg2) -> {}; + } +} diff --git a/runtime/src/main/java/dev/cel/runtime/CelRuntime.java b/runtime/src/main/java/dev/cel/runtime/CelRuntime.java index 1d764a52d..e6e7167d1 100644 --- a/runtime/src/main/java/dev/cel/runtime/CelRuntime.java +++ b/runtime/src/main/java/dev/cel/runtime/CelRuntime.java @@ -67,6 +67,41 @@ public Object eval(CelVariableResolver resolver) throws CelEvaluationException { return evalInternal((name) -> resolver.find(name).orElse(null)); } + /** + * Trace evaluates a compiled program without any variables and invokes the listener as + * evaluation progresses through the AST. + */ + public Object trace(CelEvaluationListener listener) throws CelEvaluationException { + return evalInternal(Activation.EMPTY, listener); + } + + /** + * Trace evaluates a compiled program using a {@code mapValue} as the source of input variables. + * The listener is invoked as evaluation progresses through the AST. + */ + public Object trace(Map mapValue, CelEvaluationListener listener) + throws CelEvaluationException { + return evalInternal(Activation.copyOf(mapValue), listener); + } + + /** + * Trace evaluates a compiled program using {@code message} fields as the source of input + * variables. The listener is invoked as evaluation progresses through the AST. + */ + public Object trace(Message message, CelEvaluationListener listener) + throws CelEvaluationException { + return evalInternal(Activation.fromProto(message, getOptions()), listener); + } + + /** + * Trace evaluates a compiled program using a custom variable {@code resolver}. The listener is + * invoked as evaluation progresses through the AST. + */ + public Object trace(CelVariableResolver resolver, CelEvaluationListener listener) + throws CelEvaluationException { + return evalInternal((name) -> resolver.find(name).orElse(null), listener); + } + /** * Advance evaluation based on the current unknown context. * @@ -77,18 +112,24 @@ public Object eval(CelVariableResolver resolver) throws CelEvaluationException { * UnknownTracking} is disabled, this is equivalent to eval. */ public Object advanceEvaluation(UnknownContext context) throws CelEvaluationException { - return evalInternal(context); + return evalInternal(context, CelEvaluationListener.noOpListener()); } private Object evalInternal(GlobalResolver resolver) throws CelEvaluationException { - return evalInternal(UnknownContext.create(resolver)); + return evalInternal(resolver, CelEvaluationListener.noOpListener()); + } + + private Object evalInternal(GlobalResolver resolver, CelEvaluationListener listener) + throws CelEvaluationException { + return evalInternal(UnknownContext.create(resolver), listener); } /** * Evaluate an expr node with an UnknownContext (an activation annotated with which attributes * are unknown). */ - private Object evalInternal(UnknownContext context) throws CelEvaluationException { + private Object evalInternal(UnknownContext context, CelEvaluationListener listener) + throws CelEvaluationException { try { Interpretable impl = getInterpretable(); if (getOptions().enableUnknownTracking()) { @@ -102,9 +143,10 @@ private Object evalInternal(UnknownContext context) throws CelEvaluationExceptio RuntimeUnknownResolver.builder() .setResolver(context.variableResolver()) .setAttributeResolver(context.createAttributeResolver()) - .build()); + .build(), + listener); } else { - return impl.eval(context.variableResolver()); + return impl.eval(context.variableResolver(), listener); } } catch (InterpreterException e) { throw unwrapOrCreateEvaluationException(e); diff --git a/runtime/src/main/java/dev/cel/runtime/CelRuntimeLegacyImpl.java b/runtime/src/main/java/dev/cel/runtime/CelRuntimeLegacyImpl.java index 318a400bd..4c052a03b 100644 --- a/runtime/src/main/java/dev/cel/runtime/CelRuntimeLegacyImpl.java +++ b/runtime/src/main/java/dev/cel/runtime/CelRuntimeLegacyImpl.java @@ -75,7 +75,6 @@ public static final class Builder implements CelRuntimeBuilder { private boolean standardEnvironmentEnabled; private Function customTypeFactory; - /** {@inheritDoc} */ @Override @CanIgnoreReturnValue public Builder setOptions(CelOptions options) { @@ -83,14 +82,12 @@ public Builder setOptions(CelOptions options) { return this; } - /** {@inheritDoc} */ @Override @CanIgnoreReturnValue public Builder addFunctionBindings(CelFunctionBinding... bindings) { return addFunctionBindings(Arrays.asList(bindings)); } - /** {@inheritDoc} */ @Override @CanIgnoreReturnValue public Builder addFunctionBindings(Iterable bindings) { @@ -98,28 +95,24 @@ public Builder addFunctionBindings(Iterable bindings) { return this; } - /** {@inheritDoc} */ @Override @CanIgnoreReturnValue public Builder addMessageTypes(Descriptor... descriptors) { return addMessageTypes(Arrays.asList(descriptors)); } - /** {@inheritDoc} */ @Override @CanIgnoreReturnValue public Builder addMessageTypes(Iterable descriptors) { return addFileTypes(CelDescriptorUtil.getFileDescriptorsForDescriptors(descriptors)); } - /** {@inheritDoc} */ @Override @CanIgnoreReturnValue public Builder addFileTypes(FileDescriptor... fileDescriptors) { return addFileTypes(Arrays.asList(fileDescriptors)); } - /** {@inheritDoc} */ @Override @CanIgnoreReturnValue public Builder addFileTypes(Iterable fileDescriptors) { @@ -127,7 +120,6 @@ public Builder addFileTypes(Iterable fileDescriptors) { return this; } - /** {@inheritDoc} */ @Override @CanIgnoreReturnValue public Builder addFileTypes(FileDescriptorSet fileDescriptorSet) { @@ -135,7 +127,6 @@ public Builder addFileTypes(FileDescriptorSet fileDescriptorSet) { CelDescriptorUtil.getFileDescriptorsFromFileDescriptorSet(fileDescriptorSet)); } - /** {@inheritDoc} */ @Override @CanIgnoreReturnValue public Builder setTypeFactory(Function typeFactory) { @@ -143,7 +134,6 @@ public Builder setTypeFactory(Function typeFactory) { return this; } - /** {@inheritDoc} */ @Override @CanIgnoreReturnValue public Builder setStandardEnvironmentEnabled(boolean value) { @@ -151,7 +141,6 @@ public Builder setStandardEnvironmentEnabled(boolean value) { return this; } - /** {@inheritDoc} */ @Override @CanIgnoreReturnValue public Builder addLibraries(CelRuntimeLibrary... libraries) { @@ -159,7 +148,6 @@ public Builder addLibraries(CelRuntimeLibrary... libraries) { return this.addLibraries(Arrays.asList(libraries)); } - /** {@inheritDoc} */ @Override @CanIgnoreReturnValue public Builder addLibraries(Iterable libraries) { diff --git a/runtime/src/main/java/dev/cel/runtime/DefaultInterpreter.java b/runtime/src/main/java/dev/cel/runtime/DefaultInterpreter.java index b5c8733b2..44832a2e7 100644 --- a/runtime/src/main/java/dev/cel/runtime/DefaultInterpreter.java +++ b/runtime/src/main/java/dev/cel/runtime/DefaultInterpreter.java @@ -127,14 +127,12 @@ public DefaultInterpreter( this.celOptions = celOptions; } - /** {@inheritDoc} */ @Override @Deprecated public Interpretable createInterpretable(CheckedExpr checkedExpr) { return createInterpretable(CelProtoAbstractSyntaxTree.fromCheckedExpr(checkedExpr).getAst()); } - /** {@inheritDoc} */ @Override public Interpretable createInterpretable(CelAbstractSyntaxTree ast) { return new DefaultInterpretable(typeProvider, dispatcher, ast, celOptions); @@ -164,13 +162,21 @@ private static final class DefaultInterpretable @Override public Object eval(GlobalResolver resolver) throws InterpreterException { // Result is already unwrapped from IntermediateResult. - return evalTrackingUnknowns(RuntimeUnknownResolver.fromResolver(resolver)); + return eval(resolver, CelEvaluationListener.noOpListener()); } @Override - public Object evalTrackingUnknowns(RuntimeUnknownResolver resolver) + public Object eval(GlobalResolver resolver, CelEvaluationListener listener) throws InterpreterException { - ExecutionFrame frame = new ExecutionFrame(resolver, celOptions.comprehensionMaxIterations()); + return evalTrackingUnknowns(RuntimeUnknownResolver.fromResolver(resolver), listener); + } + + @Override + public Object evalTrackingUnknowns( + RuntimeUnknownResolver resolver, CelEvaluationListener listener) + throws InterpreterException { + ExecutionFrame frame = + new ExecutionFrame(listener, resolver, celOptions.comprehensionMaxIterations()); IntermediateResult internalResult = evalInternal(frame, ast.getExpr()); Object result = internalResult.value(); // TODO: remove support for IncompleteData. @@ -182,27 +188,38 @@ private IntermediateResult evalInternal(ExecutionFrame frame, CelExpr expr) throws InterpreterException { try { ExprKind.Kind exprKind = expr.exprKind().getKind(); + IntermediateResult result; switch (exprKind) { case CONSTANT: - return IntermediateResult.create(evalConstant(frame, expr, expr.constant())); + result = IntermediateResult.create(evalConstant(frame, expr, expr.constant())); + break; case IDENT: - return evalIdent(frame, expr, expr.ident()); + result = evalIdent(frame, expr, expr.ident()); + break; case SELECT: - return evalSelect(frame, expr, expr.select()); + result = evalSelect(frame, expr, expr.select()); + break; case CALL: - return evalCall(frame, expr, expr.call()); + result = evalCall(frame, expr, expr.call()); + break; case CREATE_LIST: - return evalList(frame, expr, expr.createList()); + result = evalList(frame, expr, expr.createList()); + break; case CREATE_STRUCT: - return evalStruct(frame, expr, expr.createStruct()); + result = evalStruct(frame, expr, expr.createStruct()); + break; case CREATE_MAP: - return evalMap(frame, expr.createMap()); + result = evalMap(frame, expr.createMap()); + break; case COMPREHENSION: - return evalComprehension(frame, expr, expr.comprehension()); + result = evalComprehension(frame, expr, expr.comprehension()); + break; default: throw new IllegalStateException( "unexpected expression kind: " + expr.exprKind().getKind()); } + frame.getEvaluationListener().callback(expr, result.value()); + return result; } catch (RuntimeException e) { throw new InterpreterException.Builder(e, e.getMessage()) .setLocation(metadata, expr.id()) @@ -816,18 +833,27 @@ private IntermediateResult evalComprehension( /** This class tracks the state meaningful to a single evaluation pass. */ private static class ExecutionFrame { + private final CelEvaluationListener evaluationListener; private final int maxIterations; private final ArrayDeque resolvers; private RuntimeUnknownResolver currentResolver; private int iterations; - private ExecutionFrame(RuntimeUnknownResolver resolver, int maxIterations) { + private ExecutionFrame( + CelEvaluationListener evaluationListener, + RuntimeUnknownResolver resolver, + int maxIterations) { + this.evaluationListener = evaluationListener; this.resolvers = new ArrayDeque<>(); this.resolvers.add(resolver); this.currentResolver = resolver; this.maxIterations = maxIterations; } + private CelEvaluationListener getEvaluationListener() { + return evaluationListener; + } + private RuntimeUnknownResolver getResolver() { return currentResolver; } diff --git a/runtime/src/main/java/dev/cel/runtime/Interpretable.java b/runtime/src/main/java/dev/cel/runtime/Interpretable.java index 02462171c..0c967416a 100644 --- a/runtime/src/main/java/dev/cel/runtime/Interpretable.java +++ b/runtime/src/main/java/dev/cel/runtime/Interpretable.java @@ -28,4 +28,6 @@ public interface Interpretable { /** Runs interpretation with the given activation which supplies name/value bindings. */ Object eval(GlobalResolver resolver) throws InterpreterException; + + Object eval(GlobalResolver resolver, CelEvaluationListener listener) throws InterpreterException; } diff --git a/runtime/src/main/java/dev/cel/runtime/UnknownTrackingInterpretable.java b/runtime/src/main/java/dev/cel/runtime/UnknownTrackingInterpretable.java index 68d9d483a..96dbd2b6d 100644 --- a/runtime/src/main/java/dev/cel/runtime/UnknownTrackingInterpretable.java +++ b/runtime/src/main/java/dev/cel/runtime/UnknownTrackingInterpretable.java @@ -23,5 +23,6 @@ */ @Internal public interface UnknownTrackingInterpretable { - Object evalTrackingUnknowns(RuntimeUnknownResolver resolver) throws InterpreterException; + Object evalTrackingUnknowns(RuntimeUnknownResolver resolver, CelEvaluationListener listener) + throws InterpreterException; } diff --git a/runtime/src/test/java/dev/cel/runtime/BUILD.bazel b/runtime/src/test/java/dev/cel/runtime/BUILD.bazel index b33e16ccc..17e4179e2 100644 --- a/runtime/src/test/java/dev/cel/runtime/BUILD.bazel +++ b/runtime/src/test/java/dev/cel/runtime/BUILD.bazel @@ -14,26 +14,30 @@ java_library( deps = [ "//:auto_value", "//:java_truth", + "//bundle:cel", "//common", "//common:error_codes", "//common:options", - "//common:proto_ast", "//common:proto_v1alpha1_ast", "//common:runtime_exception", + "//common/ast", "//common/internal:converter", "//common/internal:dynamic_proto", "//common/resources/testdata/proto2:messages_extensions_proto2_java_proto", "//common/resources/testdata/proto2:messages_proto2_java_proto", "//common/resources/testdata/proto3:test_all_types_java_proto", + "//common/types", "//common/types:cel_v1alpha1_types", "//compiler", "//compiler:compiler_builder", + "//parser:macro", + "//parser:unparser", "//runtime", + "//runtime:evaluation_listener", "//runtime:interpreter", "//runtime:runtime_helper", "//runtime:unknown_attributes", "//runtime:unknown_options", - "@cel_spec//proto/cel/expr:expr_java_proto", "@com_google_googleapis//google/api/expr/v1alpha1:expr_java_proto", "@com_google_googleapis//google/rpc/context:attribute_context_java_proto", "@maven//:com_google_guava_guava", diff --git a/runtime/src/test/java/dev/cel/runtime/CelRuntimeTest.java b/runtime/src/test/java/dev/cel/runtime/CelRuntimeTest.java index 95ab0a5fe..0c4605cef 100644 --- a/runtime/src/test/java/dev/cel/runtime/CelRuntimeTest.java +++ b/runtime/src/test/java/dev/cel/runtime/CelRuntimeTest.java @@ -16,20 +16,32 @@ import static com.google.common.truth.Truth.assertThat; -import dev.cel.expr.CheckedExpr; import com.google.api.expr.v1alpha1.Constant; import com.google.api.expr.v1alpha1.Expr; import com.google.api.expr.v1alpha1.Type.PrimitiveType; import com.google.common.collect.ImmutableMap; import com.google.protobuf.Any; import com.google.protobuf.ByteString; -import com.google.protobuf.ExtensionRegistry; import com.google.rpc.context.AttributeContext; +import dev.cel.bundle.Cel; +import dev.cel.bundle.CelFactory; +import dev.cel.common.CelAbstractSyntaxTree; import dev.cel.common.CelOptions; -import dev.cel.common.CelProtoAbstractSyntaxTree; import dev.cel.common.CelProtoV1Alpha1AbstractSyntaxTree; +import dev.cel.common.CelSource; +import dev.cel.common.ast.CelConstant; +import dev.cel.common.ast.CelExpr; +import dev.cel.common.ast.CelExpr.ExprKind.Kind; import dev.cel.common.types.CelV1AlphaTypes; -import java.util.Base64; +import dev.cel.common.types.SimpleType; +import dev.cel.common.types.StructTypeReference; +import dev.cel.parser.CelStandardMacro; +import dev.cel.parser.CelUnparserFactory; +import dev.cel.testing.testdata.proto3.TestAllTypesProto.TestAllTypes; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.concurrent.atomic.AtomicReference; import org.junit.Test; import org.junit.runner.RunWith; import org.junit.runners.JUnit4; @@ -39,23 +51,15 @@ public class CelRuntimeTest { @Test public void evaluate_anyPackedEqualityUsingProtoDifferencer_success() throws Exception { - CelRuntime celRuntime = - CelRuntimeFactory.standardCelRuntimeBuilder() + Cel cel = + CelFactory.standardCelBuilder() .setOptions(CelOptions.current().enableProtoDifferencerEquality(true).build()) + .addVar("a", StructTypeReference.create(AttributeContext.getDescriptor().getFullName())) + .addVar("b", StructTypeReference.create(AttributeContext.getDescriptor().getFullName())) .addMessageTypes(AttributeContext.getDescriptor()) .build(); - // Checked Expression for 'a == b' where a, b are google.rpc.context.AttributeContext message - // types. - // This will be removed in favor of compiling the expression inside this test once Cel Compiler - // is OSSed. - String base64EncodedCheckedExpr = - "EgcIARIDCgFhEgcIAxIDCgFiEgwIAhIIGgZlcXVhbHMaKQgBEiVKI2dvb2dsZS5ycGMuY29udGV4dC5BdHRyaWJ1dGVDb250ZXh0GikIAxIlSiNnb29nbGUucnBjLmNvbnRleHQuQXR0cmlidXRlQ29udGV4dBoGCAISAhgBIhwQAjIYEgRfPT1fGgcQASIDCgFhGgcQAyIDCgFiKh4SBzxpbnB1dD4aAQciBAgBEAAiBAgCEAIiBAgDEAU="; - CheckedExpr expr = - CheckedExpr.parseFrom( - Base64.getDecoder().decode(base64EncodedCheckedExpr), - ExtensionRegistry.getEmptyRegistry()); - CelRuntime.Program program = - celRuntime.createProgram(CelProtoAbstractSyntaxTree.fromCheckedExpr(expr).getAst()); + CelAbstractSyntaxTree ast = cel.compile("a == b").getAst(); + CelRuntime.Program program = cel.createProgram(ast); Object evaluatedResult = program.eval( @@ -102,4 +106,197 @@ public void evaluate_v1alpha1CheckedExpr() throws Exception { assertThat(evaluatedResult).isEqualTo("Hello world!"); } + + @Test + public void trace_callExpr_identifyFalseBranch() throws Exception { + AtomicReference capturedExpr = new AtomicReference<>(); + CelEvaluationListener listener = + (expr, res) -> { + if (res instanceof Boolean && !(boolean) res && capturedExpr.get() == null) { + capturedExpr.set(expr); + } + }; + Cel cel = + CelFactory.standardCelBuilder() + .addVar("a", SimpleType.INT) + .addVar("b", SimpleType.INT) + .addVar("c", SimpleType.INT) + .build(); + CelAbstractSyntaxTree ast = cel.compile("a < 0 && b < 0 && c < 0").getAst(); + + boolean result = + (boolean) cel.createProgram(ast).trace(ImmutableMap.of("a", -1, "b", 1, "c", -4), listener); + + assertThat(result).isFalse(); + // Demonstrate that "b < 0" is what caused the expression to be false + CelAbstractSyntaxTree subtree = + CelAbstractSyntaxTree.newParsedAst(capturedExpr.get(), CelSource.newBuilder().build()); + assertThat(CelUnparserFactory.newUnparser().unparse(subtree)).isEqualTo("b < 0"); + } + + @Test + public void trace_constant() throws Exception { + CelEvaluationListener listener = + (expr, res) -> { + assertThat(res).isEqualTo("hello world"); + assertThat(expr.constant().getKind()).isEqualTo(CelConstant.Kind.STRING_VALUE); + }; + Cel cel = CelFactory.standardCelBuilder().build(); + CelAbstractSyntaxTree ast = cel.compile("'hello world'").getAst(); + + String result = (String) cel.createProgram(ast).trace(listener); + + assertThat(result).isEqualTo("hello world"); + } + + @Test + public void trace_ident() throws Exception { + CelEvaluationListener listener = + (expr, res) -> { + assertThat(res).isEqualTo("test"); + assertThat(expr.ident().name()).isEqualTo("a"); + }; + Cel cel = CelFactory.standardCelBuilder().addVar("a", SimpleType.STRING).build(); + CelAbstractSyntaxTree ast = cel.compile("a").getAst(); + + String result = (String) cel.createProgram(ast).trace(ImmutableMap.of("a", "test"), listener); + + assertThat(result).isEqualTo("test"); + } + + @Test + public void trace_select() throws Exception { + CelEvaluationListener listener = + (expr, res) -> { + if (expr.exprKind().getKind().equals(Kind.SELECT)) { + assertThat(res).isEqualTo(3L); + assertThat(expr.select().field()).isEqualTo("single_int64"); + } + }; + Cel cel = + CelFactory.standardCelBuilder() + .addMessageTypes(TestAllTypes.getDescriptor()) + .setContainer("dev.cel.testing.testdata.proto3") + .build(); + CelAbstractSyntaxTree ast = cel.compile("TestAllTypes{single_int64: 3}.single_int64").getAst(); + + Long result = (Long) cel.createProgram(ast).trace(listener); + + assertThat(result).isEqualTo(3L); + } + + @Test + public void trace_createStruct() throws Exception { + CelEvaluationListener listener = + (expr, res) -> { + assertThat(res).isEqualTo(TestAllTypes.getDefaultInstance()); + assertThat(expr.createStruct().messageName()).isEqualTo("TestAllTypes"); + }; + Cel cel = + CelFactory.standardCelBuilder() + .addMessageTypes(TestAllTypes.getDescriptor()) + .setContainer("dev.cel.testing.testdata.proto3") + .build(); + CelAbstractSyntaxTree ast = cel.compile("TestAllTypes{}").getAst(); + + TestAllTypes result = (TestAllTypes) cel.createProgram(ast).trace(listener); + + assertThat(result).isEqualTo(TestAllTypes.getDefaultInstance()); + } + + @Test + @SuppressWarnings("unchecked") // Test only + public void trace_createList() throws Exception { + CelEvaluationListener listener = + (expr, res) -> { + if (expr.exprKind().getKind().equals(Kind.CREATE_LIST)) { + assertThat((List) res).containsExactly(1L, 2L, 3L); + assertThat(expr.createList().elements()).hasSize(3); + } + }; + Cel cel = CelFactory.standardCelBuilder().build(); + CelAbstractSyntaxTree ast = cel.compile("[1, 2, 3]").getAst(); + + List result = (List) cel.createProgram(ast).trace(listener); + + assertThat(result).containsExactly(1L, 2L, 3L); + } + + @Test + @SuppressWarnings("unchecked") // Test only + public void trace_createMap() throws Exception { + CelEvaluationListener listener = + (expr, res) -> { + if (expr.exprKind().getKind().equals(Kind.CREATE_MAP)) { + assertThat((Map) res).containsExactly(1L, "a"); + assertThat(expr.createMap().entries()).hasSize(1); + } + }; + Cel cel = CelFactory.standardCelBuilder().build(); + CelAbstractSyntaxTree ast = cel.compile("{1: 'a'}").getAst(); + + Map result = (Map) cel.createProgram(ast).trace(listener); + + assertThat(result).containsExactly(1L, "a"); + } + + @Test + public void trace_comprehension() throws Exception { + CelEvaluationListener listener = + (expr, res) -> { + if (expr.exprKind().getKind().equals(Kind.COMPREHENSION)) { + assertThat(expr.comprehension().iterVar()).isEqualTo("i"); + } + }; + Cel cel = + CelFactory.standardCelBuilder().setStandardMacros(CelStandardMacro.STANDARD_MACROS).build(); + CelAbstractSyntaxTree ast = cel.compile("[true].exists(i, i)").getAst(); + + boolean result = (boolean) cel.createProgram(ast).trace(listener); + + assertThat(result).isTrue(); + } + + @Test + public void trace_withMessageInput() throws Exception { + CelEvaluationListener listener = + (expr, res) -> { + assertThat(res).isEqualTo(6L); + assertThat(expr.ident().name()).isEqualTo("single_int64"); + }; + Cel cel = + CelFactory.standardCelBuilder() + .addMessageTypes(TestAllTypes.getDescriptor()) + .addVar("single_int64", SimpleType.INT) + .build(); + CelAbstractSyntaxTree ast = cel.compile("single_int64").getAst(); + + Long result = + (Long) + cel.createProgram(ast) + .trace(TestAllTypes.newBuilder().setSingleInt64(6L).build(), listener); + + assertThat(result).isEqualTo(6L); + } + + @Test + public void trace_withVariableResolver() throws Exception { + CelEvaluationListener listener = + (expr, res) -> { + assertThat(res).isEqualTo("hello"); + assertThat(expr.ident().name()).isEqualTo("variable"); + }; + Cel cel = + CelFactory.standardCelBuilder() + .addMessageTypes(TestAllTypes.getDescriptor()) + .addVar("variable", SimpleType.STRING) + .build(); + CelAbstractSyntaxTree ast = cel.compile("variable").getAst(); + CelVariableResolver resolver = + (name) -> name.equals("variable") ? Optional.of("hello") : Optional.empty(); + + String result = (String) cel.createProgram(ast).trace(resolver, listener); + + assertThat(result).isEqualTo("hello"); + } }