From 5facf075c3ff8443691962560e14b2d595ac0ba2 Mon Sep 17 00:00:00 2001 From: Sean Huh Date: Sat, 24 Oct 2026 17:17:53 -0700 Subject: [PATCH] No public description PiperOrigin-RevId: 1006483129 --- .../optimizers/ConstantFoldingOptimizer.java | 212 ++++++++++++++++-- .../dev/cel/optimizer/optimizers/BUILD.bazel | 2 + .../ConstantFoldingOptimizerTest.java | 121 ++++++++++ 3 files changed, 316 insertions(+), 19 deletions(-) diff --git a/optimizer/src/main/java/dev/cel/optimizer/optimizers/ConstantFoldingOptimizer.java b/optimizer/src/main/java/dev/cel/optimizer/optimizers/ConstantFoldingOptimizer.java index aa724fdb6..244d0a8ae 100644 --- a/optimizer/src/main/java/dev/cel/optimizer/optimizers/ConstantFoldingOptimizer.java +++ b/optimizer/src/main/java/dev/cel/optimizer/optimizers/ConstantFoldingOptimizer.java @@ -38,6 +38,7 @@ import dev.cel.common.ast.CelMutableExpr.CelMutableCall; import dev.cel.common.ast.CelMutableExpr.CelMutableList; import dev.cel.common.ast.CelMutableExpr.CelMutableMap; +import dev.cel.common.ast.CelMutableExpr.CelMutableSelect; import dev.cel.common.ast.CelMutableExpr.CelMutableStruct; import dev.cel.common.ast.CelMutableExprConverter; import dev.cel.common.internal.DateTimeHelpers; @@ -55,7 +56,6 @@ import dev.cel.common.values.StructValue; import dev.cel.extensions.CelOptionalLibrary.Function; import dev.cel.optimizer.AstMutator; -import dev.cel.optimizer.AstMutator.SubtreeReplacement; import dev.cel.optimizer.CelAstOptimizer; import dev.cel.optimizer.CelOptimizationException; import dev.cel.runtime.CelAttributePattern; @@ -73,6 +73,7 @@ import java.util.Map; import java.util.Objects; import java.util.Optional; +import java.util.Set; import org.jspecify.annotations.Nullable; /** @@ -141,7 +142,7 @@ public OptimizationResult optimize(CelAbstractSyntaxTree ast, Cel cel) ImmutableMap identTypes = precomputeIdentTypes(initialMutableAst); CelMutableAst mutableAst = - foldConstants(optimizerEnv, valueProvider, identTypes, initialMutableAst); + foldConstants(cel, optimizerEnv, valueProvider, identTypes, initialMutableAst); mutableAst = pruneOptionalElements(mutableAst); if (mutableAst == initialMutableAst) { @@ -172,23 +173,25 @@ private static ImmutableMap precomputeIdentTypes(CelMutableAst } private CelMutableAst foldConstants( + Cel cel, Cel optimizerEnv, @Nullable CelValueProvider valueProvider, ImmutableMap identTypes, CelMutableAst mutableAst) throws CelOptimizationException { for (int iterCount = 0; iterCount < constantFoldingOptions.maxIterationLimit(); iterCount++) { - Optional replacement = - findNextFoldableSubtree(optimizerEnv, valueProvider, identTypes, mutableAst); - if (!replacement.isPresent()) { + Optional nextAst = + findNextFoldableSubtree(cel, optimizerEnv, valueProvider, identTypes, mutableAst); + if (!nextAst.isPresent()) { return mutableAst; } - mutableAst = astMutator.replaceSubtree(mutableAst, replacement.get()); + mutableAst = nextAst.get(); } throw new IllegalStateException("Max iteration count reached."); } - private Optional findNextFoldableSubtree( + private Optional findNextFoldableSubtree( + Cel cel, Cel optimizerEnv, @Nullable CelValueProvider valueProvider, ImmutableMap identTypes, @@ -199,19 +202,40 @@ private Optional findNextFoldableSubtree( navAst.getRoot().allNodes(TraversalOrder.PRE_ORDER).filter(this::canFold).iterator(); while (foldableExprs.hasNext()) { CelNavigableMutableExpr foldableExpr = foldableExprs.next(); - Optional pruned = + Optional candidate = maybePruneBranches(mutableAst, identTypes, foldableExpr.expr()); - if (pruned.isPresent()) { - return Optional.of(SubtreeReplacement.of(foldableExpr.id(), pruned.get())); + if (!candidate.isPresent()) { + candidate = maybeFold(optimizerEnv, valueProvider, foldableExpr); } - Optional folded = maybeFold(optimizerEnv, valueProvider, foldableExpr); - if (folded.isPresent()) { - return Optional.of(SubtreeReplacement.of(foldableExpr.id(), folded.get())); + if (candidate.isPresent()) { + Optional replacedAst = + tryReplaceSubtree(cel, optimizerEnv, mutableAst, foldableExpr, candidate.get()); + if (replacedAst.isPresent()) { + return replacedAst; + } } } return Optional.empty(); } + private Optional tryReplaceSubtree( + Cel cel, + Cel optimizerEnv, + CelMutableAst mutableAst, + CelNavigableMutableExpr foldableExpr, + CelMutableExpr candidate) { + // Folding an expression containing a dynamic identifier can narrow its static type from dyn to + // a concrete type (e.g. `false || [3, x][0]` -> `false || 3`), which may cause the enclosing + // expression or expected result type to fail type-checking. + CelMutableAst candidateAst = + astMutator.replaceSubtree( + CelMutableAst.fromCelAst(mutableAst.toParsedAst()), candidate, foldableExpr.id()); + Cel envToCheck = foldableExpr.parent().isPresent() ? optimizerEnv : cel; + return envToCheck.check(candidateAst.toParsedAst()).hasError() + ? Optional.empty() + : Optional.of(candidateAst); + } + private boolean canFold(CelNavigableMutableExpr navigableExpr) { switch (navigableExpr.getKind()) { case CALL: @@ -269,12 +293,13 @@ private boolean canFold(CelNavigableMutableExpr navigableExpr) { return !CelNavigableExprUtil.hasComprehensionVariable(navigableExpr); } - // Default case: all call arguments must be constants. If the argument is a container (ex: - // list, map), then its arguments must be a constant. - return areChildrenArgConstant(navigableExpr); + return areChildrenArgConstant(navigableExpr) + || maybePruneContainerLookup(navigableExpr.expr()).isPresent() + || maybePruneOptionalCall(mutableCall).isPresent(); case SELECT: CelNavigableMutableExpr operand = navigableExpr.children().collect(onlyElement()); - return areChildrenArgConstant(operand); + return areChildrenArgConstant(operand) + || maybePruneContainerLookup(navigableExpr.expr()).isPresent(); case COMPREHENSION: return !isNestedComprehension(navigableExpr) && containsFoldableFunctionOnly(navigableExpr); default: @@ -282,6 +307,47 @@ private boolean canFold(CelNavigableMutableExpr navigableExpr) { } } + private static boolean isErrorFree(CelMutableExpr expr) { + switch (expr.getKind()) { + case CONSTANT: + case IDENT: + return true; + case CALL: + CelMutableCall call = expr.call(); + return call.function().equals(Function.OPTIONAL_NONE.getFunction()) + || (call.function().equals(Function.OPTIONAL_OF.getFunction()) + && isErrorFree(call.args().get(0))); + case LIST: + CelMutableList list = expr.list(); + return list.optionalIndices().isEmpty() + && list.elements().stream().allMatch(ConstantFoldingOptimizer::isErrorFree); + case MAP: + return isErrorFreeMap(expr.map()); + default: + return false; + } + } + + private static boolean isErrorFreeMap(CelMutableMap map) { + Set keysSeen = new HashSet<>(); + for (CelMutableMap.Entry entry : map.entries()) { + if (entry.optionalEntry() + || !entry.key().getKind().equals(Kind.CONSTANT) + || !isErrorFree(entry.value()) + || !keysSeen.add(normalizeMapKey(entry.key().constant()))) { + return false; + } + } + return true; + } + + private static CelConstant normalizeMapKey(CelConstant key) { + if (key.getKind().equals(CelConstant.Kind.UINT64_VALUE) && key.uint64Value().longValue() >= 0) { + return CelConstant.ofValue(key.uint64Value().longValue()); + } + return key; + } + private boolean containsFoldableFunctionOnly(CelNavigableMutableExpr navigableExpr) { return navigableExpr .allNodes() @@ -334,7 +400,13 @@ private Optional maybeFold( Object result; try { result = evaluateExpr(cel, node); - } catch (CelEvaluationException | CelValidationException e) { + } catch (CelValidationException | CelEvaluationException e) { + if (node.getKind().equals(Kind.COMPREHENSION)) { + // Comprehension may fail to evaluate due to other unknowns the expression references. We'll + // simply opt to not fold the subtree and move on. + return Optional.empty(); + } + throw new CelOptimizationException( "Constant folding failure. Failed to evaluate subtree due to: " + e.getMessage(), e); } @@ -479,7 +551,7 @@ private static boolean isCallToFunction(CelMutableExpr expr, String functionName private Optional maybePruneBranches( CelMutableAst mutableAst, Map identTypes, CelMutableExpr expr) { if (!expr.getKind().equals(Kind.CALL)) { - return Optional.empty(); + return maybePruneContainerLookup(expr); } CelMutableCall call = expr.call(); @@ -575,6 +647,108 @@ private Optional maybePruneBranches( return replacementExpr; } + Optional containerLookup = maybePruneContainerLookup(expr); + return containerLookup.isPresent() ? containerLookup : maybePruneOptionalCall(call); + } + + private static Optional maybePruneOptionalCall(CelMutableCall call) { + if (!call.target().isPresent()) { + return Optional.empty(); + } + String function = call.function(); + CelMutableExpr target = call.target().get(); + if (function.equals(Function.OR.getFunction()) + || function.equals(Function.OR_VALUE.getFunction())) { + CelMutableExpr arg = call.args().get(0); + if (isCallToFunction(target, Function.OPTIONAL_NONE.getFunction())) { + return Optional.of(arg); + } + if (isCallToFunction(target, Function.OPTIONAL_OF.getFunction()) + && isErrorFree(target) + && isErrorFree(arg)) { + return Optional.of( + function.equals(Function.OR.getFunction()) ? target : target.call().args().get(0)); + } + } else if (function.equals(Function.VALUE.getFunction())) { + if (isCallToFunction(target, Function.OPTIONAL_OF.getFunction()) && isErrorFree(target)) { + return Optional.of(target.call().args().get(0)); + } + } else if (function.equals(Function.HAS_VALUE.getFunction())) { + if (isCallToFunction(target, Function.OPTIONAL_NONE.getFunction())) { + return Optional.of(CelMutableExpr.ofConstant(CelConstant.ofValue(false))); + } + if (isCallToFunction(target, Function.OPTIONAL_OF.getFunction()) && isErrorFree(target)) { + return Optional.of(CelMutableExpr.ofConstant(CelConstant.ofValue(true))); + } + } + return Optional.empty(); + } + + private static Optional maybePruneContainerLookup(CelMutableExpr expr) { + if (expr.getKind().equals(Kind.SELECT)) { + CelMutableSelect select = expr.select(); + if (select.operand().getKind().equals(Kind.MAP) && isErrorFreeMap(select.operand().map())) { + Optional val = + findMapEntryByConstantKey(select.operand().map(), CelConstant.ofValue(select.field())); + return select.testOnly() + ? Optional.of(CelMutableExpr.ofConstant(CelConstant.ofValue(val.isPresent()))) + : val; + } + return Optional.empty(); + } + if (!expr.getKind().equals(Kind.CALL)) { + return Optional.empty(); + } + CelMutableCall call = expr.call(); + String function = call.function(); + if (function.equals(Operator.INDEX.getFunction())) { + return findContainerElement(call.args().get(0), call.args().get(1), /* optional= */ false); + } + if (function.equals(Operator.OPTIONAL_INDEX.getFunction()) + || function.equals(Operator.OPTIONAL_SELECT.getFunction())) { + return findContainerElement(call.args().get(0), call.args().get(1), /* optional= */ true); + } + return Optional.empty(); + } + + private static Optional findContainerElement( + CelMutableExpr target, CelMutableExpr key, boolean optional) { + Optional elem; + if (target.getKind().equals(Kind.LIST) + && isErrorFree(target) + && isExprConstantOfKind(key, CelConstant.Kind.INT64_VALUE)) { + long idx = key.constant().int64Value(); + List elements = target.list().elements(); + elem = + idx >= 0 && idx < elements.size() + ? Optional.of(elements.get((int) idx)) + : Optional.empty(); + } else if (target.getKind().equals(Kind.MAP) + && isErrorFreeMap(target.map()) + && key.getKind().equals(Kind.CONSTANT)) { + elem = findMapEntryByConstantKey(target.map(), key.constant()); + } else { + return Optional.empty(); + } + if (!optional) { + return elem; + } + return Optional.of( + elem.map( + val -> + CelMutableExpr.ofCall( + CelMutableCall.create(Function.OPTIONAL_OF.getFunction(), val))) + .orElseGet(ConstantFoldingOptimizer::newOptionalNoneExpr)); + } + + private static Optional findMapEntryByConstantKey( + CelMutableMap map, CelConstant targetKey) { + CelConstant normalizedTarget = normalizeMapKey(targetKey); + for (CelMutableMap.Entry entry : map.entries()) { + if (normalizeMapKey(entry.key().constant()).equals(normalizedTarget)) { + return Optional.of(entry.value()); + } + } return Optional.empty(); } diff --git a/optimizer/src/test/java/dev/cel/optimizer/optimizers/BUILD.bazel b/optimizer/src/test/java/dev/cel/optimizer/optimizers/BUILD.bazel index 13d8e1e46..80d9f4246 100644 --- a/optimizer/src/test/java/dev/cel/optimizer/optimizers/BUILD.bazel +++ b/optimizer/src/test/java/dev/cel/optimizer/optimizers/BUILD.bazel @@ -49,6 +49,8 @@ java_library( "@maven//:com_google_testparameterinjector_test_parameter_injector", "//:java_truth", "@maven//:com_google_truth_extensions_truth_proto_extension", + "//verifier", + "//verifier:verifier_factory", "@cel_spec//proto/cel/expr:syntax_java_proto", "@cel_spec//proto/cel/expr/conformance/proto2:test_all_types_java_proto", "@cel_spec//proto/cel/expr/conformance/proto3:test_all_types_java_proto", diff --git a/optimizer/src/test/java/dev/cel/optimizer/optimizers/ConstantFoldingOptimizerTest.java b/optimizer/src/test/java/dev/cel/optimizer/optimizers/ConstantFoldingOptimizerTest.java index 3c6bcf6d3..04f1bd5c2 100644 --- a/optimizer/src/test/java/dev/cel/optimizer/optimizers/ConstantFoldingOptimizerTest.java +++ b/optimizer/src/test/java/dev/cel/optimizer/optimizers/ConstantFoldingOptimizerTest.java @@ -50,6 +50,10 @@ import dev.cel.parser.CelUnparserFactory; import dev.cel.runtime.CelFunctionBinding; import dev.cel.testing.CelRuntimeFlavor; +import dev.cel.verifier.CelVerificationResult; +import dev.cel.verifier.CelVerificationResult.VerificationStatus; +import dev.cel.verifier.CelVerifier; +import dev.cel.verifier.CelVerifierFactory; import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; @@ -71,6 +75,7 @@ public class ConstantFoldingOptimizerTest { @Before public void setUp() { + System.setProperty("z3.skipLibraryLoad", "true"); this.cel = setupEnv(runtimeFlavor.builder()); this.celOptimizer = CelOptimizerFactory.standardCelOptimizerBuilder(this.cel) @@ -84,6 +89,7 @@ private static Cel setupEnv(CelBuilder celBuilder) { .addVar("y", SimpleType.DYN) .addVar("dyn_x", SimpleType.DYN) .addVar("int_x", SimpleType.INT) + .addVar("int_y", SimpleType.INT) .addVar("double_x", SimpleType.DOUBLE) .addVar("bool_x", SimpleType.BOOL) .addVar("string_x", SimpleType.STRING) @@ -129,12 +135,14 @@ private static Cel setupEnv(CelBuilder celBuilder) { CelExtensions.comprehensions(), CelExtensions.bindings(), CelOptionalLibrary.INSTANCE, + CelExtensions.lists(), CelExtensions.math(), CelExtensions.strings(), CelExtensions.sets(CEL_OPTIONS), CelExtensions.encoders(CEL_OPTIONS)) .addRuntimeLibraries( CelOptionalLibrary.INSTANCE, + CelExtensions.lists(), CelExtensions.math(), CelExtensions.strings(), CelExtensions.sets(CEL_OPTIONS), @@ -591,6 +599,14 @@ public void constantFold_macros_withoutMacroCallMetadata(String source) throws E @TestParameters("{source: 'x != false'}") @TestParameters("{source: 'false != x'}") @TestParameters("{source: '[x].exists(item, item == true)'}") + @TestParameters("{source: '[1, 2, 3, ?optional_int_x][0]'}") + @TestParameters("{source: '[1, 2, 3, int_x][4]'}") + @TestParameters("{source: '[1, 2, 3, int_x][-1]'}") + @TestParameters("{source: '{\"a\": 1, \"a\": 2, \"b\": x}[\"b\"]'}") + @TestParameters("{source: '{\"a\": 1, \"b\": int_x}[string_x]'}") + @TestParameters("{source: '{1: 10, 1u: 20, 2: x}[2]'}") + @TestParameters("{source: '{\"a\": 1, ?string_x: optional_int_x}[\"a\"]'}") + @TestParameters("{source: '{\"a\": 2, string_x: 1}.a'}") public void constantFold_noOp(String source) throws Exception { CelAbstractSyntaxTree ast = cel.compile(source).getAst(); @@ -954,4 +970,109 @@ public void constantFold_inOperator_withoutMacros_skipsDoubleNan() throws Except assertThat(CEL_UNPARSER.unparse(optimizedAst)).isEqualTo("NaN in [NaN, double_x]"); } + + @Test + @TestParameters("{source: '{0: x, 2u: y}[0u]', expected: 'x'}") + @TestParameters("{source: '{-1: 10, 18446744073709551615u: int_x}[-1]', expected: '10'}") + @TestParameters("{source: '[{\"a\": 1, \"b\": int_x}][0].a', expected: '1'}") + @TestParameters( + "{source: 'optional.none().orValue(int_x in [1, 2])', expected: 'int_x in [1, 2]'}") + public void constantFold_partialEvaluation_success(String source, String expected) + throws Exception { + CelAbstractSyntaxTree ast = cel.compile(source).getAst(); + + CelAbstractSyntaxTree optimizedAst = celOptimizer.optimize(ast); + + assertThat(CEL_UNPARSER.unparse(optimizedAst)).isEqualTo(expected); + } + + @Test + public void constantFold_partialEvaluation_rootResultTypeMismatch_skipsFold() throws Exception { + Cel stringResultCel = + setupEnv(runtimeFlavor.builder()).toCelBuilder().setResultType(SimpleType.STRING).build(); + CelOptimizer optimizer = + CelOptimizerFactory.standardCelOptimizerBuilder(stringResultCel) + .addAstOptimizers(ConstantFoldingOptimizer.getInstance()) + .build(); + CelAbstractSyntaxTree ast = stringResultCel.compile("[1, x][0]").getAst(); + + CelAbstractSyntaxTree optimizedAst = optimizer.optimize(ast); + + assertThat(CEL_UNPARSER.unparse(optimizedAst)).isEqualTo("[1, x][0]"); + } + + @Test + @TestParameters("{source: '[1, 2, 3, int_x][0]', expected: '1'}") + @TestParameters("{source: '[1, 2, 3, int_x][3]', expected: 'int_x'}") + @TestParameters("{source: '[1, 2, 3, int_x][?-1]', expected: 'optional.none()'}") + @TestParameters("{source: '[1, 2, 3, int_x][?0]', expected: 'optional.of(1)'}") + @TestParameters("{source: '[1, 2, 3, int_x][?3]', expected: 'optional.of(int_x)'}") + @TestParameters("{source: '[1, 2, 3, int_x][?4]', expected: 'optional.none()'}") + @TestParameters("{source: '[1, 2, 3, int_x][?0].orValue(99)', expected: '1'}") + @TestParameters("{source: '[1, 2, 3, int_x][?3].orValue(99)', expected: 'int_x'}") + @TestParameters("{source: '[1, 2, 3, int_x][?4].orValue(int_x)', expected: 'int_x'}") + @TestParameters("{source: '{\"a\": 1, \"b\": int_x}[\"a\"]', expected: '1'}") + @TestParameters("{source: '{\"a\": 1, \"b\": int_x}[\"b\"]', expected: 'int_x'}") + @TestParameters("{source: '{\"a\": 1, \"b\": int_x}.a', expected: '1'}") + @TestParameters("{source: '{\"a\": 1, \"b\": int_x}.b', expected: 'int_x'}") + @TestParameters("{source: 'has({\"a\": 1, \"b\": int_x}.a)', expected: 'true'}") + @TestParameters("{source: 'has({\"a\": 1, \"b\": int_x}.b)', expected: 'true'}") + @TestParameters("{source: 'has({\"a\": 1, \"b\": int_x}.c)', expected: 'false'}") + @TestParameters("{source: '{\"a\": 1, \"b\": int_x}[?\"a\"]', expected: 'optional.of(1)'}") + @TestParameters("{source: '{\"a\": 1, \"b\": int_x}[?\"b\"]', expected: 'optional.of(int_x)'}") + @TestParameters("{source: '{\"a\": 1, \"b\": int_x}[?\"c\"]', expected: 'optional.none()'}") + @TestParameters("{source: '{\"a\": 1, \"b\": int_x}.?a', expected: 'optional.of(1)'}") + @TestParameters("{source: '{\"a\": 1, \"b\": int_x}.?b', expected: 'optional.of(int_x)'}") + @TestParameters("{source: '{\"a\": 1, \"b\": int_x}.?c', expected: 'optional.none()'}") + @TestParameters("{source: '{\"a\": 1, \"b\": int_x}.?a.orValue(0)', expected: '1'}") + @TestParameters("{source: '{\"a\": 1, \"b\": int_x}.?b.orValue(0)', expected: 'int_x'}") + @TestParameters("{source: '{\"a\": 1, \"b\": int_x}.?c.orValue(int_x)', expected: 'int_x'}") + @TestParameters("{source: 'optional.of(1).or(optional_int_x)', expected: 'optional.of(1)'}") + @TestParameters( + "{source: 'optional.of(int_x).or(optional.of(1))', expected: 'optional.of(int_x)'}") + @TestParameters( + "{source: 'optional.of(int_x).or(optional.none())', expected: 'optional.of(int_x)'}") + @TestParameters("{source: 'optional.none().or(optional_int_x)', expected: 'optional_int_x'}") + @TestParameters("{source: 'optional.of(1).orValue(int_x)', expected: '1'}") + @TestParameters("{source: 'optional.of(int_x).orValue(1)', expected: 'int_x'}") + @TestParameters("{source: 'optional.none().orValue(int_x)', expected: 'int_x'}") + @TestParameters("{source: 'optional.of(int_x).value()', expected: 'int_x'}") + @TestParameters("{source: 'optional.of(int_x).hasValue()', expected: 'true'}") + @TestParameters("{source: 'optional.none().hasValue()', expected: 'false'}") + public void verifyEquivalence_partialConstantFolding_isVerified(String source, String expected) + throws Exception { + CelAbstractSyntaxTree ast = cel.compile(source).getAst(); + + CelAbstractSyntaxTree optimizedAst = celOptimizer.optimize(ast); + + assertThat(CEL_UNPARSER.unparse(optimizedAst)).isEqualTo(expected); + if (runtimeFlavor != CelRuntimeFlavor.PLANNER) { + return; + } + CelVerifier verifier = CelVerifierFactory.newVerifier(cel).build(); + CelVerificationResult verificationResult = verifier.verifyEquivalence(ast, optimizedAst); + assertThat(verificationResult.status()).isEqualTo(VerificationStatus.VERIFIED); + } + + @Test + @TestParameters("{source: '[1, 1 / int_x][0]', unsoundFold: '1'}") + @TestParameters("{source: '[1, int_x + 1][0]', unsoundFold: '1'}") + @TestParameters("{source: '{\"a\": 2, string_x: 1}[\"a\"]', unsoundFold: '2'}") + @TestParameters("{source: 'optional.of(1).orValue(1 / int_x)', unsoundFold: '1'}") + @TestParameters("{source: 'optional.of(1 / int_x).orValue(0)', unsoundFold: '0'}") + public void verifyEquivalence_unsoundPartialFold_isViolated(String source, String unsoundFold) + throws Exception { + CelAbstractSyntaxTree ast = cel.compile(source).getAst(); + + CelAbstractSyntaxTree optimizedAst = celOptimizer.optimize(ast); + + assertThat(CEL_UNPARSER.unparse(optimizedAst)).isEqualTo(source); + if (runtimeFlavor != CelRuntimeFlavor.PLANNER) { + return; + } + CelVerifier verifier = CelVerifierFactory.newVerifier(cel).build(); + CelAbstractSyntaxTree unsoundAst = cel.compile(unsoundFold).getAst(); + assertThat(verifier.verifyEquivalence(ast, unsoundAst).status()) + .isEqualTo(VerificationStatus.VIOLATED); + } }