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..0acd1f540 100644 --- a/optimizer/src/main/java/dev/cel/optimizer/optimizers/ConstantFoldingOptimizer.java +++ b/optimizer/src/main/java/dev/cel/optimizer/optimizers/ConstantFoldingOptimizer.java @@ -19,6 +19,7 @@ import static com.google.common.collect.MoreCollectors.onlyElement; import static dev.cel.checker.CelStandardDeclarations.StandardFunction.DURATION; import static dev.cel.checker.CelStandardDeclarations.StandardFunction.TIMESTAMP; +import static dev.cel.checker.CelStandardDeclarations.StandardFunction.TYPE; import com.google.auto.value.AutoValue; import com.google.common.collect.ImmutableList; @@ -36,8 +37,10 @@ import dev.cel.common.ast.CelExpr.ExprKind.Kind; import dev.cel.common.ast.CelMutableExpr; import dev.cel.common.ast.CelMutableExpr.CelMutableCall; +import dev.cel.common.ast.CelMutableExpr.CelMutableComprehension; 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 +58,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 +75,7 @@ import java.util.Map; import java.util.Objects; import java.util.Optional; +import java.util.Set; import org.jspecify.annotations.Nullable; /** @@ -93,6 +96,20 @@ public final class ConstantFoldingOptimizer implements CelAstOptimizer { Operator.GREATER_EQUALS.getFunction(), Operator.IN.getFunction()); + private static final ImmutableSet OPTIONAL_PRODUCING_FUNCTIONS = + ImmutableSet.of( + Function.OPTIONAL_OF.getFunction(), + Function.FIRST.getFunction(), + Function.LAST.getFunction()); + + private static final ImmutableSet EQUALITY_FUNCTIONS = + ImmutableSet.of( + Operator.EQUALS.getFunction(), + Operator.NOT_EQUALS.getFunction(), + "sets.contains", + "sets.intersects", + "sets.equivalent"); + private static final ConstantFoldingOptimizer INSTANCE = new ConstantFoldingOptimizer(ConstantFoldingOptions.newBuilder().build()); @@ -136,12 +153,19 @@ public OptimizationResult optimize(CelAbstractSyntaxTree ast, Cel cel) } // Override the environment's expected type to generally allow all subtrees to be folded. Cel optimizerEnv = builder.setResultType(SimpleType.DYN).build(); + boolean disableStandardEquality = cel.toCheckerBuilder().options().disableCelStandardEquality(); CelMutableAst initialMutableAst = CelMutableAst.fromCelAst(ast); ImmutableMap identTypes = precomputeIdentTypes(initialMutableAst); CelMutableAst mutableAst = - foldConstants(optimizerEnv, valueProvider, identTypes, initialMutableAst); + foldConstants( + cel, + optimizerEnv, + disableStandardEquality, + valueProvider, + identTypes, + initialMutableAst); mutableAst = pruneOptionalElements(mutableAst); if (mutableAst == initialMutableAst) { @@ -172,47 +196,77 @@ private static ImmutableMap precomputeIdentTypes(CelMutableAst } private CelMutableAst foldConstants( + Cel cel, Cel optimizerEnv, + boolean disableStandardEquality, @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, disableStandardEquality, 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, + boolean disableStandardEquality, @Nullable CelValueProvider valueProvider, ImmutableMap identTypes, CelMutableAst mutableAst) throws CelOptimizationException { CelNavigableMutableAst navAst = CelNavigableMutableAst.fromAst(mutableAst); Iterator foldableExprs = - navAst.getRoot().allNodes(TraversalOrder.PRE_ORDER).filter(this::canFold).iterator(); + navAst + .getRoot() + .allNodes(TraversalOrder.PRE_ORDER) + .filter(node -> canFold(disableStandardEquality, node)) + .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, disableStandardEquality, 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 boolean canFold(CelNavigableMutableExpr navigableExpr) { + 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(boolean disableStandardEquality, CelNavigableMutableExpr navigableExpr) { switch (navigableExpr.getKind()) { case CALL: if (!containsFoldableFunctionOnly(navigableExpr)) { @@ -269,12 +323,14 @@ 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() + || canPartiallyEvaluate(disableStandardEquality, navigableExpr); 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 +338,96 @@ private boolean canFold(CelNavigableMutableExpr navigableExpr) { } } + private static boolean canPartiallyEvaluate( + boolean disableStandardEquality, CelNavigableMutableExpr navigableExpr) { + if (containsUnsupportedSentinelFunction(disableStandardEquality, navigableExpr)) { + return false; + } + return navigableExpr + .children() + .allMatch(child -> !child.getKind().equals(Kind.IDENT) && isErrorFree(child.expr())); + } + + private static boolean canPartiallyEvaluateComprehension( + boolean disableStandardEquality, CelNavigableMutableExpr navigableExpr) { + if (containsUnsupportedSentinelFunction(disableStandardEquality, navigableExpr)) { + return false; + } + CelMutableComprehension comprehension = navigableExpr.expr().comprehension(); + return isErrorFree(comprehension.iterRange()) && isErrorFree(comprehension.accuInit()); + } + + private static boolean containsUnsupportedSentinelFunction( + boolean disableStandardEquality, CelNavigableMutableExpr expr) { + return expr.allNodes() + .filter(node -> node.getKind().equals(Kind.CALL)) + .anyMatch( + node -> { + CelMutableCall call = node.expr().call(); + String fn = call.function(); + if (!DefaultOptimizerConstants.CEL_CANONICAL_FUNCTIONS.contains(fn) + || fn.equals(Function.OPTIONAL_OF_NON_ZERO_VALUE.getFunction()) + || fn.equals(TYPE.functionName()) + || fn.equals("flatten")) { + return true; + } + if (!node.equals(expr) && OPTIONAL_PRODUCING_FUNCTIONS.contains(fn)) { + return true; + } + if (fn.equals(Operator.IN.getFunction())) { + return !call.args().get(0).getKind().equals(Kind.CONSTANT) + || !call.args().get(1).getKind().equals(Kind.MAP) + || !isErrorFreeMap(call.args().get(1).map()); + } + if (EQUALITY_FUNCTIONS.contains(fn)) { + return disableStandardEquality + || node.children().allMatch(CelNavigableExprUtil::hasComprehensionVariable); + } + return false; + }); + } + + 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() @@ -325,16 +471,33 @@ private static boolean isNestedComprehension(CelNavigableMutableExpr expr) { } private Optional maybeFold( - Cel cel, @Nullable CelValueProvider valueProvider, CelNavigableMutableExpr node) + Cel cel, + boolean disableStandardEquality, + @Nullable CelValueProvider valueProvider, + CelNavigableMutableExpr node) throws CelOptimizationException { if (!node.getKind().equals(Kind.COMPREHENSION) && CelNavigableExprUtil.hasComprehensionVariable(node)) { return Optional.empty(); } + boolean allConstant = areChildrenArgConstant(node); + if (!allConstant + && !node.getKind().equals(Kind.COMPREHENSION) + && !canPartiallyEvaluate(disableStandardEquality, node)) { + return Optional.empty(); + } Object result; try { - result = evaluateExpr(cel, node); - } catch (CelEvaluationException | CelValidationException e) { + result = + evaluateExpr( + cel, + node, + !node.getKind().equals(Kind.COMPREHENSION) + || canPartiallyEvaluateComprehension(disableStandardEquality, node)); + } catch (CelEvaluationException | CelValidationException | RuntimeException e) { + if (!allConstant) { + return Optional.empty(); + } throw new CelOptimizationException( "Constant folding failure. Failed to evaluate subtree due to: " + e.getMessage(), e); } @@ -479,7 +642,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 +738,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(); } @@ -849,8 +1114,19 @@ private CelMutableAst pruneOptionalStructElements(CelMutableAst ast, CelMutableE } @CanIgnoreReturnValue - private static Object evaluateExpr(Cel cel, CelNavigableMutableExpr navigableMutableExpr) + private static Object evaluateExpr( + Cel cel, CelNavigableMutableExpr navigableMutableExpr, boolean useSentinels) throws CelValidationException, CelEvaluationException { + CelAbstractSyntaxTree ast = + CelAbstractSyntaxTree.newParsedAst( + CelMutableExprConverter.fromMutableExpr(navigableMutableExpr.expr()), + CelSource.newBuilder().build()); + ast = cel.check(ast).getAst(); + + if (useSentinels) { + return cel.createProgram(ast).eval(unused -> Optional.of(new UnknownSentinel())); + } + ImmutableList attributePatterns = navigableMutableExpr .allNodes() @@ -858,15 +1134,29 @@ private static Object evaluateExpr(Cel cel, CelNavigableMutableExpr navigableMut .map(node -> node.expr().ident().name()) .map(CelAttributePattern::fromQualifiedIdentifier) .collect(toImmutableList()); - CelAbstractSyntaxTree ast = - CelAbstractSyntaxTree.newParsedAst( - CelMutableExprConverter.fromMutableExpr(navigableMutableExpr.expr()), - CelSource.newBuilder().build()); - ast = cel.check(ast).getAst(); return cel.createProgram(ast).eval(PartialVars.of(attributePatterns)); } + private static final class UnknownElementAccessedException extends RuntimeException {} + + private static final class UnknownSentinel { + @Override + public boolean equals(Object obj) { + throw new UnknownElementAccessedException(); + } + + @Override + public int hashCode() { + throw new UnknownElementAccessedException(); + } + + @Override + public String toString() { + throw new UnknownElementAccessedException(); + } + } + /** Options to configure how Constant Folding behave. */ @AutoValue public abstract static class ConstantFoldingOptions { 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..71c5659e2 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,38 @@ 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: '[1, 2, 3, x].slice(0, 4)'}") + @TestParameters("{source: '[1, 2, 3, 1 / int_x].slice(0, 1)'}") + @TestParameters("{source: '[1, 2, 3, int_x + 1].slice(0, 1)'}") + @TestParameters("{source: '[1, 2, 3, ?optional_int_x].slice(0, 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'}") + @TestParameters("{source: '\"%s %s\".format([\"hello\", x])'}") + @TestParameters("{source: 'sets.contains([1, 2, x], [3])'}") + @TestParameters("{source: 'sets.contains([1, 2], [int_x])'}") + @TestParameters("{source: 'sets.contains([1, 2, int_x], [int_y])'}") + @TestParameters("{source: 'sets.intersects([1, 2], [int_x])'}") + @TestParameters("{source: 'sets.intersects([1, 2, int_x], [int_y])'}") + @TestParameters("{source: 'sets.equivalent([int_x], [int_y])'}") + @TestParameters("{source: 'sets.contains([optional.of(1)], [optional.of(int_x)])'}") + @TestParameters("{source: '[1] == [int_x]'}") + @TestParameters("{source: '{\"a\": 1} == {\"a\": int_x}'}") + @TestParameters("{source: '[1, 2, int_x].exists(i, optional.of(99) == [i].first())'}") + @TestParameters("{source: '{1: 10, 1u: 20, 2: int_x}.exists(k, k == 2)'}") + @TestParameters("{source: 'cel.bind(r, {1: 10, 1u: 20, 2: int_x}, r.size())'}") + @TestParameters("{source: 'cel.bind(r, int_x, 99 in {1: \"a\", 1u: \"b\", 2: r})'}") + @TestParameters("{source: '[int_list_x, [1, 2]].flatten().slice(1, 3)'}") + @TestParameters("{source: 'cel.bind(r, [int_list_x], r.flatten().size())'}") + @TestParameters("{source: '[2, 1, int_x].sort()'}") + @TestParameters("{source: '[1, 2, int_x].distinct()'}") + @TestParameters("{source: 'cel.bind(r, [1, x], type(r[1]) == int)'}") + @TestParameters("{source: 'optional.ofNonZeroValue(x)'}") public void constantFold_noOp(String source) throws Exception { CelAbstractSyntaxTree ast = cel.compile(source).getAst(); @@ -954,4 +994,172 @@ public void constantFold_inOperator_withoutMacros_skipsDoubleNan() throws Except assertThat(CEL_UNPARSER.unparse(optimizedAst)).isEqualTo("NaN in [NaN, double_x]"); } + + @Test + @TestParameters("{source: '[1, 2, 3, x].slice(0, 1)', expected: '[1]'}") + @TestParameters("{source: '[1, 2, 3, x].slice(0, 3)', expected: '[1, 2, 3]'}") + @TestParameters("{source: '[x, 2, 3].slice(1, 3)', expected: '[2, 3]'}") + @TestParameters("{source: '[x, 2, 3].slice(1, 1)', expected: '[]'}") + @TestParameters("{source: '[[1, 2], [3, x]].slice(0, 1).flatten(1)', expected: '[1, 2]'}") + @TestParameters("{source: '[1, x].first()', expected: 'optional.of(1)'}") + @TestParameters("{source: '[1, x].first().value()', expected: '1'}") + @TestParameters("{source: '[x, 2].last()', expected: 'optional.of(2)'}") + @TestParameters("{source: '[x, 2].last().orValue(0)', expected: '2'}") + @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]'}") + @TestParameters("{source: 'sets.contains([1, 2, x], [1, 2])', expected: 'true'}") + @TestParameters("{source: 'sets.contains([1, 2], [3, x])', expected: 'false'}") + @TestParameters("{source: 'sets.intersects([1, 2, x], [2, y])', expected: 'true'}") + @TestParameters("{source: '\"%s\".format([\"hello\", x])', expected: '\"hello\"'}") + @TestParameters("{source: '\"%d-%s\".format([42, \"ok\", x])', expected: '\"42-ok\"'}") + @TestParameters( + "{source: 'cel.bind(r, {\"a\": 1, \"b\": int_x}, r.?a.orValue(0))', expected: '1'}") + 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 + public void constantFold_partialEvaluation_disableCelStandardEquality_preservesEquality() + throws Exception { + Cel nonStandardEqCel = + setupEnv(runtimeFlavor.builder()) + .toCelBuilder() + .setOptions(CEL_OPTIONS.toBuilder().disableCelStandardEquality(true).build()) + .build(); + CelOptimizer optimizer = + CelOptimizerFactory.standardCelOptimizerBuilder(nonStandardEqCel) + .addAstOptimizers(ConstantFoldingOptimizer.getInstance()) + .build(); + CelAbstractSyntaxTree ast = nonStandardEqCel.compile("[1, int_x].all(i, 1 == i)").getAst(); + + CelAbstractSyntaxTree optimizedAst = optimizer.optimize(ast); + + assertThat(CEL_UNPARSER.unparse(optimizedAst)).isEqualTo("[1, int_x].all(i, 1 == i)"); + } + + @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: '[1, 2, 3, int_x].size()', expected: '4'}") + @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: '\"a\" in {\"a\": 1, \"b\": int_x}', expected: 'true'}") + @TestParameters("{source: '\"b\" in {\"a\": 1, \"b\": int_x}', expected: 'true'}") + @TestParameters("{source: '\"c\" in {\"a\": 1, \"b\": int_x}', expected: 'false'}") + @TestParameters("{source: '{\"a\": 1, \"b\": int_x}.size()', expected: '2'}") + @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'}") + @TestParameters("{source: '[1, 2, int_x].exists(i, i == 2)', expected: 'true'}") + @TestParameters("{source: '[1, 2, int_x].all(i, i > 1)', expected: 'false'}") + @TestParameters("{source: '{\"a\": 1, \"b\": int_x}.exists(k, k == \"b\")', expected: 'true'}") + @TestParameters("{source: '{\"a\": 1, \"b\": int_x}.all(k, k != \"b\")', expected: 'false'}") + @TestParameters("{source: 'cel.bind(r, [1, 2, int_x], r[0] + r[1])', expected: '3'}") + @TestParameters("{source: 'cel.bind(r, {\"a\": 10, \"b\": int_x}, r.a)', expected: '10'}") + 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'}") + @TestParameters("{source: '[int_x] == [int_y]', unsoundFold: 'true'}") + @TestParameters("{source: '[double_x] == [double_x]', unsoundFold: 'true'}") + @TestParameters("{source: '{\"a\": int_x} == {\"a\": int_y}', unsoundFold: 'true'}") + @TestParameters("{source: 'optional.of(1) == optional.of(int_x)', unsoundFold: 'false'}") + @TestParameters("{source: 'optional.of(int_x) == optional.of(int_y)', unsoundFold: 'true'}") + @TestParameters( + "{source: '[1, 2, int_x].exists(i, optional.of(99) == optional.of(i))'," + + " unsoundFold: 'false'}") + @TestParameters("{source: '[1, 2, int_x].exists(i, i in {99: \"a\"})', unsoundFold: 'false'}") + @TestParameters("{source: 'cel.bind(r, [int_x, int_y], r[0] == r[1])', unsoundFold: 'true'}") + @TestParameters("{source: 'cel.bind(r, [double_x], r[0] == r[0])', unsoundFold: 'true'}") + @TestParameters("{source: 'cel.bind(r, [1, int_x], r[1] in {99: \"a\"})', unsoundFold: 'false'}") + @TestParameters( + "{source: 'cel.bind(r, int_x, optional.of({99: 10})[r])', unsoundFold: 'optional.none()'}") + @TestParameters( + "{source: '[2, 3, int_x].exists(i, optional.of({1: 99})[i].hasValue())'," + + " unsoundFold: 'false'}") + 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); + } }