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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line numberDiff line numberDiff line change
Expand Up@@ -21,6 +21,7 @@

import com.google.auto.value.AutoValue;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.ImmutableSet;
import com.google.errorprone.annotations.CanIgnoreReturnValue;
import dev.cel.bundle.Cel;
Expand DownExpand Up@@ -62,9 +63,12 @@
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Optional;
import org.jspecify.annotations.Nullable;

Expand All@@ -73,6 +77,19 @@
* calls and select statements with their evaluated result.
*/
public final class ConstantFoldingOptimizer implements CelAstOptimizer {
private static final ImmutableSet<String> BOOLEAN_RETURN_OPERATORS =
ImmutableSet.of(
Operator.LOGICAL_AND.getFunction(),
Operator.LOGICAL_OR.getFunction(),
Operator.LOGICAL_NOT.getFunction(),
Operator.EQUALS.getFunction(),
Operator.NOT_EQUALS.getFunction(),
Operator.LESS.getFunction(),
Operator.LESS_EQUALS.getFunction(),
Operator.GREATER.getFunction(),
Operator.GREATER_EQUALS.getFunction(),
Operator.IN.getFunction());

private static final ConstantFoldingOptimizer INSTANCE =
new ConstantFoldingOptimizer(ConstantFoldingOptions.newBuilder().build());

Expand DownExpand Up@@ -115,6 +132,25 @@ public OptimizationResult optimize(CelAbstractSyntaxTree ast, Cel cel)
Cel optimizerEnv = builder.setResultType(SimpleType.DYN).build();

CelMutableAst mutableAst = CelMutableAst.fromCelAst(ast);

// HACK: The AstMutator strips type metadata during intermediate folds due to ID renumbering.
// We pre-compute identifier types from the unmutated AST to safely evaluate boolean conditions
// later.
// TODO: Improve AstMutator to retain type metadata when possible.
Map<String, CelType> mutableIdentTypes = new HashMap<>();
Iterator<CelNavigableMutableExpr> identNodes =
CelNavigableMutableAst.fromAst(mutableAst)
.getRoot()
.allNodes()
.filter(node -> node.getKind().equals(Kind.IDENT))
.iterator();
while (identNodes.hasNext()) {
CelNavigableMutableExpr node = identNodes.next();
Optional<CelType> type = mutableAst.getType(node.id());
type.ifPresent(celType -> mutableIdentTypes.put(node.expr().ident().name(), celType));
}
ImmutableMap<String, CelType> identTypes = ImmutableMap.copyOf(mutableIdentTypes);

int iterCount = 0;
boolean continueFolding = true;
while (continueFolding) {
Expand All@@ -123,7 +159,6 @@ public OptimizationResult optimize(CelAbstractSyntaxTree ast, Cel cel)
}
iterCount++;
continueFolding = false;

ImmutableList<CelNavigableMutableExpr> foldableExprs =
CelNavigableMutableAst.fromAst(mutableAst)
.getRoot()
Expand All@@ -135,7 +170,7 @@ public OptimizationResult optimize(CelAbstractSyntaxTree ast, Cel cel)

Optional<CelMutableAst> mutatedResult;
// Attempt to prune if it is a non-strict call
mutatedResult = maybePruneBranches(mutableAst, foldableExpr.expr());
mutatedResult = maybePruneBranches(mutableAst, identTypes, foldableExpr.expr());
if (!mutatedResult.isPresent()) {
// Evaluate the call then fold
try {
Expand DownExpand Up@@ -210,6 +245,9 @@ private boolean canFold(CelNavigableMutableExpr navigableExpr) {

if (functionName.equals(Operator.EQUALS.getFunction())
|| functionName.equals(Operator.NOT_EQUALS.getFunction())) {
if (hasComprehensionVar(navigableExpr)) {
return false;
}
if (mutableCall.args().stream()
.anyMatch(node -> isExprConstantOfKind(node, CelConstant.Kind.BOOLEAN_VALUE))
|| mutableCall.args().stream()
Expand All@@ -219,7 +257,7 @@ private boolean canFold(CelNavigableMutableExpr navigableExpr) {
}

if (functionName.equals(Operator.IN.getFunction())) {
return canFoldInOperator(navigableExpr);
return !hasComprehensionVar(navigableExpr);
}

// Default case: all call arguments must be constants. If the argument is a container (ex:
Expand DownExpand Up@@ -248,32 +286,31 @@ private static boolean isCallTimestampOrDuration(CelMutableCall call) {
|| call.function().equals(DURATION.functionName());
}

private static boolean canFoldInOperator(CelNavigableMutableExpr navigableExpr) {
ImmutableList<CelNavigableMutableExpr> allIdents =
navigableExpr
.allNodes()
.filter(node -> node.getKind().equals(Kind.IDENT))
.collect(toImmutableList());
for (CelNavigableMutableExpr identNode : allIdents) {
CelNavigableMutableExpr parent = identNode.parent().orElse(null);
while (parent != null) {
if (parent.getKind().equals(Kind.COMPREHENSION)) {
String identName = identNode.expr().ident().name();
CelMutableComprehension parentComprehension = parent.expr().comprehension();
if (parentComprehension.accuVar().equals(identName)
|| parentComprehension.iterVar().equals(identName)
|| parentComprehension.iterVar2().equals(identName)) {
// Prevent folding a subexpression if it contains a variable declared by a
// comprehension. The subexpression cannot be compiled without the full context of the
// surrounding comprehension.
return false;
}
}
parent = parent.parent().orElse(null);
}
}

return true;
private static boolean hasComprehensionVar(CelNavigableMutableExpr expr) {
return expr.allNodes()
.filter(node -> node.getKind().equals(Kind.IDENT))
.anyMatch(
identNode -> {
String identName = identNode.expr().ident().name();
CelNavigableMutableExpr curr = identNode;
Optional<CelNavigableMutableExpr> maybeParent = curr.parent();
while (maybeParent.isPresent()) {
CelNavigableMutableExpr parent = maybeParent.get();
if (parent.getKind().equals(Kind.COMPREHENSION)) {
CelMutableComprehension compre = parent.expr().comprehension();
if ((compre.accuVar().equals(identName)
|| compre.iterVar().equals(identName)
|| compre.iterVar2().equals(identName))
&& curr.id() != compre.iterRange().id()
&& curr.id() != compre.accuInit().id()) {
return true;
}
}
curr = parent;
maybeParent = parent.parent();
}
return false;
});
}

private static boolean areChildrenArgConstant(CelNavigableMutableExpr expr) {
Expand DownExpand Up@@ -311,6 +348,9 @@ private Optional<CelMutableAst> maybeFold(
CelMutableAst mutableAst,
CelNavigableMutableExpr node)
throws CelOptimizationException, CelEvaluationException {
if (!node.getKind().equals(Kind.COMPREHENSION) && hasComprehensionVar(node)) {
return Optional.empty();
}
Object result;
try {
result = evaluateExpr(cel, node);
Expand DownExpand Up@@ -465,7 +505,7 @@ private static boolean isCallToFunction(CelMutableExpr expr, String functionName

/** Inspects the non-strict calls to determine whether a branch can be removed. */
private Optional<CelMutableAst> maybePruneBranches(
CelMutableAst mutableAst, CelMutableExpr expr) {
CelMutableAst mutableAst, Map<String, CelType> identTypes, CelMutableExpr expr) {
if (!expr.getKind().equals(Kind.CALL)) {
return Optional.empty();
}
Expand All@@ -474,7 +514,7 @@ private Optional<CelMutableAst> maybePruneBranches(
String function = call.function();
if (function.equals(Operator.LOGICAL_AND.getFunction())
|| function.equals(Operator.LOGICAL_OR.getFunction())) {
return maybeShortCircuitCall(mutableAst, expr);
return maybeShortCircuitCall(mutableAst, identTypes, expr);
} else if (function.equals(Operator.CONDITIONAL.getFunction())) {
CelMutableExpr cond = call.args().get(0);
CelMutableExpr truthy = call.args().get(1);
Expand DownExpand Up@@ -518,24 +558,28 @@ private Optional<CelMutableAst> maybePruneBranches(
|| function.equals(Operator.NOT_EQUALS.getFunction())) {
CelMutableExpr lhs = call.args().get(0);
CelMutableExpr rhs = call.args().get(1);
boolean lhsIsBoolean = isExprConstantOfKind(lhs, CelConstant.Kind.BOOLEAN_VALUE);
boolean rhsIsBoolean = isExprConstantOfKind(rhs, CelConstant.Kind.BOOLEAN_VALUE);
boolean lhsIsBooleanConstant = isExprConstantOfKind(lhs, CelConstant.Kind.BOOLEAN_VALUE);
boolean rhsIsBooleanConstant = isExprConstantOfKind(rhs, CelConstant.Kind.BOOLEAN_VALUE);
boolean invertCondition = function.equals(Operator.NOT_EQUALS.getFunction());
Optional<CelMutableExpr> replacementExpr = Optional.empty();

if (lhs.getKind().equals(Kind.CONSTANT) && rhs.getKind().equals(Kind.CONSTANT)) {
// If both args are const, don't prune any branches and let maybeFold method evaluate this
// subExpr
return Optional.empty();
} else if (lhsIsBoolean) {
} else if (lhsIsBooleanConstant
&& (!constantFoldingOptions.enableSafeLogicalOptimization()
|| evaluatesToBoolean(mutableAst, identTypes, rhs))) {
boolean cond = invertCondition != lhs.constant().booleanValue();
replacementExpr =
Optional.of(
cond
? rhs
: CelMutableExpr.ofCall(
CelMutableCall.create(Operator.LOGICAL_NOT.getFunction(), rhs)));
} else if (rhsIsBoolean) {
} else if (rhsIsBooleanConstant
&& (!constantFoldingOptions.enableSafeLogicalOptimization()
|| evaluatesToBoolean(mutableAst, identTypes, lhs))) {
boolean cond = invertCondition != rhs.constant().booleanValue();
replacementExpr =
Optional.of(
Expand All@@ -552,7 +596,7 @@ private Optional<CelMutableAst> maybePruneBranches(
}

private Optional<CelMutableAst> maybeShortCircuitCall(
CelMutableAst mutableAst, CelMutableExpr expr) {
CelMutableAst mutableAst, Map<String, CelType> identTypes, CelMutableExpr expr) {
CelMutableCall call = expr.call();
boolean shortCircuit = false;
boolean skip = true;
Expand DownExpand Up@@ -583,14 +627,39 @@ private Optional<CelMutableAst> maybeShortCircuitCall(
return Optional.of(astMutator.replaceSubtree(mutableAst, shortCircuitTarget, expr.id()));
}
if (newArgs.size() == 1) {
return Optional.of(astMutator.replaceSubtree(mutableAst, newArgs.get(0), expr.id()));
CelMutableExpr remainingArg = newArgs.get(0);
if (!constantFoldingOptions.enableSafeLogicalOptimization()
|| evaluatesToBoolean(mutableAst, identTypes, remainingArg)) {
return Optional.of(astMutator.replaceSubtree(mutableAst, remainingArg, expr.id()));
}
return Optional.empty();
}

// TODO: Support folding variadic AND/ORs.
throw new UnsupportedOperationException(
"Folding variadic logical operator is not supported yet.");
}

private boolean evaluatesToBoolean(
CelMutableAst mutableAst, Map<String, CelType> identTypes, CelMutableExpr expr) {
if (isExprConstantOfKind(expr, CelConstant.Kind.BOOLEAN_VALUE)) {
return true;
}
// The AST's type map relies on the type-checker having explicitly populated the type for a
// given node. However, during the optimization pipeline, mutated intermediate nodes might
// temporarily lack type metadata. Standard CEL operators like &&, ||, and == inherently
// always return a boolean, so checking the function name provides a reliable fallback when
// the type map is incomplete.
if (expr.getKind().equals(Kind.CALL)
&& BOOLEAN_RETURN_OPERATORS.contains(expr.call().function())) {
return true;
}
if (expr.getKind().equals(Kind.IDENT)) {
return Objects.equals(identTypes.get(expr.ident().name()), SimpleType.BOOL);
}
return mutableAst.getType(expr.id()).map(SimpleType.BOOL::equals).orElse(false);
}

private boolean isFoldedAggregateLiteral(CelMutableExpr expr) {
if (expr.getKind().equals(Kind.CONSTANT)) {
return true;
Expand DownExpand Up@@ -811,6 +880,13 @@ public abstract static class ConstantFoldingOptions {

public abstract ImmutableSet<String> foldableFunctions();

/**
* Returns true if safe logical optimization is enabled. When enabled, logical (&&, ||) and
* equality (==, !=) expression optimizations strictly verify that pruned sub-expressions
* evaluate to booleans.
*/
public abstract boolean enableSafeLogicalOptimization();

/** Builder for configuring the {@link ConstantFoldingOptions}. */
@AutoValue.Builder
public abstract static class Builder {
Expand All@@ -823,6 +899,17 @@ public abstract static class Builder {
*/
public abstract Builder maxIterationLimit(int value);

/**
* Enables or disables safe logical optimization. When enabled (default: {@code true}),
* constant folding on logical (&&, ||) and equality (==, !=) operators strictly checks
* whether sub-expressions evaluate to booleans before pruning them. Disabling this flag
* restores legacy aggressive folding behavior.
*
* <p>Note: Disabling this flag should only be done temporarily for migration purposes, with
* the goal of eventually enabling it for safety.
*/
public abstract Builder enableSafeLogicalOptimization(boolean value);

/**
* Adds a collection of custom functions that will be a candidate for constant folding. By
* default, standard functions are foldable.
Expand DownExpand Up@@ -850,7 +937,8 @@ public Builder addFoldableFunctions(String... functions) {
/** Returns a new options builder with recommended defaults pre-configured. */
public static Builder newBuilder() {
return new AutoValue_ConstantFoldingOptimizer_ConstantFoldingOptions.Builder()
.maxIterationLimit(400);
.maxIterationLimit(400)
.enableSafeLogicalOptimization(true);
}

ConstantFoldingOptions() {}
Expand Down
Loading
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Add copy buttons to all
 blocks\n(function() {\n function addCopyButtons() {\n document.querySelectorAll('pre code').forEach(function(codeBlock) {\n if (codeBlock.parentElement.hasAttribute('data-copy-added')) return;\n codeBlock.parentElement.setAttribute('data-copy-added', 'true');\n \n var btn = document.createElement('button');\n btn.textContent = 'Copy';\n btn.style.cssText = 'position:absolute;top:4px;right:4px;padding:2px 8px;font-size:11px;background:#4ecdc4;border:none;border-radius:4px;color:#1a1a2e;cursor:pointer;opacity:0.7;transition:opacity 0.2s;';\n btn.onmouseover = function() { this.style.opacity = '1'; };\n btn.onmouseout = function() { this.style.opacity = '0.7'; };\n btn.onclick = function() {\n navigator.clipboard.writeText(codeBlock.textContent).then(function() {\n btn.textContent = 'Copied!';\n setTimeout(function() { btn.textContent = 'Copy'; }, 1500);\n });\n };\n codeBlock.parentElement.style.position = 'relative';\n codeBlock.parentElement.appendChild(btn);\n });\n }\n \n addCopyButtons();\n \n // Re-run on dynamic content\n var observer = new MutationObserver(addCopyButtons);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Add Copy Buttons to Code Blocks");
}
} catch(__e) { console.warn('[Userscript:Add Copy Buttons to Code Blocks]', __e); }
})();
(function(){
try {
var __m = "github.com";
var __re = new RegExp('^' + "github\\.com" + '
Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line numberDiff line numberDiff line change
Expand Up@@ -21,6 +21,7 @@

import com.google.auto.value.AutoValue;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.ImmutableSet;
import com.google.errorprone.annotations.CanIgnoreReturnValue;
import dev.cel.bundle.Cel;
Expand DownExpand Up@@ -62,9 +63,12 @@
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Optional;
import org.jspecify.annotations.Nullable;

Expand All@@ -73,6 +77,19 @@
* calls and select statements with their evaluated result.
*/
public final class ConstantFoldingOptimizer implements CelAstOptimizer {
private static final ImmutableSet<String> BOOLEAN_RETURN_OPERATORS =
ImmutableSet.of(
Operator.LOGICAL_AND.getFunction(),
Operator.LOGICAL_OR.getFunction(),
Operator.LOGICAL_NOT.getFunction(),
Operator.EQUALS.getFunction(),
Operator.NOT_EQUALS.getFunction(),
Operator.LESS.getFunction(),
Operator.LESS_EQUALS.getFunction(),
Operator.GREATER.getFunction(),
Operator.GREATER_EQUALS.getFunction(),
Operator.IN.getFunction());

private static final ConstantFoldingOptimizer INSTANCE =
new ConstantFoldingOptimizer(ConstantFoldingOptions.newBuilder().build());

Expand DownExpand Up@@ -115,6 +132,25 @@ public OptimizationResult optimize(CelAbstractSyntaxTree ast, Cel cel)
Cel optimizerEnv = builder.setResultType(SimpleType.DYN).build();

CelMutableAst mutableAst = CelMutableAst.fromCelAst(ast);

// HACK: The AstMutator strips type metadata during intermediate folds due to ID renumbering.
// We pre-compute identifier types from the unmutated AST to safely evaluate boolean conditions
// later.
// TODO: Improve AstMutator to retain type metadata when possible.
Map<String, CelType> mutableIdentTypes = new HashMap<>();
Iterator<CelNavigableMutableExpr> identNodes =
CelNavigableMutableAst.fromAst(mutableAst)
.getRoot()
.allNodes()
.filter(node -> node.getKind().equals(Kind.IDENT))
.iterator();
while (identNodes.hasNext()) {
CelNavigableMutableExpr node = identNodes.next();
Optional<CelType> type = mutableAst.getType(node.id());
type.ifPresent(celType -> mutableIdentTypes.put(node.expr().ident().name(), celType));
}
ImmutableMap<String, CelType> identTypes = ImmutableMap.copyOf(mutableIdentTypes);

int iterCount = 0;
boolean continueFolding = true;
while (continueFolding) {
Expand All@@ -123,7 +159,6 @@ public OptimizationResult optimize(CelAbstractSyntaxTree ast, Cel cel)
}
iterCount++;
continueFolding = false;

ImmutableList<CelNavigableMutableExpr> foldableExprs =
CelNavigableMutableAst.fromAst(mutableAst)
.getRoot()
Expand All@@ -135,7 +170,7 @@ public OptimizationResult optimize(CelAbstractSyntaxTree ast, Cel cel)

Optional<CelMutableAst> mutatedResult;
// Attempt to prune if it is a non-strict call
mutatedResult = maybePruneBranches(mutableAst, foldableExpr.expr());
mutatedResult = maybePruneBranches(mutableAst, identTypes, foldableExpr.expr());
if (!mutatedResult.isPresent()) {
// Evaluate the call then fold
try {
Expand DownExpand Up@@ -210,6 +245,9 @@ private boolean canFold(CelNavigableMutableExpr navigableExpr) {

if (functionName.equals(Operator.EQUALS.getFunction())
|| functionName.equals(Operator.NOT_EQUALS.getFunction())) {
if (hasComprehensionVar(navigableExpr)) {
return false;
}
if (mutableCall.args().stream()
.anyMatch(node -> isExprConstantOfKind(node, CelConstant.Kind.BOOLEAN_VALUE))
|| mutableCall.args().stream()
Expand All@@ -219,7 +257,7 @@ private boolean canFold(CelNavigableMutableExpr navigableExpr) {
}

if (functionName.equals(Operator.IN.getFunction())) {
return canFoldInOperator(navigableExpr);
return !hasComprehensionVar(navigableExpr);
}

// Default case: all call arguments must be constants. If the argument is a container (ex:
Expand DownExpand Up@@ -248,32 +286,31 @@ private static boolean isCallTimestampOrDuration(CelMutableCall call) {
|| call.function().equals(DURATION.functionName());
}

private static boolean canFoldInOperator(CelNavigableMutableExpr navigableExpr) {
ImmutableList<CelNavigableMutableExpr> allIdents =
navigableExpr
.allNodes()
.filter(node -> node.getKind().equals(Kind.IDENT))
.collect(toImmutableList());
for (CelNavigableMutableExpr identNode : allIdents) {
CelNavigableMutableExpr parent = identNode.parent().orElse(null);
while (parent != null) {
if (parent.getKind().equals(Kind.COMPREHENSION)) {
String identName = identNode.expr().ident().name();
CelMutableComprehension parentComprehension = parent.expr().comprehension();
if (parentComprehension.accuVar().equals(identName)
|| parentComprehension.iterVar().equals(identName)
|| parentComprehension.iterVar2().equals(identName)) {
// Prevent folding a subexpression if it contains a variable declared by a
// comprehension. The subexpression cannot be compiled without the full context of the
// surrounding comprehension.
return false;
}
}
parent = parent.parent().orElse(null);
}
}

return true;
private static boolean hasComprehensionVar(CelNavigableMutableExpr expr) {
return expr.allNodes()
.filter(node -> node.getKind().equals(Kind.IDENT))
.anyMatch(
identNode -> {
String identName = identNode.expr().ident().name();
CelNavigableMutableExpr curr = identNode;
Optional<CelNavigableMutableExpr> maybeParent = curr.parent();
while (maybeParent.isPresent()) {
CelNavigableMutableExpr parent = maybeParent.get();
if (parent.getKind().equals(Kind.COMPREHENSION)) {
CelMutableComprehension compre = parent.expr().comprehension();
if ((compre.accuVar().equals(identName)
|| compre.iterVar().equals(identName)
|| compre.iterVar2().equals(identName))
&& curr.id() != compre.iterRange().id()
&& curr.id() != compre.accuInit().id()) {
return true;
}
}
curr = parent;
maybeParent = parent.parent();
}
return false;
});
}

private static boolean areChildrenArgConstant(CelNavigableMutableExpr expr) {
Expand DownExpand Up@@ -311,6 +348,9 @@ private Optional<CelMutableAst> maybeFold(
CelMutableAst mutableAst,
CelNavigableMutableExpr node)
throws CelOptimizationException, CelEvaluationException {
if (!node.getKind().equals(Kind.COMPREHENSION) && hasComprehensionVar(node)) {
return Optional.empty();
}
Object result;
try {
result = evaluateExpr(cel, node);
Expand DownExpand Up@@ -465,7 +505,7 @@ private static boolean isCallToFunction(CelMutableExpr expr, String functionName

/** Inspects the non-strict calls to determine whether a branch can be removed. */
private Optional<CelMutableAst> maybePruneBranches(
CelMutableAst mutableAst, CelMutableExpr expr) {
CelMutableAst mutableAst, Map<String, CelType> identTypes, CelMutableExpr expr) {
if (!expr.getKind().equals(Kind.CALL)) {
return Optional.empty();
}
Expand All@@ -474,7 +514,7 @@ private Optional<CelMutableAst> maybePruneBranches(
String function = call.function();
if (function.equals(Operator.LOGICAL_AND.getFunction())
|| function.equals(Operator.LOGICAL_OR.getFunction())) {
return maybeShortCircuitCall(mutableAst, expr);
return maybeShortCircuitCall(mutableAst, identTypes, expr);
} else if (function.equals(Operator.CONDITIONAL.getFunction())) {
CelMutableExpr cond = call.args().get(0);
CelMutableExpr truthy = call.args().get(1);
Expand DownExpand Up@@ -518,24 +558,28 @@ private Optional<CelMutableAst> maybePruneBranches(
|| function.equals(Operator.NOT_EQUALS.getFunction())) {
CelMutableExpr lhs = call.args().get(0);
CelMutableExpr rhs = call.args().get(1);
boolean lhsIsBoolean = isExprConstantOfKind(lhs, CelConstant.Kind.BOOLEAN_VALUE);
boolean rhsIsBoolean = isExprConstantOfKind(rhs, CelConstant.Kind.BOOLEAN_VALUE);
boolean lhsIsBooleanConstant = isExprConstantOfKind(lhs, CelConstant.Kind.BOOLEAN_VALUE);
boolean rhsIsBooleanConstant = isExprConstantOfKind(rhs, CelConstant.Kind.BOOLEAN_VALUE);
boolean invertCondition = function.equals(Operator.NOT_EQUALS.getFunction());
Optional<CelMutableExpr> replacementExpr = Optional.empty();

if (lhs.getKind().equals(Kind.CONSTANT) && rhs.getKind().equals(Kind.CONSTANT)) {
// If both args are const, don't prune any branches and let maybeFold method evaluate this
// subExpr
return Optional.empty();
} else if (lhsIsBoolean) {
} else if (lhsIsBooleanConstant
&& (!constantFoldingOptions.enableSafeLogicalOptimization()
|| evaluatesToBoolean(mutableAst, identTypes, rhs))) {
boolean cond = invertCondition != lhs.constant().booleanValue();
replacementExpr =
Optional.of(
cond
? rhs
: CelMutableExpr.ofCall(
CelMutableCall.create(Operator.LOGICAL_NOT.getFunction(), rhs)));
} else if (rhsIsBoolean) {
} else if (rhsIsBooleanConstant
&& (!constantFoldingOptions.enableSafeLogicalOptimization()
|| evaluatesToBoolean(mutableAst, identTypes, lhs))) {
boolean cond = invertCondition != rhs.constant().booleanValue();
replacementExpr =
Optional.of(
Expand All@@ -552,7 +596,7 @@ private Optional<CelMutableAst> maybePruneBranches(
}

private Optional<CelMutableAst> maybeShortCircuitCall(
CelMutableAst mutableAst, CelMutableExpr expr) {
CelMutableAst mutableAst, Map<String, CelType> identTypes, CelMutableExpr expr) {
CelMutableCall call = expr.call();
boolean shortCircuit = false;
boolean skip = true;
Expand DownExpand Up@@ -583,14 +627,39 @@ private Optional<CelMutableAst> maybeShortCircuitCall(
return Optional.of(astMutator.replaceSubtree(mutableAst, shortCircuitTarget, expr.id()));
}
if (newArgs.size() == 1) {
return Optional.of(astMutator.replaceSubtree(mutableAst, newArgs.get(0), expr.id()));
CelMutableExpr remainingArg = newArgs.get(0);
if (!constantFoldingOptions.enableSafeLogicalOptimization()
|| evaluatesToBoolean(mutableAst, identTypes, remainingArg)) {
return Optional.of(astMutator.replaceSubtree(mutableAst, remainingArg, expr.id()));
}
return Optional.empty();
}

// TODO: Support folding variadic AND/ORs.
throw new UnsupportedOperationException(
"Folding variadic logical operator is not supported yet.");
}

private boolean evaluatesToBoolean(
CelMutableAst mutableAst, Map<String, CelType> identTypes, CelMutableExpr expr) {
if (isExprConstantOfKind(expr, CelConstant.Kind.BOOLEAN_VALUE)) {
return true;
}
// The AST's type map relies on the type-checker having explicitly populated the type for a
// given node. However, during the optimization pipeline, mutated intermediate nodes might
// temporarily lack type metadata. Standard CEL operators like &&, ||, and == inherently
// always return a boolean, so checking the function name provides a reliable fallback when
// the type map is incomplete.
if (expr.getKind().equals(Kind.CALL)
&& BOOLEAN_RETURN_OPERATORS.contains(expr.call().function())) {
return true;
}
if (expr.getKind().equals(Kind.IDENT)) {
return Objects.equals(identTypes.get(expr.ident().name()), SimpleType.BOOL);
}
return mutableAst.getType(expr.id()).map(SimpleType.BOOL::equals).orElse(false);
}

private boolean isFoldedAggregateLiteral(CelMutableExpr expr) {
if (expr.getKind().equals(Kind.CONSTANT)) {
return true;
Expand DownExpand Up@@ -811,6 +880,13 @@ public abstract static class ConstantFoldingOptions {

public abstract ImmutableSet<String> foldableFunctions();

/**
* Returns true if safe logical optimization is enabled. When enabled, logical (&&, ||) and
* equality (==, !=) expression optimizations strictly verify that pruned sub-expressions
* evaluate to booleans.
*/
public abstract boolean enableSafeLogicalOptimization();

/** Builder for configuring the {@link ConstantFoldingOptions}. */
@AutoValue.Builder
public abstract static class Builder {
Expand All@@ -823,6 +899,17 @@ public abstract static class Builder {
*/
public abstract Builder maxIterationLimit(int value);

/**
* Enables or disables safe logical optimization. When enabled (default: {@code true}),
* constant folding on logical (&&, ||) and equality (==, !=) operators strictly checks
* whether sub-expressions evaluate to booleans before pruning them. Disabling this flag
* restores legacy aggressive folding behavior.
*
* <p>Note: Disabling this flag should only be done temporarily for migration purposes, with
* the goal of eventually enabling it for safety.
*/
public abstract Builder enableSafeLogicalOptimization(boolean value);

/**
* Adds a collection of custom functions that will be a candidate for constant folding. By
* default, standard functions are foldable.
Expand DownExpand Up@@ -850,7 +937,8 @@ public Builder addFoldableFunctions(String... functions) {
/** Returns a new options builder with recommended defaults pre-configured. */
public static Builder newBuilder() {
return new AutoValue_ConstantFoldingOptimizer_ConstantFoldingOptions.Builder()
.maxIterationLimit(400);
.maxIterationLimit(400)
.enableSafeLogicalOptimization(true);
}

ConstantFoldingOptions() {}
Expand Down
Loading
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Force GitHub README to respect dark mode\n(function() {\n var style = document.createElement('style');\n style.textContent = '\n .markdown-body {\n color-scheme: dark light;\n }\n .markdown-body pre { background: #161b22 !important; }\n .markdown-body code { background: rgba(110, 118, 129, 0.4) !important; }\n .markdown-body table th, .markdown-body table td { border-color: #30363d !important; }\n .markdown-body img { background: #0d1117; }\n .markdown-body blockquote { border-left-color: #8b949e; }\n .markdown-body hr { border-color: #30363d; }\n ';\n document.head.appendChild(style);\n})();", "GitHub Dark Mode README Fix"); } } catch(__e) { console.warn('[Userscript:GitHub Dark Mode README Fix]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line numberDiff line numberDiff line change
Expand Up@@ -21,6 +21,7 @@

import com.google.auto.value.AutoValue;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.ImmutableSet;
import com.google.errorprone.annotations.CanIgnoreReturnValue;
import dev.cel.bundle.Cel;
Expand DownExpand Up@@ -62,9 +63,12 @@
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Optional;
import org.jspecify.annotations.Nullable;

Expand All@@ -73,6 +77,19 @@
* calls and select statements with their evaluated result.
*/
public final class ConstantFoldingOptimizer implements CelAstOptimizer {
private static final ImmutableSet<String> BOOLEAN_RETURN_OPERATORS =
ImmutableSet.of(
Operator.LOGICAL_AND.getFunction(),
Operator.LOGICAL_OR.getFunction(),
Operator.LOGICAL_NOT.getFunction(),
Operator.EQUALS.getFunction(),
Operator.NOT_EQUALS.getFunction(),
Operator.LESS.getFunction(),
Operator.LESS_EQUALS.getFunction(),
Operator.GREATER.getFunction(),
Operator.GREATER_EQUALS.getFunction(),
Operator.IN.getFunction());

private static final ConstantFoldingOptimizer INSTANCE =
new ConstantFoldingOptimizer(ConstantFoldingOptions.newBuilder().build());

Expand DownExpand Up@@ -115,6 +132,25 @@ public OptimizationResult optimize(CelAbstractSyntaxTree ast, Cel cel)
Cel optimizerEnv = builder.setResultType(SimpleType.DYN).build();

CelMutableAst mutableAst = CelMutableAst.fromCelAst(ast);

// HACK: The AstMutator strips type metadata during intermediate folds due to ID renumbering.
// We pre-compute identifier types from the unmutated AST to safely evaluate boolean conditions
// later.
// TODO: Improve AstMutator to retain type metadata when possible.
Map<String, CelType> mutableIdentTypes = new HashMap<>();
Iterator<CelNavigableMutableExpr> identNodes =
CelNavigableMutableAst.fromAst(mutableAst)
.getRoot()
.allNodes()
.filter(node -> node.getKind().equals(Kind.IDENT))
.iterator();
while (identNodes.hasNext()) {
CelNavigableMutableExpr node = identNodes.next();
Optional<CelType> type = mutableAst.getType(node.id());
type.ifPresent(celType -> mutableIdentTypes.put(node.expr().ident().name(), celType));
}
ImmutableMap<String, CelType> identTypes = ImmutableMap.copyOf(mutableIdentTypes);

int iterCount = 0;
boolean continueFolding = true;
while (continueFolding) {
Expand All@@ -123,7 +159,6 @@ public OptimizationResult optimize(CelAbstractSyntaxTree ast, Cel cel)
}
iterCount++;
continueFolding = false;

ImmutableList<CelNavigableMutableExpr> foldableExprs =
CelNavigableMutableAst.fromAst(mutableAst)
.getRoot()
Expand All@@ -135,7 +170,7 @@ public OptimizationResult optimize(CelAbstractSyntaxTree ast, Cel cel)

Optional<CelMutableAst> mutatedResult;
// Attempt to prune if it is a non-strict call
mutatedResult = maybePruneBranches(mutableAst, foldableExpr.expr());
mutatedResult = maybePruneBranches(mutableAst, identTypes, foldableExpr.expr());
if (!mutatedResult.isPresent()) {
// Evaluate the call then fold
try {
Expand DownExpand Up@@ -210,6 +245,9 @@ private boolean canFold(CelNavigableMutableExpr navigableExpr) {

if (functionName.equals(Operator.EQUALS.getFunction())
|| functionName.equals(Operator.NOT_EQUALS.getFunction())) {
if (hasComprehensionVar(navigableExpr)) {
return false;
}
if (mutableCall.args().stream()
.anyMatch(node -> isExprConstantOfKind(node, CelConstant.Kind.BOOLEAN_VALUE))
|| mutableCall.args().stream()
Expand All@@ -219,7 +257,7 @@ private boolean canFold(CelNavigableMutableExpr navigableExpr) {
}

if (functionName.equals(Operator.IN.getFunction())) {
return canFoldInOperator(navigableExpr);
return !hasComprehensionVar(navigableExpr);
}

// Default case: all call arguments must be constants. If the argument is a container (ex:
Expand DownExpand Up@@ -248,32 +286,31 @@ private static boolean isCallTimestampOrDuration(CelMutableCall call) {
|| call.function().equals(DURATION.functionName());
}

private static boolean canFoldInOperator(CelNavigableMutableExpr navigableExpr) {
ImmutableList<CelNavigableMutableExpr> allIdents =
navigableExpr
.allNodes()
.filter(node -> node.getKind().equals(Kind.IDENT))
.collect(toImmutableList());
for (CelNavigableMutableExpr identNode : allIdents) {
CelNavigableMutableExpr parent = identNode.parent().orElse(null);
while (parent != null) {
if (parent.getKind().equals(Kind.COMPREHENSION)) {
String identName = identNode.expr().ident().name();
CelMutableComprehension parentComprehension = parent.expr().comprehension();
if (parentComprehension.accuVar().equals(identName)
|| parentComprehension.iterVar().equals(identName)
|| parentComprehension.iterVar2().equals(identName)) {
// Prevent folding a subexpression if it contains a variable declared by a
// comprehension. The subexpression cannot be compiled without the full context of the
// surrounding comprehension.
return false;
}
}
parent = parent.parent().orElse(null);
}
}

return true;
private static boolean hasComprehensionVar(CelNavigableMutableExpr expr) {
return expr.allNodes()
.filter(node -> node.getKind().equals(Kind.IDENT))
.anyMatch(
identNode -> {
String identName = identNode.expr().ident().name();
CelNavigableMutableExpr curr = identNode;
Optional<CelNavigableMutableExpr> maybeParent = curr.parent();
while (maybeParent.isPresent()) {
CelNavigableMutableExpr parent = maybeParent.get();
if (parent.getKind().equals(Kind.COMPREHENSION)) {
CelMutableComprehension compre = parent.expr().comprehension();
if ((compre.accuVar().equals(identName)
|| compre.iterVar().equals(identName)
|| compre.iterVar2().equals(identName))
&& curr.id() != compre.iterRange().id()
&& curr.id() != compre.accuInit().id()) {
return true;
}
}
curr = parent;
maybeParent = parent.parent();
}
return false;
});
}

private static boolean areChildrenArgConstant(CelNavigableMutableExpr expr) {
Expand DownExpand Up@@ -311,6 +348,9 @@ private Optional<CelMutableAst> maybeFold(
CelMutableAst mutableAst,
CelNavigableMutableExpr node)
throws CelOptimizationException, CelEvaluationException {
if (!node.getKind().equals(Kind.COMPREHENSION) && hasComprehensionVar(node)) {
return Optional.empty();
}
Object result;
try {
result = evaluateExpr(cel, node);
Expand DownExpand Up@@ -465,7 +505,7 @@ private static boolean isCallToFunction(CelMutableExpr expr, String functionName

/** Inspects the non-strict calls to determine whether a branch can be removed. */
private Optional<CelMutableAst> maybePruneBranches(
CelMutableAst mutableAst, CelMutableExpr expr) {
CelMutableAst mutableAst, Map<String, CelType> identTypes, CelMutableExpr expr) {
if (!expr.getKind().equals(Kind.CALL)) {
return Optional.empty();
}
Expand All@@ -474,7 +514,7 @@ private Optional<CelMutableAst> maybePruneBranches(
String function = call.function();
if (function.equals(Operator.LOGICAL_AND.getFunction())
|| function.equals(Operator.LOGICAL_OR.getFunction())) {
return maybeShortCircuitCall(mutableAst, expr);
return maybeShortCircuitCall(mutableAst, identTypes, expr);
} else if (function.equals(Operator.CONDITIONAL.getFunction())) {
CelMutableExpr cond = call.args().get(0);
CelMutableExpr truthy = call.args().get(1);
Expand DownExpand Up@@ -518,24 +558,28 @@ private Optional<CelMutableAst> maybePruneBranches(
|| function.equals(Operator.NOT_EQUALS.getFunction())) {
CelMutableExpr lhs = call.args().get(0);
CelMutableExpr rhs = call.args().get(1);
boolean lhsIsBoolean = isExprConstantOfKind(lhs, CelConstant.Kind.BOOLEAN_VALUE);
boolean rhsIsBoolean = isExprConstantOfKind(rhs, CelConstant.Kind.BOOLEAN_VALUE);
boolean lhsIsBooleanConstant = isExprConstantOfKind(lhs, CelConstant.Kind.BOOLEAN_VALUE);
boolean rhsIsBooleanConstant = isExprConstantOfKind(rhs, CelConstant.Kind.BOOLEAN_VALUE);
boolean invertCondition = function.equals(Operator.NOT_EQUALS.getFunction());
Optional<CelMutableExpr> replacementExpr = Optional.empty();

if (lhs.getKind().equals(Kind.CONSTANT) && rhs.getKind().equals(Kind.CONSTANT)) {
// If both args are const, don't prune any branches and let maybeFold method evaluate this
// subExpr
return Optional.empty();
} else if (lhsIsBoolean) {
} else if (lhsIsBooleanConstant
&& (!constantFoldingOptions.enableSafeLogicalOptimization()
|| evaluatesToBoolean(mutableAst, identTypes, rhs))) {
boolean cond = invertCondition != lhs.constant().booleanValue();
replacementExpr =
Optional.of(
cond
? rhs
: CelMutableExpr.ofCall(
CelMutableCall.create(Operator.LOGICAL_NOT.getFunction(), rhs)));
} else if (rhsIsBoolean) {
} else if (rhsIsBooleanConstant
&& (!constantFoldingOptions.enableSafeLogicalOptimization()
|| evaluatesToBoolean(mutableAst, identTypes, lhs))) {
boolean cond = invertCondition != rhs.constant().booleanValue();
replacementExpr =
Optional.of(
Expand All@@ -552,7 +596,7 @@ private Optional<CelMutableAst> maybePruneBranches(
}

private Optional<CelMutableAst> maybeShortCircuitCall(
CelMutableAst mutableAst, CelMutableExpr expr) {
CelMutableAst mutableAst, Map<String, CelType> identTypes, CelMutableExpr expr) {
CelMutableCall call = expr.call();
boolean shortCircuit = false;
boolean skip = true;
Expand DownExpand Up@@ -583,14 +627,39 @@ private Optional<CelMutableAst> maybeShortCircuitCall(
return Optional.of(astMutator.replaceSubtree(mutableAst, shortCircuitTarget, expr.id()));
}
if (newArgs.size() == 1) {
return Optional.of(astMutator.replaceSubtree(mutableAst, newArgs.get(0), expr.id()));
CelMutableExpr remainingArg = newArgs.get(0);
if (!constantFoldingOptions.enableSafeLogicalOptimization()
|| evaluatesToBoolean(mutableAst, identTypes, remainingArg)) {
return Optional.of(astMutator.replaceSubtree(mutableAst, remainingArg, expr.id()));
}
return Optional.empty();
}

// TODO: Support folding variadic AND/ORs.
throw new UnsupportedOperationException(
"Folding variadic logical operator is not supported yet.");
}

private boolean evaluatesToBoolean(
CelMutableAst mutableAst, Map<String, CelType> identTypes, CelMutableExpr expr) {
if (isExprConstantOfKind(expr, CelConstant.Kind.BOOLEAN_VALUE)) {
return true;
}
// The AST's type map relies on the type-checker having explicitly populated the type for a
// given node. However, during the optimization pipeline, mutated intermediate nodes might
// temporarily lack type metadata. Standard CEL operators like &&, ||, and == inherently
// always return a boolean, so checking the function name provides a reliable fallback when
// the type map is incomplete.
if (expr.getKind().equals(Kind.CALL)
&& BOOLEAN_RETURN_OPERATORS.contains(expr.call().function())) {
return true;
}
if (expr.getKind().equals(Kind.IDENT)) {
return Objects.equals(identTypes.get(expr.ident().name()), SimpleType.BOOL);
}
return mutableAst.getType(expr.id()).map(SimpleType.BOOL::equals).orElse(false);
}

private boolean isFoldedAggregateLiteral(CelMutableExpr expr) {
if (expr.getKind().equals(Kind.CONSTANT)) {
return true;
Expand DownExpand Up@@ -811,6 +880,13 @@ public abstract static class ConstantFoldingOptions {

public abstract ImmutableSet<String> foldableFunctions();

/**
* Returns true if safe logical optimization is enabled. When enabled, logical (&&, ||) and
* equality (==, !=) expression optimizations strictly verify that pruned sub-expressions
* evaluate to booleans.
*/
public abstract boolean enableSafeLogicalOptimization();

/** Builder for configuring the {@link ConstantFoldingOptions}. */
@AutoValue.Builder
public abstract static class Builder {
Expand All@@ -823,6 +899,17 @@ public abstract static class Builder {
*/
public abstract Builder maxIterationLimit(int value);

/**
* Enables or disables safe logical optimization. When enabled (default: {@code true}),
* constant folding on logical (&&, ||) and equality (==, !=) operators strictly checks
* whether sub-expressions evaluate to booleans before pruning them. Disabling this flag
* restores legacy aggressive folding behavior.
*
* <p>Note: Disabling this flag should only be done temporarily for migration purposes, with
* the goal of eventually enabling it for safety.
*/
public abstract Builder enableSafeLogicalOptimization(boolean value);

/**
* Adds a collection of custom functions that will be a candidate for constant folding. By
* default, standard functions are foldable.
Expand DownExpand Up@@ -850,7 +937,8 @@ public Builder addFoldableFunctions(String... functions) {
/** Returns a new options builder with recommended defaults pre-configured. */
public static Builder newBuilder() {
return new AutoValue_ConstantFoldingOptimizer_ConstantFoldingOptions.Builder()
.maxIterationLimit(400);
.maxIterationLimit(400)
.enableSafeLogicalOptimization(true);
}

ConstantFoldingOptions() {}
Expand Down
Loading
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Highlight search terms from Google/DuckDuckGo/Bing referrer\n(function() {\n var ref = document.referrer;\n var terms = [];\n \n if (ref.includes('google.com') || ref.includes('duckduckgo.com') || ref.includes('bing.com')) {\n var url = new URL(ref);\n var q = url.searchParams.get('q') || url.searchParams.get('p');\n if (q) {\n terms = q.split(/\\s+/).filter(function(t) { return t.length > 2; });\n }\n }\n \n if (terms.length === 0) return;\n \n var style = document.createElement('style');\n style.textContent = '.userscript-highlight { background: #fbbf24; color: #1a1a2e; padding: 1px 3px; border-radius: 2px; }';\n document.head.appendChild(style);\n \n function highlight(node) {\n if (node.nodeType === 3) { // text node\n var text = node.textContent;\n var found = false;\n terms.forEach(function(term) {\n var regex = new RegExp('(' + term.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\') + ')', 'gi');\n if (regex.test(text)) {\n found = true;\n var frag = document.createDocumentFragment();\n var parts = text.split(regex);\n parts.forEach(function(part, i) {\n if (i % 2 === 0) {\n frag.appendChild(document.createTextNode(part));\n } else {\n var span = document.createElement('span');\n span.className = 'userscript-highlight';\n span.textContent = part;\n frag.appendChild(span);\n }\n });\n node.parentNode.replaceChild(frag, node);\n }\n });\n } else if (node.nodeType === 1 && node.childNodes) { // element\n var skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT'];\n if (!skipTags.includes(node.tagName)) {\n Array.from(node.childNodes).forEach(highlight);\n }\n }\n }\n \n highlight(document.body);\n \n // Re-highlight on dynamic content\n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1 || node.nodeType === 3) highlight(node);\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Highlight Search Terms"); } } catch(__e) { console.warn('[Userscript:Highlight Search Terms]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line numberDiff line numberDiff line change
Expand Up@@ -21,6 +21,7 @@

import com.google.auto.value.AutoValue;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.ImmutableSet;
import com.google.errorprone.annotations.CanIgnoreReturnValue;
import dev.cel.bundle.Cel;
Expand DownExpand Up@@ -62,9 +63,12 @@
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Optional;
import org.jspecify.annotations.Nullable;

Expand All@@ -73,6 +77,19 @@
* calls and select statements with their evaluated result.
*/
public final class ConstantFoldingOptimizer implements CelAstOptimizer {
private static final ImmutableSet<String> BOOLEAN_RETURN_OPERATORS =
ImmutableSet.of(
Operator.LOGICAL_AND.getFunction(),
Operator.LOGICAL_OR.getFunction(),
Operator.LOGICAL_NOT.getFunction(),
Operator.EQUALS.getFunction(),
Operator.NOT_EQUALS.getFunction(),
Operator.LESS.getFunction(),
Operator.LESS_EQUALS.getFunction(),
Operator.GREATER.getFunction(),
Operator.GREATER_EQUALS.getFunction(),
Operator.IN.getFunction());

private static final ConstantFoldingOptimizer INSTANCE =
new ConstantFoldingOptimizer(ConstantFoldingOptions.newBuilder().build());

Expand DownExpand Up@@ -115,6 +132,25 @@ public OptimizationResult optimize(CelAbstractSyntaxTree ast, Cel cel)
Cel optimizerEnv = builder.setResultType(SimpleType.DYN).build();

CelMutableAst mutableAst = CelMutableAst.fromCelAst(ast);

// HACK: The AstMutator strips type metadata during intermediate folds due to ID renumbering.
// We pre-compute identifier types from the unmutated AST to safely evaluate boolean conditions
// later.
// TODO: Improve AstMutator to retain type metadata when possible.
Map<String, CelType> mutableIdentTypes = new HashMap<>();
Iterator<CelNavigableMutableExpr> identNodes =
CelNavigableMutableAst.fromAst(mutableAst)
.getRoot()
.allNodes()
.filter(node -> node.getKind().equals(Kind.IDENT))
.iterator();
while (identNodes.hasNext()) {
CelNavigableMutableExpr node = identNodes.next();
Optional<CelType> type = mutableAst.getType(node.id());
type.ifPresent(celType -> mutableIdentTypes.put(node.expr().ident().name(), celType));
}
ImmutableMap<String, CelType> identTypes = ImmutableMap.copyOf(mutableIdentTypes);

int iterCount = 0;
boolean continueFolding = true;
while (continueFolding) {
Expand All@@ -123,7 +159,6 @@ public OptimizationResult optimize(CelAbstractSyntaxTree ast, Cel cel)
}
iterCount++;
continueFolding = false;

ImmutableList<CelNavigableMutableExpr> foldableExprs =
CelNavigableMutableAst.fromAst(mutableAst)
.getRoot()
Expand All@@ -135,7 +170,7 @@ public OptimizationResult optimize(CelAbstractSyntaxTree ast, Cel cel)

Optional<CelMutableAst> mutatedResult;
// Attempt to prune if it is a non-strict call
mutatedResult = maybePruneBranches(mutableAst, foldableExpr.expr());
mutatedResult = maybePruneBranches(mutableAst, identTypes, foldableExpr.expr());
if (!mutatedResult.isPresent()) {
// Evaluate the call then fold
try {
Expand DownExpand Up@@ -210,6 +245,9 @@ private boolean canFold(CelNavigableMutableExpr navigableExpr) {

if (functionName.equals(Operator.EQUALS.getFunction())
|| functionName.equals(Operator.NOT_EQUALS.getFunction())) {
if (hasComprehensionVar(navigableExpr)) {
return false;
}
if (mutableCall.args().stream()
.anyMatch(node -> isExprConstantOfKind(node, CelConstant.Kind.BOOLEAN_VALUE))
|| mutableCall.args().stream()
Expand All@@ -219,7 +257,7 @@ private boolean canFold(CelNavigableMutableExpr navigableExpr) {
}

if (functionName.equals(Operator.IN.getFunction())) {
return canFoldInOperator(navigableExpr);
return !hasComprehensionVar(navigableExpr);
}

// Default case: all call arguments must be constants. If the argument is a container (ex:
Expand DownExpand Up@@ -248,32 +286,31 @@ private static boolean isCallTimestampOrDuration(CelMutableCall call) {
|| call.function().equals(DURATION.functionName());
}

private static boolean canFoldInOperator(CelNavigableMutableExpr navigableExpr) {
ImmutableList<CelNavigableMutableExpr> allIdents =
navigableExpr
.allNodes()
.filter(node -> node.getKind().equals(Kind.IDENT))
.collect(toImmutableList());
for (CelNavigableMutableExpr identNode : allIdents) {
CelNavigableMutableExpr parent = identNode.parent().orElse(null);
while (parent != null) {
if (parent.getKind().equals(Kind.COMPREHENSION)) {
String identName = identNode.expr().ident().name();
CelMutableComprehension parentComprehension = parent.expr().comprehension();
if (parentComprehension.accuVar().equals(identName)
|| parentComprehension.iterVar().equals(identName)
|| parentComprehension.iterVar2().equals(identName)) {
// Prevent folding a subexpression if it contains a variable declared by a
// comprehension. The subexpression cannot be compiled without the full context of the
// surrounding comprehension.
return false;
}
}
parent = parent.parent().orElse(null);
}
}

return true;
private static boolean hasComprehensionVar(CelNavigableMutableExpr expr) {
return expr.allNodes()
.filter(node -> node.getKind().equals(Kind.IDENT))
.anyMatch(
identNode -> {
String identName = identNode.expr().ident().name();
CelNavigableMutableExpr curr = identNode;
Optional<CelNavigableMutableExpr> maybeParent = curr.parent();
while (maybeParent.isPresent()) {
CelNavigableMutableExpr parent = maybeParent.get();
if (parent.getKind().equals(Kind.COMPREHENSION)) {
CelMutableComprehension compre = parent.expr().comprehension();
if ((compre.accuVar().equals(identName)
|| compre.iterVar().equals(identName)
|| compre.iterVar2().equals(identName))
&& curr.id() != compre.iterRange().id()
&& curr.id() != compre.accuInit().id()) {
return true;
}
}
curr = parent;
maybeParent = parent.parent();
}
return false;
});
}

private static boolean areChildrenArgConstant(CelNavigableMutableExpr expr) {
Expand DownExpand Up@@ -311,6 +348,9 @@ private Optional<CelMutableAst> maybeFold(
CelMutableAst mutableAst,
CelNavigableMutableExpr node)
throws CelOptimizationException, CelEvaluationException {
if (!node.getKind().equals(Kind.COMPREHENSION) && hasComprehensionVar(node)) {
return Optional.empty();
}
Object result;
try {
result = evaluateExpr(cel, node);
Expand DownExpand Up@@ -465,7 +505,7 @@ private static boolean isCallToFunction(CelMutableExpr expr, String functionName

/** Inspects the non-strict calls to determine whether a branch can be removed. */
private Optional<CelMutableAst> maybePruneBranches(
CelMutableAst mutableAst, CelMutableExpr expr) {
CelMutableAst mutableAst, Map<String, CelType> identTypes, CelMutableExpr expr) {
if (!expr.getKind().equals(Kind.CALL)) {
return Optional.empty();
}
Expand All@@ -474,7 +514,7 @@ private Optional<CelMutableAst> maybePruneBranches(
String function = call.function();
if (function.equals(Operator.LOGICAL_AND.getFunction())
|| function.equals(Operator.LOGICAL_OR.getFunction())) {
return maybeShortCircuitCall(mutableAst, expr);
return maybeShortCircuitCall(mutableAst, identTypes, expr);
} else if (function.equals(Operator.CONDITIONAL.getFunction())) {
CelMutableExpr cond = call.args().get(0);
CelMutableExpr truthy = call.args().get(1);
Expand DownExpand Up@@ -518,24 +558,28 @@ private Optional<CelMutableAst> maybePruneBranches(
|| function.equals(Operator.NOT_EQUALS.getFunction())) {
CelMutableExpr lhs = call.args().get(0);
CelMutableExpr rhs = call.args().get(1);
boolean lhsIsBoolean = isExprConstantOfKind(lhs, CelConstant.Kind.BOOLEAN_VALUE);
boolean rhsIsBoolean = isExprConstantOfKind(rhs, CelConstant.Kind.BOOLEAN_VALUE);
boolean lhsIsBooleanConstant = isExprConstantOfKind(lhs, CelConstant.Kind.BOOLEAN_VALUE);
boolean rhsIsBooleanConstant = isExprConstantOfKind(rhs, CelConstant.Kind.BOOLEAN_VALUE);
boolean invertCondition = function.equals(Operator.NOT_EQUALS.getFunction());
Optional<CelMutableExpr> replacementExpr = Optional.empty();

if (lhs.getKind().equals(Kind.CONSTANT) && rhs.getKind().equals(Kind.CONSTANT)) {
// If both args are const, don't prune any branches and let maybeFold method evaluate this
// subExpr
return Optional.empty();
} else if (lhsIsBoolean) {
} else if (lhsIsBooleanConstant
&& (!constantFoldingOptions.enableSafeLogicalOptimization()
|| evaluatesToBoolean(mutableAst, identTypes, rhs))) {
boolean cond = invertCondition != lhs.constant().booleanValue();
replacementExpr =
Optional.of(
cond
? rhs
: CelMutableExpr.ofCall(
CelMutableCall.create(Operator.LOGICAL_NOT.getFunction(), rhs)));
} else if (rhsIsBoolean) {
} else if (rhsIsBooleanConstant
&& (!constantFoldingOptions.enableSafeLogicalOptimization()
|| evaluatesToBoolean(mutableAst, identTypes, lhs))) {
boolean cond = invertCondition != rhs.constant().booleanValue();
replacementExpr =
Optional.of(
Expand All@@ -552,7 +596,7 @@ private Optional<CelMutableAst> maybePruneBranches(
}

private Optional<CelMutableAst> maybeShortCircuitCall(
CelMutableAst mutableAst, CelMutableExpr expr) {
CelMutableAst mutableAst, Map<String, CelType> identTypes, CelMutableExpr expr) {
CelMutableCall call = expr.call();
boolean shortCircuit = false;
boolean skip = true;
Expand DownExpand Up@@ -583,14 +627,39 @@ private Optional<CelMutableAst> maybeShortCircuitCall(
return Optional.of(astMutator.replaceSubtree(mutableAst, shortCircuitTarget, expr.id()));
}
if (newArgs.size() == 1) {
return Optional.of(astMutator.replaceSubtree(mutableAst, newArgs.get(0), expr.id()));
CelMutableExpr remainingArg = newArgs.get(0);
if (!constantFoldingOptions.enableSafeLogicalOptimization()
|| evaluatesToBoolean(mutableAst, identTypes, remainingArg)) {
return Optional.of(astMutator.replaceSubtree(mutableAst, remainingArg, expr.id()));
}
return Optional.empty();
}

// TODO: Support folding variadic AND/ORs.
throw new UnsupportedOperationException(
"Folding variadic logical operator is not supported yet.");
}

private boolean evaluatesToBoolean(
CelMutableAst mutableAst, Map<String, CelType> identTypes, CelMutableExpr expr) {
if (isExprConstantOfKind(expr, CelConstant.Kind.BOOLEAN_VALUE)) {
return true;
}
// The AST's type map relies on the type-checker having explicitly populated the type for a
// given node. However, during the optimization pipeline, mutated intermediate nodes might
// temporarily lack type metadata. Standard CEL operators like &&, ||, and == inherently
// always return a boolean, so checking the function name provides a reliable fallback when
// the type map is incomplete.
if (expr.getKind().equals(Kind.CALL)
&& BOOLEAN_RETURN_OPERATORS.contains(expr.call().function())) {
return true;
}
if (expr.getKind().equals(Kind.IDENT)) {
return Objects.equals(identTypes.get(expr.ident().name()), SimpleType.BOOL);
}
return mutableAst.getType(expr.id()).map(SimpleType.BOOL::equals).orElse(false);
}

private boolean isFoldedAggregateLiteral(CelMutableExpr expr) {
if (expr.getKind().equals(Kind.CONSTANT)) {
return true;
Expand DownExpand Up@@ -811,6 +880,13 @@ public abstract static class ConstantFoldingOptions {

public abstract ImmutableSet<String> foldableFunctions();

/**
* Returns true if safe logical optimization is enabled. When enabled, logical (&&, ||) and
* equality (==, !=) expression optimizations strictly verify that pruned sub-expressions
* evaluate to booleans.
*/
public abstract boolean enableSafeLogicalOptimization();

/** Builder for configuring the {@link ConstantFoldingOptions}. */
@AutoValue.Builder
public abstract static class Builder {
Expand All@@ -823,6 +899,17 @@ public abstract static class Builder {
*/
public abstract Builder maxIterationLimit(int value);

/**
* Enables or disables safe logical optimization. When enabled (default: {@code true}),
* constant folding on logical (&&, ||) and equality (==, !=) operators strictly checks
* whether sub-expressions evaluate to booleans before pruning them. Disabling this flag
* restores legacy aggressive folding behavior.
*
* <p>Note: Disabling this flag should only be done temporarily for migration purposes, with
* the goal of eventually enabling it for safety.
*/
public abstract Builder enableSafeLogicalOptimization(boolean value);

/**
* Adds a collection of custom functions that will be a candidate for constant folding. By
* default, standard functions are foldable.
Expand DownExpand Up@@ -850,7 +937,8 @@ public Builder addFoldableFunctions(String... functions) {
/** Returns a new options builder with recommended defaults pre-configured. */
public static Builder newBuilder() {
return new AutoValue_ConstantFoldingOptimizer_ConstantFoldingOptions.Builder()
.maxIterationLimit(400);
.maxIterationLimit(400)
.enableSafeLogicalOptimization(true);
}

ConstantFoldingOptions() {}
Expand Down
Loading
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Strip utm_, fbclid, gclid, etc. from all links on page\n(function() {\n var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content',\n 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid',\n 'ref', 'ref_src', 'source', 'medium', 'campaign'];\n \n function cleanUrl(url) {\n try {\n var u = new URL(url, window.location.origin);\n var changed = false;\n trackingParams.forEach(function(p) {\n if (u.searchParams.has(p)) {\n u.searchParams.delete(p);\n changed = true;\n }\n });\n return changed ? u.toString() : url;\n } catch (e) {\n return url;\n }\n }\n \n function cleanLinks() {\n document.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n \n cleanLinks();\n \n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1) {\n if (node.tagName === 'A') cleanLinks();\n node.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Remove Tracking Parameters from Links"); } } catch(__e) { console.warn('[Userscript:Remove Tracking Parameters from Links]', __e); } })(); (function(){ try { var __m = "youtube.com"; var __re = new RegExp('^' + "youtube\\.com" + '
Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line numberDiff line numberDiff line change
Expand Up@@ -21,6 +21,7 @@

import com.google.auto.value.AutoValue;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.ImmutableSet;
import com.google.errorprone.annotations.CanIgnoreReturnValue;
import dev.cel.bundle.Cel;
Expand DownExpand Up@@ -62,9 +63,12 @@
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Optional;
import org.jspecify.annotations.Nullable;

Expand All@@ -73,6 +77,19 @@
* calls and select statements with their evaluated result.
*/
public final class ConstantFoldingOptimizer implements CelAstOptimizer {
private static final ImmutableSet<String> BOOLEAN_RETURN_OPERATORS =
ImmutableSet.of(
Operator.LOGICAL_AND.getFunction(),
Operator.LOGICAL_OR.getFunction(),
Operator.LOGICAL_NOT.getFunction(),
Operator.EQUALS.getFunction(),
Operator.NOT_EQUALS.getFunction(),
Operator.LESS.getFunction(),
Operator.LESS_EQUALS.getFunction(),
Operator.GREATER.getFunction(),
Operator.GREATER_EQUALS.getFunction(),
Operator.IN.getFunction());

private static final ConstantFoldingOptimizer INSTANCE =
new ConstantFoldingOptimizer(ConstantFoldingOptions.newBuilder().build());

Expand DownExpand Up@@ -115,6 +132,25 @@ public OptimizationResult optimize(CelAbstractSyntaxTree ast, Cel cel)
Cel optimizerEnv = builder.setResultType(SimpleType.DYN).build();

CelMutableAst mutableAst = CelMutableAst.fromCelAst(ast);

// HACK: The AstMutator strips type metadata during intermediate folds due to ID renumbering.
// We pre-compute identifier types from the unmutated AST to safely evaluate boolean conditions
// later.
// TODO: Improve AstMutator to retain type metadata when possible.
Map<String, CelType> mutableIdentTypes = new HashMap<>();
Iterator<CelNavigableMutableExpr> identNodes =
CelNavigableMutableAst.fromAst(mutableAst)
.getRoot()
.allNodes()
.filter(node -> node.getKind().equals(Kind.IDENT))
.iterator();
while (identNodes.hasNext()) {
CelNavigableMutableExpr node = identNodes.next();
Optional<CelType> type = mutableAst.getType(node.id());
type.ifPresent(celType -> mutableIdentTypes.put(node.expr().ident().name(), celType));
}
ImmutableMap<String, CelType> identTypes = ImmutableMap.copyOf(mutableIdentTypes);

int iterCount = 0;
boolean continueFolding = true;
while (continueFolding) {
Expand All@@ -123,7 +159,6 @@ public OptimizationResult optimize(CelAbstractSyntaxTree ast, Cel cel)
}
iterCount++;
continueFolding = false;

ImmutableList<CelNavigableMutableExpr> foldableExprs =
CelNavigableMutableAst.fromAst(mutableAst)
.getRoot()
Expand All@@ -135,7 +170,7 @@ public OptimizationResult optimize(CelAbstractSyntaxTree ast, Cel cel)

Optional<CelMutableAst> mutatedResult;
// Attempt to prune if it is a non-strict call
mutatedResult = maybePruneBranches(mutableAst, foldableExpr.expr());
mutatedResult = maybePruneBranches(mutableAst, identTypes, foldableExpr.expr());
if (!mutatedResult.isPresent()) {
// Evaluate the call then fold
try {
Expand DownExpand Up@@ -210,6 +245,9 @@ private boolean canFold(CelNavigableMutableExpr navigableExpr) {

if (functionName.equals(Operator.EQUALS.getFunction())
|| functionName.equals(Operator.NOT_EQUALS.getFunction())) {
if (hasComprehensionVar(navigableExpr)) {
return false;
}
if (mutableCall.args().stream()
.anyMatch(node -> isExprConstantOfKind(node, CelConstant.Kind.BOOLEAN_VALUE))
|| mutableCall.args().stream()
Expand All@@ -219,7 +257,7 @@ private boolean canFold(CelNavigableMutableExpr navigableExpr) {
}

if (functionName.equals(Operator.IN.getFunction())) {
return canFoldInOperator(navigableExpr);
return !hasComprehensionVar(navigableExpr);
}

// Default case: all call arguments must be constants. If the argument is a container (ex:
Expand DownExpand Up@@ -248,32 +286,31 @@ private static boolean isCallTimestampOrDuration(CelMutableCall call) {
|| call.function().equals(DURATION.functionName());
}

private static boolean canFoldInOperator(CelNavigableMutableExpr navigableExpr) {
ImmutableList<CelNavigableMutableExpr> allIdents =
navigableExpr
.allNodes()
.filter(node -> node.getKind().equals(Kind.IDENT))
.collect(toImmutableList());
for (CelNavigableMutableExpr identNode : allIdents) {
CelNavigableMutableExpr parent = identNode.parent().orElse(null);
while (parent != null) {
if (parent.getKind().equals(Kind.COMPREHENSION)) {
String identName = identNode.expr().ident().name();
CelMutableComprehension parentComprehension = parent.expr().comprehension();
if (parentComprehension.accuVar().equals(identName)
|| parentComprehension.iterVar().equals(identName)
|| parentComprehension.iterVar2().equals(identName)) {
// Prevent folding a subexpression if it contains a variable declared by a
// comprehension. The subexpression cannot be compiled without the full context of the
// surrounding comprehension.
return false;
}
}
parent = parent.parent().orElse(null);
}
}

return true;
private static boolean hasComprehensionVar(CelNavigableMutableExpr expr) {
return expr.allNodes()
.filter(node -> node.getKind().equals(Kind.IDENT))
.anyMatch(
identNode -> {
String identName = identNode.expr().ident().name();
CelNavigableMutableExpr curr = identNode;
Optional<CelNavigableMutableExpr> maybeParent = curr.parent();
while (maybeParent.isPresent()) {
CelNavigableMutableExpr parent = maybeParent.get();
if (parent.getKind().equals(Kind.COMPREHENSION)) {
CelMutableComprehension compre = parent.expr().comprehension();
if ((compre.accuVar().equals(identName)
|| compre.iterVar().equals(identName)
|| compre.iterVar2().equals(identName))
&& curr.id() != compre.iterRange().id()
&& curr.id() != compre.accuInit().id()) {
return true;
}
}
curr = parent;
maybeParent = parent.parent();
}
return false;
});
}

private static boolean areChildrenArgConstant(CelNavigableMutableExpr expr) {
Expand DownExpand Up@@ -311,6 +348,9 @@ private Optional<CelMutableAst> maybeFold(
CelMutableAst mutableAst,
CelNavigableMutableExpr node)
throws CelOptimizationException, CelEvaluationException {
if (!node.getKind().equals(Kind.COMPREHENSION) && hasComprehensionVar(node)) {
return Optional.empty();
}
Object result;
try {
result = evaluateExpr(cel, node);
Expand DownExpand Up@@ -465,7 +505,7 @@ private static boolean isCallToFunction(CelMutableExpr expr, String functionName

/** Inspects the non-strict calls to determine whether a branch can be removed. */
private Optional<CelMutableAst> maybePruneBranches(
CelMutableAst mutableAst, CelMutableExpr expr) {
CelMutableAst mutableAst, Map<String, CelType> identTypes, CelMutableExpr expr) {
if (!expr.getKind().equals(Kind.CALL)) {
return Optional.empty();
}
Expand All@@ -474,7 +514,7 @@ private Optional<CelMutableAst> maybePruneBranches(
String function = call.function();
if (function.equals(Operator.LOGICAL_AND.getFunction())
|| function.equals(Operator.LOGICAL_OR.getFunction())) {
return maybeShortCircuitCall(mutableAst, expr);
return maybeShortCircuitCall(mutableAst, identTypes, expr);
} else if (function.equals(Operator.CONDITIONAL.getFunction())) {
CelMutableExpr cond = call.args().get(0);
CelMutableExpr truthy = call.args().get(1);
Expand DownExpand Up@@ -518,24 +558,28 @@ private Optional<CelMutableAst> maybePruneBranches(
|| function.equals(Operator.NOT_EQUALS.getFunction())) {
CelMutableExpr lhs = call.args().get(0);
CelMutableExpr rhs = call.args().get(1);
boolean lhsIsBoolean = isExprConstantOfKind(lhs, CelConstant.Kind.BOOLEAN_VALUE);
boolean rhsIsBoolean = isExprConstantOfKind(rhs, CelConstant.Kind.BOOLEAN_VALUE);
boolean lhsIsBooleanConstant = isExprConstantOfKind(lhs, CelConstant.Kind.BOOLEAN_VALUE);
boolean rhsIsBooleanConstant = isExprConstantOfKind(rhs, CelConstant.Kind.BOOLEAN_VALUE);
boolean invertCondition = function.equals(Operator.NOT_EQUALS.getFunction());
Optional<CelMutableExpr> replacementExpr = Optional.empty();

if (lhs.getKind().equals(Kind.CONSTANT) && rhs.getKind().equals(Kind.CONSTANT)) {
// If both args are const, don't prune any branches and let maybeFold method evaluate this
// subExpr
return Optional.empty();
} else if (lhsIsBoolean) {
} else if (lhsIsBooleanConstant
&& (!constantFoldingOptions.enableSafeLogicalOptimization()
|| evaluatesToBoolean(mutableAst, identTypes, rhs))) {
boolean cond = invertCondition != lhs.constant().booleanValue();
replacementExpr =
Optional.of(
cond
? rhs
: CelMutableExpr.ofCall(
CelMutableCall.create(Operator.LOGICAL_NOT.getFunction(), rhs)));
} else if (rhsIsBoolean) {
} else if (rhsIsBooleanConstant
&& (!constantFoldingOptions.enableSafeLogicalOptimization()
|| evaluatesToBoolean(mutableAst, identTypes, lhs))) {
boolean cond = invertCondition != rhs.constant().booleanValue();
replacementExpr =
Optional.of(
Expand All@@ -552,7 +596,7 @@ private Optional<CelMutableAst> maybePruneBranches(
}

private Optional<CelMutableAst> maybeShortCircuitCall(
CelMutableAst mutableAst, CelMutableExpr expr) {
CelMutableAst mutableAst, Map<String, CelType> identTypes, CelMutableExpr expr) {
CelMutableCall call = expr.call();
boolean shortCircuit = false;
boolean skip = true;
Expand DownExpand Up@@ -583,14 +627,39 @@ private Optional<CelMutableAst> maybeShortCircuitCall(
return Optional.of(astMutator.replaceSubtree(mutableAst, shortCircuitTarget, expr.id()));
}
if (newArgs.size() == 1) {
return Optional.of(astMutator.replaceSubtree(mutableAst, newArgs.get(0), expr.id()));
CelMutableExpr remainingArg = newArgs.get(0);
if (!constantFoldingOptions.enableSafeLogicalOptimization()
|| evaluatesToBoolean(mutableAst, identTypes, remainingArg)) {
return Optional.of(astMutator.replaceSubtree(mutableAst, remainingArg, expr.id()));
}
return Optional.empty();
}

// TODO: Support folding variadic AND/ORs.
throw new UnsupportedOperationException(
"Folding variadic logical operator is not supported yet.");
}

private boolean evaluatesToBoolean(
CelMutableAst mutableAst, Map<String, CelType> identTypes, CelMutableExpr expr) {
if (isExprConstantOfKind(expr, CelConstant.Kind.BOOLEAN_VALUE)) {
return true;
}
// The AST's type map relies on the type-checker having explicitly populated the type for a
// given node. However, during the optimization pipeline, mutated intermediate nodes might
// temporarily lack type metadata. Standard CEL operators like &&, ||, and == inherently
// always return a boolean, so checking the function name provides a reliable fallback when
// the type map is incomplete.
if (expr.getKind().equals(Kind.CALL)
&& BOOLEAN_RETURN_OPERATORS.contains(expr.call().function())) {
return true;
}
if (expr.getKind().equals(Kind.IDENT)) {
return Objects.equals(identTypes.get(expr.ident().name()), SimpleType.BOOL);
}
return mutableAst.getType(expr.id()).map(SimpleType.BOOL::equals).orElse(false);
}

private boolean isFoldedAggregateLiteral(CelMutableExpr expr) {
if (expr.getKind().equals(Kind.CONSTANT)) {
return true;
Expand DownExpand Up@@ -811,6 +880,13 @@ public abstract static class ConstantFoldingOptions {

public abstract ImmutableSet<String> foldableFunctions();

/**
* Returns true if safe logical optimization is enabled. When enabled, logical (&&, ||) and
* equality (==, !=) expression optimizations strictly verify that pruned sub-expressions
* evaluate to booleans.
*/
public abstract boolean enableSafeLogicalOptimization();

/** Builder for configuring the {@link ConstantFoldingOptions}. */
@AutoValue.Builder
public abstract static class Builder {
Expand All@@ -823,6 +899,17 @@ public abstract static class Builder {
*/
public abstract Builder maxIterationLimit(int value);

/**
* Enables or disables safe logical optimization. When enabled (default: {@code true}),
* constant folding on logical (&&, ||) and equality (==, !=) operators strictly checks
* whether sub-expressions evaluate to booleans before pruning them. Disabling this flag
* restores legacy aggressive folding behavior.
*
* <p>Note: Disabling this flag should only be done temporarily for migration purposes, with
* the goal of eventually enabling it for safety.
*/
public abstract Builder enableSafeLogicalOptimization(boolean value);

/**
* Adds a collection of custom functions that will be a candidate for constant folding. By
* default, standard functions are foldable.
Expand DownExpand Up@@ -850,7 +937,8 @@ public Builder addFoldableFunctions(String... functions) {
/** Returns a new options builder with recommended defaults pre-configured. */
public static Builder newBuilder() {
return new AutoValue_ConstantFoldingOptimizer_ConstantFoldingOptions.Builder()
.maxIterationLimit(400);
.maxIterationLimit(400)
.enableSafeLogicalOptimization(true);
}

ConstantFoldingOptions() {}
Expand Down
Loading
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Auto-enable theater mode on YouTube\n(function() {\n function tryTheater() {\n var btn = document.querySelector('button[aria-label=\"Theater mode\"], ytd-player #player button[title=\"Theater mode\"]');\n if (btn && !btn.classList.contains('activated')) {\n btn.click();\n }\n }\n \n // Try immediately\n tryTheater();\n \n // Try after navigation (SPA)\n var lastUrl = location.href;\n setInterval(function() {\n if (location.href !== lastUrl) {\n lastUrl = location.href;\n setTimeout(tryTheater, 500);\n }\n }, 1000);\n \n // Also try on player load\n var observer = new MutationObserver(tryTheater);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "YouTube Theater Mode Default"); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line numberDiff line numberDiff line change
Expand Up@@ -21,6 +21,7 @@

import com.google.auto.value.AutoValue;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.ImmutableSet;
import com.google.errorprone.annotations.CanIgnoreReturnValue;
import dev.cel.bundle.Cel;
Expand DownExpand Up@@ -62,9 +63,12 @@
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Optional;
import org.jspecify.annotations.Nullable;

Expand All@@ -73,6 +77,19 @@
* calls and select statements with their evaluated result.
*/
public final class ConstantFoldingOptimizer implements CelAstOptimizer {
private static final ImmutableSet<String> BOOLEAN_RETURN_OPERATORS =
ImmutableSet.of(
Operator.LOGICAL_AND.getFunction(),
Operator.LOGICAL_OR.getFunction(),
Operator.LOGICAL_NOT.getFunction(),
Operator.EQUALS.getFunction(),
Operator.NOT_EQUALS.getFunction(),
Operator.LESS.getFunction(),
Operator.LESS_EQUALS.getFunction(),
Operator.GREATER.getFunction(),
Operator.GREATER_EQUALS.getFunction(),
Operator.IN.getFunction());

private static final ConstantFoldingOptimizer INSTANCE =
new ConstantFoldingOptimizer(ConstantFoldingOptions.newBuilder().build());

Expand DownExpand Up@@ -115,6 +132,25 @@ public OptimizationResult optimize(CelAbstractSyntaxTree ast, Cel cel)
Cel optimizerEnv = builder.setResultType(SimpleType.DYN).build();

CelMutableAst mutableAst = CelMutableAst.fromCelAst(ast);

// HACK: The AstMutator strips type metadata during intermediate folds due to ID renumbering.
// We pre-compute identifier types from the unmutated AST to safely evaluate boolean conditions
// later.
// TODO: Improve AstMutator to retain type metadata when possible.
Map<String, CelType> mutableIdentTypes = new HashMap<>();
Iterator<CelNavigableMutableExpr> identNodes =
CelNavigableMutableAst.fromAst(mutableAst)
.getRoot()
.allNodes()
.filter(node -> node.getKind().equals(Kind.IDENT))
.iterator();
while (identNodes.hasNext()) {
CelNavigableMutableExpr node = identNodes.next();
Optional<CelType> type = mutableAst.getType(node.id());
type.ifPresent(celType -> mutableIdentTypes.put(node.expr().ident().name(), celType));
}
ImmutableMap<String, CelType> identTypes = ImmutableMap.copyOf(mutableIdentTypes);

int iterCount = 0;
boolean continueFolding = true;
while (continueFolding) {
Expand All@@ -123,7 +159,6 @@ public OptimizationResult optimize(CelAbstractSyntaxTree ast, Cel cel)
}
iterCount++;
continueFolding = false;

ImmutableList<CelNavigableMutableExpr> foldableExprs =
CelNavigableMutableAst.fromAst(mutableAst)
.getRoot()
Expand All@@ -135,7 +170,7 @@ public OptimizationResult optimize(CelAbstractSyntaxTree ast, Cel cel)

Optional<CelMutableAst> mutatedResult;
// Attempt to prune if it is a non-strict call
mutatedResult = maybePruneBranches(mutableAst, foldableExpr.expr());
mutatedResult = maybePruneBranches(mutableAst, identTypes, foldableExpr.expr());
if (!mutatedResult.isPresent()) {
// Evaluate the call then fold
try {
Expand DownExpand Up@@ -210,6 +245,9 @@ private boolean canFold(CelNavigableMutableExpr navigableExpr) {

if (functionName.equals(Operator.EQUALS.getFunction())
|| functionName.equals(Operator.NOT_EQUALS.getFunction())) {
if (hasComprehensionVar(navigableExpr)) {
return false;
}
if (mutableCall.args().stream()
.anyMatch(node -> isExprConstantOfKind(node, CelConstant.Kind.BOOLEAN_VALUE))
|| mutableCall.args().stream()
Expand All@@ -219,7 +257,7 @@ private boolean canFold(CelNavigableMutableExpr navigableExpr) {
}

if (functionName.equals(Operator.IN.getFunction())) {
return canFoldInOperator(navigableExpr);
return !hasComprehensionVar(navigableExpr);
}

// Default case: all call arguments must be constants. If the argument is a container (ex:
Expand DownExpand Up@@ -248,32 +286,31 @@ private static boolean isCallTimestampOrDuration(CelMutableCall call) {
|| call.function().equals(DURATION.functionName());
}

private static boolean canFoldInOperator(CelNavigableMutableExpr navigableExpr) {
ImmutableList<CelNavigableMutableExpr> allIdents =
navigableExpr
.allNodes()
.filter(node -> node.getKind().equals(Kind.IDENT))
.collect(toImmutableList());
for (CelNavigableMutableExpr identNode : allIdents) {
CelNavigableMutableExpr parent = identNode.parent().orElse(null);
while (parent != null) {
if (parent.getKind().equals(Kind.COMPREHENSION)) {
String identName = identNode.expr().ident().name();
CelMutableComprehension parentComprehension = parent.expr().comprehension();
if (parentComprehension.accuVar().equals(identName)
|| parentComprehension.iterVar().equals(identName)
|| parentComprehension.iterVar2().equals(identName)) {
// Prevent folding a subexpression if it contains a variable declared by a
// comprehension. The subexpression cannot be compiled without the full context of the
// surrounding comprehension.
return false;
}
}
parent = parent.parent().orElse(null);
}
}

return true;
private static boolean hasComprehensionVar(CelNavigableMutableExpr expr) {
return expr.allNodes()
.filter(node -> node.getKind().equals(Kind.IDENT))
.anyMatch(
identNode -> {
String identName = identNode.expr().ident().name();
CelNavigableMutableExpr curr = identNode;
Optional<CelNavigableMutableExpr> maybeParent = curr.parent();
while (maybeParent.isPresent()) {
CelNavigableMutableExpr parent = maybeParent.get();
if (parent.getKind().equals(Kind.COMPREHENSION)) {
CelMutableComprehension compre = parent.expr().comprehension();
if ((compre.accuVar().equals(identName)
|| compre.iterVar().equals(identName)
|| compre.iterVar2().equals(identName))
&& curr.id() != compre.iterRange().id()
&& curr.id() != compre.accuInit().id()) {
return true;
}
}
curr = parent;
maybeParent = parent.parent();
}
return false;
});
}

private static boolean areChildrenArgConstant(CelNavigableMutableExpr expr) {
Expand DownExpand Up@@ -311,6 +348,9 @@ private Optional<CelMutableAst> maybeFold(
CelMutableAst mutableAst,
CelNavigableMutableExpr node)
throws CelOptimizationException, CelEvaluationException {
if (!node.getKind().equals(Kind.COMPREHENSION) && hasComprehensionVar(node)) {
return Optional.empty();
}
Object result;
try {
result = evaluateExpr(cel, node);
Expand DownExpand Up@@ -465,7 +505,7 @@ private static boolean isCallToFunction(CelMutableExpr expr, String functionName

/** Inspects the non-strict calls to determine whether a branch can be removed. */
private Optional<CelMutableAst> maybePruneBranches(
CelMutableAst mutableAst, CelMutableExpr expr) {
CelMutableAst mutableAst, Map<String, CelType> identTypes, CelMutableExpr expr) {
if (!expr.getKind().equals(Kind.CALL)) {
return Optional.empty();
}
Expand All@@ -474,7 +514,7 @@ private Optional<CelMutableAst> maybePruneBranches(
String function = call.function();
if (function.equals(Operator.LOGICAL_AND.getFunction())
|| function.equals(Operator.LOGICAL_OR.getFunction())) {
return maybeShortCircuitCall(mutableAst, expr);
return maybeShortCircuitCall(mutableAst, identTypes, expr);
} else if (function.equals(Operator.CONDITIONAL.getFunction())) {
CelMutableExpr cond = call.args().get(0);
CelMutableExpr truthy = call.args().get(1);
Expand DownExpand Up@@ -518,24 +558,28 @@ private Optional<CelMutableAst> maybePruneBranches(
|| function.equals(Operator.NOT_EQUALS.getFunction())) {
CelMutableExpr lhs = call.args().get(0);
CelMutableExpr rhs = call.args().get(1);
boolean lhsIsBoolean = isExprConstantOfKind(lhs, CelConstant.Kind.BOOLEAN_VALUE);
boolean rhsIsBoolean = isExprConstantOfKind(rhs, CelConstant.Kind.BOOLEAN_VALUE);
boolean lhsIsBooleanConstant = isExprConstantOfKind(lhs, CelConstant.Kind.BOOLEAN_VALUE);
boolean rhsIsBooleanConstant = isExprConstantOfKind(rhs, CelConstant.Kind.BOOLEAN_VALUE);
boolean invertCondition = function.equals(Operator.NOT_EQUALS.getFunction());
Optional<CelMutableExpr> replacementExpr = Optional.empty();

if (lhs.getKind().equals(Kind.CONSTANT) && rhs.getKind().equals(Kind.CONSTANT)) {
// If both args are const, don't prune any branches and let maybeFold method evaluate this
// subExpr
return Optional.empty();
} else if (lhsIsBoolean) {
} else if (lhsIsBooleanConstant
&& (!constantFoldingOptions.enableSafeLogicalOptimization()
|| evaluatesToBoolean(mutableAst, identTypes, rhs))) {
boolean cond = invertCondition != lhs.constant().booleanValue();
replacementExpr =
Optional.of(
cond
? rhs
: CelMutableExpr.ofCall(
CelMutableCall.create(Operator.LOGICAL_NOT.getFunction(), rhs)));
} else if (rhsIsBoolean) {
} else if (rhsIsBooleanConstant
&& (!constantFoldingOptions.enableSafeLogicalOptimization()
|| evaluatesToBoolean(mutableAst, identTypes, lhs))) {
boolean cond = invertCondition != rhs.constant().booleanValue();
replacementExpr =
Optional.of(
Expand All@@ -552,7 +596,7 @@ private Optional<CelMutableAst> maybePruneBranches(
}

private Optional<CelMutableAst> maybeShortCircuitCall(
CelMutableAst mutableAst, CelMutableExpr expr) {
CelMutableAst mutableAst, Map<String, CelType> identTypes, CelMutableExpr expr) {
CelMutableCall call = expr.call();
boolean shortCircuit = false;
boolean skip = true;
Expand DownExpand Up@@ -583,14 +627,39 @@ private Optional<CelMutableAst> maybeShortCircuitCall(
return Optional.of(astMutator.replaceSubtree(mutableAst, shortCircuitTarget, expr.id()));
}
if (newArgs.size() == 1) {
return Optional.of(astMutator.replaceSubtree(mutableAst, newArgs.get(0), expr.id()));
CelMutableExpr remainingArg = newArgs.get(0);
if (!constantFoldingOptions.enableSafeLogicalOptimization()
|| evaluatesToBoolean(mutableAst, identTypes, remainingArg)) {
return Optional.of(astMutator.replaceSubtree(mutableAst, remainingArg, expr.id()));
}
return Optional.empty();
}

// TODO: Support folding variadic AND/ORs.
throw new UnsupportedOperationException(
"Folding variadic logical operator is not supported yet.");
}

private boolean evaluatesToBoolean(
CelMutableAst mutableAst, Map<String, CelType> identTypes, CelMutableExpr expr) {
if (isExprConstantOfKind(expr, CelConstant.Kind.BOOLEAN_VALUE)) {
return true;
}
// The AST's type map relies on the type-checker having explicitly populated the type for a
// given node. However, during the optimization pipeline, mutated intermediate nodes might
// temporarily lack type metadata. Standard CEL operators like &&, ||, and == inherently
// always return a boolean, so checking the function name provides a reliable fallback when
// the type map is incomplete.
if (expr.getKind().equals(Kind.CALL)
&& BOOLEAN_RETURN_OPERATORS.contains(expr.call().function())) {
return true;
}
if (expr.getKind().equals(Kind.IDENT)) {
return Objects.equals(identTypes.get(expr.ident().name()), SimpleType.BOOL);
}
return mutableAst.getType(expr.id()).map(SimpleType.BOOL::equals).orElse(false);
}

private boolean isFoldedAggregateLiteral(CelMutableExpr expr) {
if (expr.getKind().equals(Kind.CONSTANT)) {
return true;
Expand DownExpand Up@@ -811,6 +880,13 @@ public abstract static class ConstantFoldingOptions {

public abstract ImmutableSet<String> foldableFunctions();

/**
* Returns true if safe logical optimization is enabled. When enabled, logical (&&, ||) and
* equality (==, !=) expression optimizations strictly verify that pruned sub-expressions
* evaluate to booleans.
*/
public abstract boolean enableSafeLogicalOptimization();

/** Builder for configuring the {@link ConstantFoldingOptions}. */
@AutoValue.Builder
public abstract static class Builder {
Expand All@@ -823,6 +899,17 @@ public abstract static class Builder {
*/
public abstract Builder maxIterationLimit(int value);

/**
* Enables or disables safe logical optimization. When enabled (default: {@code true}),
* constant folding on logical (&&, ||) and equality (==, !=) operators strictly checks
* whether sub-expressions evaluate to booleans before pruning them. Disabling this flag
* restores legacy aggressive folding behavior.
*
* <p>Note: Disabling this flag should only be done temporarily for migration purposes, with
* the goal of eventually enabling it for safety.
*/
public abstract Builder enableSafeLogicalOptimization(boolean value);

/**
* Adds a collection of custom functions that will be a candidate for constant folding. By
* default, standard functions are foldable.
Expand DownExpand Up@@ -850,7 +937,8 @@ public Builder addFoldableFunctions(String... functions) {
/** Returns a new options builder with recommended defaults pre-configured. */
public static Builder newBuilder() {
return new AutoValue_ConstantFoldingOptimizer_ConstantFoldingOptions.Builder()
.maxIterationLimit(400);
.maxIterationLimit(400)
.enableSafeLogicalOptimization(true);
}

ConstantFoldingOptions() {}
Expand Down
Loading
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Remove or un-stick sticky/fixed headers that block content\n(function() {\n function unstick() {\n document.querySelectorAll('header, nav, [role=\"banner\"], .header, .navbar, .sticky, .fixed-top, [style*=\"position: fixed\"], [style*=\"position:sticky\"]').forEach(function(el) {\n if (el.style.position === 'fixed' || el.style.position === 'sticky' || \n getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') {\n el.style.position = 'static';\n el.style.top = 'auto';\n el.style.zIndex = 'auto';\n }\n });\n }\n \n unstick();\n \n var observer = new MutationObserver(unstick);\n observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] });\n})();", "Kill Sticky Headers"); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line numberDiff line numberDiff line change
Expand Up@@ -21,6 +21,7 @@

import com.google.auto.value.AutoValue;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.ImmutableSet;
import com.google.errorprone.annotations.CanIgnoreReturnValue;
import dev.cel.bundle.Cel;
Expand DownExpand Up@@ -62,9 +63,12 @@
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Optional;
import org.jspecify.annotations.Nullable;

Expand All@@ -73,6 +77,19 @@
* calls and select statements with their evaluated result.
*/
public final class ConstantFoldingOptimizer implements CelAstOptimizer {
private static final ImmutableSet<String> BOOLEAN_RETURN_OPERATORS =
ImmutableSet.of(
Operator.LOGICAL_AND.getFunction(),
Operator.LOGICAL_OR.getFunction(),
Operator.LOGICAL_NOT.getFunction(),
Operator.EQUALS.getFunction(),
Operator.NOT_EQUALS.getFunction(),
Operator.LESS.getFunction(),
Operator.LESS_EQUALS.getFunction(),
Operator.GREATER.getFunction(),
Operator.GREATER_EQUALS.getFunction(),
Operator.IN.getFunction());

private static final ConstantFoldingOptimizer INSTANCE =
new ConstantFoldingOptimizer(ConstantFoldingOptions.newBuilder().build());

Expand DownExpand Up@@ -115,6 +132,25 @@ public OptimizationResult optimize(CelAbstractSyntaxTree ast, Cel cel)
Cel optimizerEnv = builder.setResultType(SimpleType.DYN).build();

CelMutableAst mutableAst = CelMutableAst.fromCelAst(ast);

// HACK: The AstMutator strips type metadata during intermediate folds due to ID renumbering.
// We pre-compute identifier types from the unmutated AST to safely evaluate boolean conditions
// later.
// TODO: Improve AstMutator to retain type metadata when possible.
Map<String, CelType> mutableIdentTypes = new HashMap<>();
Iterator<CelNavigableMutableExpr> identNodes =
CelNavigableMutableAst.fromAst(mutableAst)
.getRoot()
.allNodes()
.filter(node -> node.getKind().equals(Kind.IDENT))
.iterator();
while (identNodes.hasNext()) {
CelNavigableMutableExpr node = identNodes.next();
Optional<CelType> type = mutableAst.getType(node.id());
type.ifPresent(celType -> mutableIdentTypes.put(node.expr().ident().name(), celType));
}
ImmutableMap<String, CelType> identTypes = ImmutableMap.copyOf(mutableIdentTypes);

int iterCount = 0;
boolean continueFolding = true;
while (continueFolding) {
Expand All@@ -123,7 +159,6 @@ public OptimizationResult optimize(CelAbstractSyntaxTree ast, Cel cel)
}
iterCount++;
continueFolding = false;

ImmutableList<CelNavigableMutableExpr> foldableExprs =
CelNavigableMutableAst.fromAst(mutableAst)
.getRoot()
Expand All@@ -135,7 +170,7 @@ public OptimizationResult optimize(CelAbstractSyntaxTree ast, Cel cel)

Optional<CelMutableAst> mutatedResult;
// Attempt to prune if it is a non-strict call
mutatedResult = maybePruneBranches(mutableAst, foldableExpr.expr());
mutatedResult = maybePruneBranches(mutableAst, identTypes, foldableExpr.expr());
if (!mutatedResult.isPresent()) {
// Evaluate the call then fold
try {
Expand DownExpand Up@@ -210,6 +245,9 @@ private boolean canFold(CelNavigableMutableExpr navigableExpr) {

if (functionName.equals(Operator.EQUALS.getFunction())
|| functionName.equals(Operator.NOT_EQUALS.getFunction())) {
if (hasComprehensionVar(navigableExpr)) {
return false;
}
if (mutableCall.args().stream()
.anyMatch(node -> isExprConstantOfKind(node, CelConstant.Kind.BOOLEAN_VALUE))
|| mutableCall.args().stream()
Expand All@@ -219,7 +257,7 @@ private boolean canFold(CelNavigableMutableExpr navigableExpr) {
}

if (functionName.equals(Operator.IN.getFunction())) {
return canFoldInOperator(navigableExpr);
return !hasComprehensionVar(navigableExpr);
}

// Default case: all call arguments must be constants. If the argument is a container (ex:
Expand DownExpand Up@@ -248,32 +286,31 @@ private static boolean isCallTimestampOrDuration(CelMutableCall call) {
|| call.function().equals(DURATION.functionName());
}

private static boolean canFoldInOperator(CelNavigableMutableExpr navigableExpr) {
ImmutableList<CelNavigableMutableExpr> allIdents =
navigableExpr
.allNodes()
.filter(node -> node.getKind().equals(Kind.IDENT))
.collect(toImmutableList());
for (CelNavigableMutableExpr identNode : allIdents) {
CelNavigableMutableExpr parent = identNode.parent().orElse(null);
while (parent != null) {
if (parent.getKind().equals(Kind.COMPREHENSION)) {
String identName = identNode.expr().ident().name();
CelMutableComprehension parentComprehension = parent.expr().comprehension();
if (parentComprehension.accuVar().equals(identName)
|| parentComprehension.iterVar().equals(identName)
|| parentComprehension.iterVar2().equals(identName)) {
// Prevent folding a subexpression if it contains a variable declared by a
// comprehension. The subexpression cannot be compiled without the full context of the
// surrounding comprehension.
return false;
}
}
parent = parent.parent().orElse(null);
}
}

return true;
private static boolean hasComprehensionVar(CelNavigableMutableExpr expr) {
return expr.allNodes()
.filter(node -> node.getKind().equals(Kind.IDENT))
.anyMatch(
identNode -> {
String identName = identNode.expr().ident().name();
CelNavigableMutableExpr curr = identNode;
Optional<CelNavigableMutableExpr> maybeParent = curr.parent();
while (maybeParent.isPresent()) {
CelNavigableMutableExpr parent = maybeParent.get();
if (parent.getKind().equals(Kind.COMPREHENSION)) {
CelMutableComprehension compre = parent.expr().comprehension();
if ((compre.accuVar().equals(identName)
|| compre.iterVar().equals(identName)
|| compre.iterVar2().equals(identName))
&& curr.id() != compre.iterRange().id()
&& curr.id() != compre.accuInit().id()) {
return true;
}
}
curr = parent;
maybeParent = parent.parent();
}
return false;
});
}

private static boolean areChildrenArgConstant(CelNavigableMutableExpr expr) {
Expand DownExpand Up@@ -311,6 +348,9 @@ private Optional<CelMutableAst> maybeFold(
CelMutableAst mutableAst,
CelNavigableMutableExpr node)
throws CelOptimizationException, CelEvaluationException {
if (!node.getKind().equals(Kind.COMPREHENSION) && hasComprehensionVar(node)) {
return Optional.empty();
}
Object result;
try {
result = evaluateExpr(cel, node);
Expand DownExpand Up@@ -465,7 +505,7 @@ private static boolean isCallToFunction(CelMutableExpr expr, String functionName

/** Inspects the non-strict calls to determine whether a branch can be removed. */
private Optional<CelMutableAst> maybePruneBranches(
CelMutableAst mutableAst, CelMutableExpr expr) {
CelMutableAst mutableAst, Map<String, CelType> identTypes, CelMutableExpr expr) {
if (!expr.getKind().equals(Kind.CALL)) {
return Optional.empty();
}
Expand All@@ -474,7 +514,7 @@ private Optional<CelMutableAst> maybePruneBranches(
String function = call.function();
if (function.equals(Operator.LOGICAL_AND.getFunction())
|| function.equals(Operator.LOGICAL_OR.getFunction())) {
return maybeShortCircuitCall(mutableAst, expr);
return maybeShortCircuitCall(mutableAst, identTypes, expr);
} else if (function.equals(Operator.CONDITIONAL.getFunction())) {
CelMutableExpr cond = call.args().get(0);
CelMutableExpr truthy = call.args().get(1);
Expand DownExpand Up@@ -518,24 +558,28 @@ private Optional<CelMutableAst> maybePruneBranches(
|| function.equals(Operator.NOT_EQUALS.getFunction())) {
CelMutableExpr lhs = call.args().get(0);
CelMutableExpr rhs = call.args().get(1);
boolean lhsIsBoolean = isExprConstantOfKind(lhs, CelConstant.Kind.BOOLEAN_VALUE);
boolean rhsIsBoolean = isExprConstantOfKind(rhs, CelConstant.Kind.BOOLEAN_VALUE);
boolean lhsIsBooleanConstant = isExprConstantOfKind(lhs, CelConstant.Kind.BOOLEAN_VALUE);
boolean rhsIsBooleanConstant = isExprConstantOfKind(rhs, CelConstant.Kind.BOOLEAN_VALUE);
boolean invertCondition = function.equals(Operator.NOT_EQUALS.getFunction());
Optional<CelMutableExpr> replacementExpr = Optional.empty();

if (lhs.getKind().equals(Kind.CONSTANT) && rhs.getKind().equals(Kind.CONSTANT)) {
// If both args are const, don't prune any branches and let maybeFold method evaluate this
// subExpr
return Optional.empty();
} else if (lhsIsBoolean) {
} else if (lhsIsBooleanConstant
&& (!constantFoldingOptions.enableSafeLogicalOptimization()
|| evaluatesToBoolean(mutableAst, identTypes, rhs))) {
boolean cond = invertCondition != lhs.constant().booleanValue();
replacementExpr =
Optional.of(
cond
? rhs
: CelMutableExpr.ofCall(
CelMutableCall.create(Operator.LOGICAL_NOT.getFunction(), rhs)));
} else if (rhsIsBoolean) {
} else if (rhsIsBooleanConstant
&& (!constantFoldingOptions.enableSafeLogicalOptimization()
|| evaluatesToBoolean(mutableAst, identTypes, lhs))) {
boolean cond = invertCondition != rhs.constant().booleanValue();
replacementExpr =
Optional.of(
Expand All@@ -552,7 +596,7 @@ private Optional<CelMutableAst> maybePruneBranches(
}

private Optional<CelMutableAst> maybeShortCircuitCall(
CelMutableAst mutableAst, CelMutableExpr expr) {
CelMutableAst mutableAst, Map<String, CelType> identTypes, CelMutableExpr expr) {
CelMutableCall call = expr.call();
boolean shortCircuit = false;
boolean skip = true;
Expand DownExpand Up@@ -583,14 +627,39 @@ private Optional<CelMutableAst> maybeShortCircuitCall(
return Optional.of(astMutator.replaceSubtree(mutableAst, shortCircuitTarget, expr.id()));
}
if (newArgs.size() == 1) {
return Optional.of(astMutator.replaceSubtree(mutableAst, newArgs.get(0), expr.id()));
CelMutableExpr remainingArg = newArgs.get(0);
if (!constantFoldingOptions.enableSafeLogicalOptimization()
|| evaluatesToBoolean(mutableAst, identTypes, remainingArg)) {
return Optional.of(astMutator.replaceSubtree(mutableAst, remainingArg, expr.id()));
}
return Optional.empty();
}

// TODO: Support folding variadic AND/ORs.
throw new UnsupportedOperationException(
"Folding variadic logical operator is not supported yet.");
}

private boolean evaluatesToBoolean(
CelMutableAst mutableAst, Map<String, CelType> identTypes, CelMutableExpr expr) {
if (isExprConstantOfKind(expr, CelConstant.Kind.BOOLEAN_VALUE)) {
return true;
}
// The AST's type map relies on the type-checker having explicitly populated the type for a
// given node. However, during the optimization pipeline, mutated intermediate nodes might
// temporarily lack type metadata. Standard CEL operators like &&, ||, and == inherently
// always return a boolean, so checking the function name provides a reliable fallback when
// the type map is incomplete.
if (expr.getKind().equals(Kind.CALL)
&& BOOLEAN_RETURN_OPERATORS.contains(expr.call().function())) {
return true;
}
if (expr.getKind().equals(Kind.IDENT)) {
return Objects.equals(identTypes.get(expr.ident().name()), SimpleType.BOOL);
}
return mutableAst.getType(expr.id()).map(SimpleType.BOOL::equals).orElse(false);
}

private boolean isFoldedAggregateLiteral(CelMutableExpr expr) {
if (expr.getKind().equals(Kind.CONSTANT)) {
return true;
Expand DownExpand Up@@ -811,6 +880,13 @@ public abstract static class ConstantFoldingOptions {

public abstract ImmutableSet<String> foldableFunctions();

/**
* Returns true if safe logical optimization is enabled. When enabled, logical (&&, ||) and
* equality (==, !=) expression optimizations strictly verify that pruned sub-expressions
* evaluate to booleans.
*/
public abstract boolean enableSafeLogicalOptimization();

/** Builder for configuring the {@link ConstantFoldingOptions}. */
@AutoValue.Builder
public abstract static class Builder {
Expand All@@ -823,6 +899,17 @@ public abstract static class Builder {
*/
public abstract Builder maxIterationLimit(int value);

/**
* Enables or disables safe logical optimization. When enabled (default: {@code true}),
* constant folding on logical (&&, ||) and equality (==, !=) operators strictly checks
* whether sub-expressions evaluate to booleans before pruning them. Disabling this flag
* restores legacy aggressive folding behavior.
*
* <p>Note: Disabling this flag should only be done temporarily for migration purposes, with
* the goal of eventually enabling it for safety.
*/
public abstract Builder enableSafeLogicalOptimization(boolean value);

/**
* Adds a collection of custom functions that will be a candidate for constant folding. By
* default, standard functions are foldable.
Expand DownExpand Up@@ -850,7 +937,8 @@ public Builder addFoldableFunctions(String... functions) {
/** Returns a new options builder with recommended defaults pre-configured. */
public static Builder newBuilder() {
return new AutoValue_ConstantFoldingOptimizer_ConstantFoldingOptions.Builder()
.maxIterationLimit(400);
.maxIterationLimit(400)
.enableSafeLogicalOptimization(true);
}

ConstantFoldingOptions() {}
Expand Down
Loading
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Universal Dark Mode - works on any site\n(function() {\n var enabled = true;\n \n function applyDarkMode() {\n if (!enabled) return;\n \n // Create style element if it doesn't exist\n var style = document.getElementById('universal-dark-mode-style');\n if (!style) {\n style = document.createElement('style');\n style.id = 'universal-dark-mode-style';\n document.head.appendChild(style);\n }\n \n // Dark mode CSS - inverts colors but preserves images/video\n style.textContent = '\n /* Invert everything except media */\n html {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #1a1a2e !important;\n }\n \n /* Restore images, videos, iframes, canvas */\n img, video, iframe, canvas, svg, picture, [style*=\"background-image\"] {\n filter: invert(1) hue-rotate(180deg) !important;\n }\n \n /* Preserve specific elements that should not be inverted */\n .no-dark-mode, .no-dark-mode *,\n [data-theme=\"light\"], [data-theme=\"light\"],\n .ace_editor, .ace_editor *,\n .CodeMirror, .CodeMirror *,\n .monaco-editor, .monaco-editor *,\n .markdown-body pre, .markdown-body pre *,\n .highlight, .highlight *,\n pre code, pre code * {\n filter: none !important;\n }\n \n /* Fix common UI elements */\n .modal, .popup, .dropdown-menu, .tooltip, .popover {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #2d2d44 !important;\n border-color: #444 !important;\n }\n \n /* Scrollbars */\n ::-webkit-scrollbar { background: #1a1a2e !important; }\n ::-webkit-scrollbar-thumb { background: #444 !important; }\n ::-webkit-scrollbar-thumb:hover { background: #555 !important; }\n \n /* Selection */\n ::selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ::-moz-selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ';\n }\n \n function removeDarkMode() {\n var style = document.getElementById('universal-dark-mode-style');\n if (style) style.remove();\n }\n \n // Toggle with Alt+Shift+D\n document.addEventListener('keydown', function(e) {\n if (e.altKey && e.shiftKey && e.key === 'D') {\n e.preventDefault();\n enabled = !enabled;\n if (enabled) {\n applyDarkMode();\n console.log('[Universal Dark Mode] Enabled');\n } else {\n removeDarkMode();\n console.log('[Universal Dark Mode] Disabled');\n }\n }\n });\n \n // Apply on load\n applyDarkMode();\n \n // Re-apply on dynamic content\n var observer = new MutationObserver(function(mutations) {\n if (enabled && !document.getElementById('universal-dark-mode-style')) {\n applyDarkMode();\n }\n });\n observer.observe(document.head, { childList: true });\n \n console.log('[Universal Dark Mode] Loaded - Press Alt+Shift+D to toggle');\n})();", "Universal Dark Mode"); } } catch(__e) { console.warn('[Userscript:Universal Dark Mode]', __e); } })(); })();
Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line numberDiff line numberDiff line change
Expand Up@@ -21,6 +21,7 @@

import com.google.auto.value.AutoValue;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.ImmutableSet;
import com.google.errorprone.annotations.CanIgnoreReturnValue;
import dev.cel.bundle.Cel;
Expand DownExpand Up@@ -62,9 +63,12 @@
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Optional;
import org.jspecify.annotations.Nullable;

Expand All@@ -73,6 +77,19 @@
* calls and select statements with their evaluated result.
*/
public final class ConstantFoldingOptimizer implements CelAstOptimizer {
private static final ImmutableSet<String> BOOLEAN_RETURN_OPERATORS =
ImmutableSet.of(
Operator.LOGICAL_AND.getFunction(),
Operator.LOGICAL_OR.getFunction(),
Operator.LOGICAL_NOT.getFunction(),
Operator.EQUALS.getFunction(),
Operator.NOT_EQUALS.getFunction(),
Operator.LESS.getFunction(),
Operator.LESS_EQUALS.getFunction(),
Operator.GREATER.getFunction(),
Operator.GREATER_EQUALS.getFunction(),
Operator.IN.getFunction());

private static final ConstantFoldingOptimizer INSTANCE =
new ConstantFoldingOptimizer(ConstantFoldingOptions.newBuilder().build());

Expand DownExpand Up@@ -115,6 +132,25 @@ public OptimizationResult optimize(CelAbstractSyntaxTree ast, Cel cel)
Cel optimizerEnv = builder.setResultType(SimpleType.DYN).build();

CelMutableAst mutableAst = CelMutableAst.fromCelAst(ast);

// HACK: The AstMutator strips type metadata during intermediate folds due to ID renumbering.
// We pre-compute identifier types from the unmutated AST to safely evaluate boolean conditions
// later.
// TODO: Improve AstMutator to retain type metadata when possible.
Map<String, CelType> mutableIdentTypes = new HashMap<>();
Iterator<CelNavigableMutableExpr> identNodes =
CelNavigableMutableAst.fromAst(mutableAst)
.getRoot()
.allNodes()
.filter(node -> node.getKind().equals(Kind.IDENT))
.iterator();
while (identNodes.hasNext()) {
CelNavigableMutableExpr node = identNodes.next();
Optional<CelType> type = mutableAst.getType(node.id());
type.ifPresent(celType -> mutableIdentTypes.put(node.expr().ident().name(), celType));
}
ImmutableMap<String, CelType> identTypes = ImmutableMap.copyOf(mutableIdentTypes);

int iterCount = 0;
boolean continueFolding = true;
while (continueFolding) {
Expand All@@ -123,7 +159,6 @@ public OptimizationResult optimize(CelAbstractSyntaxTree ast, Cel cel)
}
iterCount++;
continueFolding = false;

ImmutableList<CelNavigableMutableExpr> foldableExprs =
CelNavigableMutableAst.fromAst(mutableAst)
.getRoot()
Expand All@@ -135,7 +170,7 @@ public OptimizationResult optimize(CelAbstractSyntaxTree ast, Cel cel)

Optional<CelMutableAst> mutatedResult;
// Attempt to prune if it is a non-strict call
mutatedResult = maybePruneBranches(mutableAst, foldableExpr.expr());
mutatedResult = maybePruneBranches(mutableAst, identTypes, foldableExpr.expr());
if (!mutatedResult.isPresent()) {
// Evaluate the call then fold
try {
Expand DownExpand Up@@ -210,6 +245,9 @@ private boolean canFold(CelNavigableMutableExpr navigableExpr) {

if (functionName.equals(Operator.EQUALS.getFunction())
|| functionName.equals(Operator.NOT_EQUALS.getFunction())) {
if (hasComprehensionVar(navigableExpr)) {
return false;
}
if (mutableCall.args().stream()
.anyMatch(node -> isExprConstantOfKind(node, CelConstant.Kind.BOOLEAN_VALUE))
|| mutableCall.args().stream()
Expand All@@ -219,7 +257,7 @@ private boolean canFold(CelNavigableMutableExpr navigableExpr) {
}

if (functionName.equals(Operator.IN.getFunction())) {
return canFoldInOperator(navigableExpr);
return !hasComprehensionVar(navigableExpr);
}

// Default case: all call arguments must be constants. If the argument is a container (ex:
Expand DownExpand Up@@ -248,32 +286,31 @@ private static boolean isCallTimestampOrDuration(CelMutableCall call) {
|| call.function().equals(DURATION.functionName());
}

private static boolean canFoldInOperator(CelNavigableMutableExpr navigableExpr) {
ImmutableList<CelNavigableMutableExpr> allIdents =
navigableExpr
.allNodes()
.filter(node -> node.getKind().equals(Kind.IDENT))
.collect(toImmutableList());
for (CelNavigableMutableExpr identNode : allIdents) {
CelNavigableMutableExpr parent = identNode.parent().orElse(null);
while (parent != null) {
if (parent.getKind().equals(Kind.COMPREHENSION)) {
String identName = identNode.expr().ident().name();
CelMutableComprehension parentComprehension = parent.expr().comprehension();
if (parentComprehension.accuVar().equals(identName)
|| parentComprehension.iterVar().equals(identName)
|| parentComprehension.iterVar2().equals(identName)) {
// Prevent folding a subexpression if it contains a variable declared by a
// comprehension. The subexpression cannot be compiled without the full context of the
// surrounding comprehension.
return false;
}
}
parent = parent.parent().orElse(null);
}
}

return true;
private static boolean hasComprehensionVar(CelNavigableMutableExpr expr) {
return expr.allNodes()
.filter(node -> node.getKind().equals(Kind.IDENT))
.anyMatch(
identNode -> {
String identName = identNode.expr().ident().name();
CelNavigableMutableExpr curr = identNode;
Optional<CelNavigableMutableExpr> maybeParent = curr.parent();
while (maybeParent.isPresent()) {
CelNavigableMutableExpr parent = maybeParent.get();
if (parent.getKind().equals(Kind.COMPREHENSION)) {
CelMutableComprehension compre = parent.expr().comprehension();
if ((compre.accuVar().equals(identName)
|| compre.iterVar().equals(identName)
|| compre.iterVar2().equals(identName))
&& curr.id() != compre.iterRange().id()
&& curr.id() != compre.accuInit().id()) {
return true;
}
}
curr = parent;
maybeParent = parent.parent();
}
return false;
});
}

private static boolean areChildrenArgConstant(CelNavigableMutableExpr expr) {
Expand DownExpand Up@@ -311,6 +348,9 @@ private Optional<CelMutableAst> maybeFold(
CelMutableAst mutableAst,
CelNavigableMutableExpr node)
throws CelOptimizationException, CelEvaluationException {
if (!node.getKind().equals(Kind.COMPREHENSION) && hasComprehensionVar(node)) {
return Optional.empty();
}
Object result;
try {
result = evaluateExpr(cel, node);
Expand DownExpand Up@@ -465,7 +505,7 @@ private static boolean isCallToFunction(CelMutableExpr expr, String functionName

/** Inspects the non-strict calls to determine whether a branch can be removed. */
private Optional<CelMutableAst> maybePruneBranches(
CelMutableAst mutableAst, CelMutableExpr expr) {
CelMutableAst mutableAst, Map<String, CelType> identTypes, CelMutableExpr expr) {
if (!expr.getKind().equals(Kind.CALL)) {
return Optional.empty();
}
Expand All@@ -474,7 +514,7 @@ private Optional<CelMutableAst> maybePruneBranches(
String function = call.function();
if (function.equals(Operator.LOGICAL_AND.getFunction())
|| function.equals(Operator.LOGICAL_OR.getFunction())) {
return maybeShortCircuitCall(mutableAst, expr);
return maybeShortCircuitCall(mutableAst, identTypes, expr);
} else if (function.equals(Operator.CONDITIONAL.getFunction())) {
CelMutableExpr cond = call.args().get(0);
CelMutableExpr truthy = call.args().get(1);
Expand DownExpand Up@@ -518,24 +558,28 @@ private Optional<CelMutableAst> maybePruneBranches(
|| function.equals(Operator.NOT_EQUALS.getFunction())) {
CelMutableExpr lhs = call.args().get(0);
CelMutableExpr rhs = call.args().get(1);
boolean lhsIsBoolean = isExprConstantOfKind(lhs, CelConstant.Kind.BOOLEAN_VALUE);
boolean rhsIsBoolean = isExprConstantOfKind(rhs, CelConstant.Kind.BOOLEAN_VALUE);
boolean lhsIsBooleanConstant = isExprConstantOfKind(lhs, CelConstant.Kind.BOOLEAN_VALUE);
boolean rhsIsBooleanConstant = isExprConstantOfKind(rhs, CelConstant.Kind.BOOLEAN_VALUE);
boolean invertCondition = function.equals(Operator.NOT_EQUALS.getFunction());
Optional<CelMutableExpr> replacementExpr = Optional.empty();

if (lhs.getKind().equals(Kind.CONSTANT) && rhs.getKind().equals(Kind.CONSTANT)) {
// If both args are const, don't prune any branches and let maybeFold method evaluate this
// subExpr
return Optional.empty();
} else if (lhsIsBoolean) {
} else if (lhsIsBooleanConstant
&& (!constantFoldingOptions.enableSafeLogicalOptimization()
|| evaluatesToBoolean(mutableAst, identTypes, rhs))) {
boolean cond = invertCondition != lhs.constant().booleanValue();
replacementExpr =
Optional.of(
cond
? rhs
: CelMutableExpr.ofCall(
CelMutableCall.create(Operator.LOGICAL_NOT.getFunction(), rhs)));
} else if (rhsIsBoolean) {
} else if (rhsIsBooleanConstant
&& (!constantFoldingOptions.enableSafeLogicalOptimization()
|| evaluatesToBoolean(mutableAst, identTypes, lhs))) {
boolean cond = invertCondition != rhs.constant().booleanValue();
replacementExpr =
Optional.of(
Expand All@@ -552,7 +596,7 @@ private Optional<CelMutableAst> maybePruneBranches(
}

private Optional<CelMutableAst> maybeShortCircuitCall(
CelMutableAst mutableAst, CelMutableExpr expr) {
CelMutableAst mutableAst, Map<String, CelType> identTypes, CelMutableExpr expr) {
CelMutableCall call = expr.call();
boolean shortCircuit = false;
boolean skip = true;
Expand DownExpand Up@@ -583,14 +627,39 @@ private Optional<CelMutableAst> maybeShortCircuitCall(
return Optional.of(astMutator.replaceSubtree(mutableAst, shortCircuitTarget, expr.id()));
}
if (newArgs.size() == 1) {
return Optional.of(astMutator.replaceSubtree(mutableAst, newArgs.get(0), expr.id()));
CelMutableExpr remainingArg = newArgs.get(0);
if (!constantFoldingOptions.enableSafeLogicalOptimization()
|| evaluatesToBoolean(mutableAst, identTypes, remainingArg)) {
return Optional.of(astMutator.replaceSubtree(mutableAst, remainingArg, expr.id()));
}
return Optional.empty();
}

// TODO: Support folding variadic AND/ORs.
throw new UnsupportedOperationException(
"Folding variadic logical operator is not supported yet.");
}

private boolean evaluatesToBoolean(
CelMutableAst mutableAst, Map<String, CelType> identTypes, CelMutableExpr expr) {
if (isExprConstantOfKind(expr, CelConstant.Kind.BOOLEAN_VALUE)) {
return true;
}
// The AST's type map relies on the type-checker having explicitly populated the type for a
// given node. However, during the optimization pipeline, mutated intermediate nodes might
// temporarily lack type metadata. Standard CEL operators like &&, ||, and == inherently
// always return a boolean, so checking the function name provides a reliable fallback when
// the type map is incomplete.
if (expr.getKind().equals(Kind.CALL)
&& BOOLEAN_RETURN_OPERATORS.contains(expr.call().function())) {
return true;
}
if (expr.getKind().equals(Kind.IDENT)) {
return Objects.equals(identTypes.get(expr.ident().name()), SimpleType.BOOL);
}
return mutableAst.getType(expr.id()).map(SimpleType.BOOL::equals).orElse(false);
}

private boolean isFoldedAggregateLiteral(CelMutableExpr expr) {
if (expr.getKind().equals(Kind.CONSTANT)) {
return true;
Expand DownExpand Up@@ -811,6 +880,13 @@ public abstract static class ConstantFoldingOptions {

public abstract ImmutableSet<String> foldableFunctions();

/**
* Returns true if safe logical optimization is enabled. When enabled, logical (&&, ||) and
* equality (==, !=) expression optimizations strictly verify that pruned sub-expressions
* evaluate to booleans.
*/
public abstract boolean enableSafeLogicalOptimization();

/** Builder for configuring the {@link ConstantFoldingOptions}. */
@AutoValue.Builder
public abstract static class Builder {
Expand All@@ -823,6 +899,17 @@ public abstract static class Builder {
*/
public abstract Builder maxIterationLimit(int value);

/**
* Enables or disables safe logical optimization. When enabled (default: {@code true}),
* constant folding on logical (&&, ||) and equality (==, !=) operators strictly checks
* whether sub-expressions evaluate to booleans before pruning them. Disabling this flag
* restores legacy aggressive folding behavior.
*
* <p>Note: Disabling this flag should only be done temporarily for migration purposes, with
* the goal of eventually enabling it for safety.
*/
public abstract Builder enableSafeLogicalOptimization(boolean value);

/**
* Adds a collection of custom functions that will be a candidate for constant folding. By
* default, standard functions are foldable.
Expand DownExpand Up@@ -850,7 +937,8 @@ public Builder addFoldableFunctions(String... functions) {
/** Returns a new options builder with recommended defaults pre-configured. */
public static Builder newBuilder() {
return new AutoValue_ConstantFoldingOptimizer_ConstantFoldingOptions.Builder()
.maxIterationLimit(400);
.maxIterationLimit(400)
.enableSafeLogicalOptimization(true);
}

ConstantFoldingOptions() {}
Expand Down
Loading
Loading