Skip to content

Commit 9ccf86c

Browse files
l46kokcopybara-github
authored andcommitted
Policy nested rule fix
PiperOrigin-RevId: 905187056
1 parent f566450 commit 9ccf86c

15 files changed

Lines changed: 493 additions & 82 deletions

File tree

policy/src/main/java/dev/cel/policy/RuleComposer.java

Lines changed: 158 additions & 77 deletions
Original file line numberDiff line numberDiff line change
@@ -18,14 +18,14 @@
1818
import static com.google.common.collect.ImmutableList.toImmutableList;
1919
import static java.util.stream.Collectors.toCollection;
2020

21-
import com.google.auto.value.AutoValue;
2221
import com.google.common.collect.ImmutableList;
2322
import com.google.common.collect.Lists;
2423
import dev.cel.bundle.Cel;
2524
import dev.cel.common.CelAbstractSyntaxTree;
2625
import dev.cel.common.CelMutableAst;
2726
import dev.cel.common.CelValidationException;
2827
import dev.cel.common.Operator;
28+
import dev.cel.common.ast.CelExpr;
2929
import dev.cel.common.ast.CelExpr.ExprKind.Kind;
3030
import dev.cel.common.formats.ValueString;
3131
import dev.cel.common.navigation.CelNavigableMutableAst;
@@ -48,22 +48,11 @@ final class RuleComposer implements CelAstOptimizer {
4848

4949
@Override
5050
public OptimizationResult optimize(CelAbstractSyntaxTree ast, Cel cel) {
51-
RuleOptimizationResult result = optimizeRule(cel, compiledRule);
52-
return OptimizationResult.create(result.ast().toParsedAst());
51+
Step result = optimizeRule(cel, compiledRule);
52+
return OptimizationResult.create(result.expr.toParsedAst());
5353
}
5454

55-
@AutoValue
56-
abstract static class RuleOptimizationResult {
57-
abstract CelMutableAst ast();
58-
59-
abstract boolean isOptionalResult();
60-
61-
static RuleOptimizationResult create(CelMutableAst ast, boolean isOptionalResult) {
62-
return new AutoValue_RuleComposer_RuleOptimizationResult(ast, isOptionalResult);
63-
}
64-
}
65-
66-
private RuleOptimizationResult optimizeRule(Cel cel, CelCompiledRule compiledRule) {
55+
private Step optimizeRule(Cel cel, CelCompiledRule compiledRule) {
6756
cel =
6857
cel.toCelBuilder()
6958
.addVarDeclarations(
@@ -72,81 +61,52 @@ private RuleOptimizationResult optimizeRule(Cel cel, CelCompiledRule compiledRul
7261
.collect(toImmutableList()))
7362
.build();
7463

75-
CelMutableAst matchAst = astMutator.newGlobalCall(Function.OPTIONAL_NONE.getFunction());
76-
boolean isOptionalResult = true;
64+
Step output = null;
65+
if (compiledRule.hasOptionalOutput()) {
66+
output =
67+
new Step(
68+
true,
69+
false,
70+
newTrueAst(cel),
71+
astMutator.newGlobalCall(Function.OPTIONAL_NONE.getFunction()));
72+
}
73+
7774
// Keep track of the last output ID that might cause type-check failure while attempting to
7875
// compose the subgraphs.
7976
long lastOutputId = 0;
77+
8078
for (CelCompiledMatch match : Lists.reverse(compiledRule.matches())) {
8179
CelAbstractSyntaxTree conditionAst = match.condition();
82-
// If the condition is trivially true, none of the matches in the rule causes the result
83-
// to become optional, and the rule is not the last match, then this will introduce
84-
// unreachable outputs or rules.
8580
boolean isTriviallyTrue = match.isConditionTriviallyTrue();
81+
CelMutableAst condAst = CelMutableAst.fromCelAst(conditionAst);
8682

8783
switch (match.result().kind()) {
88-
// For the match's output, determine whether the output should be wrapped
89-
// into an optional value, a conditional, or both.
9084
case OUTPUT:
9185
OutputValue matchOutput = match.result().output();
9286
CelMutableAst outAst = CelMutableAst.fromCelAst(matchOutput.ast());
93-
if (isTriviallyTrue) {
94-
matchAst = outAst;
95-
isOptionalResult = false;
96-
lastOutputId = matchOutput.sourceId();
97-
continue;
98-
}
99-
if (isOptionalResult) {
100-
outAst = astMutator.newGlobalCall(Function.OPTIONAL_OF.getFunction(), outAst);
101-
}
87+
Step step = new Step(false, !isTriviallyTrue, condAst, outAst);
88+
output = combine(cel, astMutator, step, output);
10289

103-
matchAst =
104-
astMutator.newGlobalCall(
105-
Operator.CONDITIONAL.getFunction(),
106-
CelMutableAst.fromCelAst(conditionAst),
107-
outAst,
108-
matchAst);
10990
assertComposedAstIsValid(
11091
cel,
111-
matchAst,
92+
output.expr,
11293
"conflicting output types found.",
11394
matchOutput.sourceId(),
11495
lastOutputId);
96+
11597
lastOutputId = matchOutput.sourceId();
11698
continue;
11799
case RULE:
118-
// If the match has a nested rule, then compute the rule and whether it has
119-
// an optional return value.
120100
CelCompiledRule matchNestedRule = match.result().rule();
121-
RuleOptimizationResult nestedRule = optimizeRule(cel, matchNestedRule);
101+
Step nestedRule = optimizeRule(cel, matchNestedRule);
122102
boolean nestedHasOptional = matchNestedRule.hasOptionalOutput();
123-
CelMutableAst nestedRuleAst = nestedRule.ast();
124-
if (isOptionalResult && !nestedHasOptional) {
125-
nestedRuleAst =
126-
astMutator.newGlobalCall(Function.OPTIONAL_OF.getFunction(), nestedRuleAst);
127-
}
128-
if (!isOptionalResult && nestedHasOptional) {
129-
matchAst = astMutator.newGlobalCall(Function.OPTIONAL_OF.getFunction(), matchAst);
130-
isOptionalResult = true;
131-
}
132-
// If either the nested rule or current condition output are optional then
133-
// use optional.or() to specify the combination of the first and second results
134-
// Note, the argument order is reversed due to the traversal of matches in
135-
// reverse order.
136-
if (isOptionalResult && isTriviallyTrue) {
137-
matchAst = astMutator.newMemberCall(nestedRuleAst, Function.OR.getFunction(), matchAst);
138-
} else {
139-
matchAst =
140-
astMutator.newGlobalCall(
141-
Operator.CONDITIONAL.getFunction(),
142-
CelMutableAst.fromCelAst(conditionAst),
143-
nestedRuleAst,
144-
matchAst);
145-
}
103+
104+
Step ruleStep = new Step(nestedHasOptional, !isTriviallyTrue, condAst, nestedRule.expr);
105+
output = combine(cel, astMutator, ruleStep, output);
146106

147107
assertComposedAstIsValid(
148108
cel,
149-
matchAst,
109+
output.expr,
150110
String.format(
151111
"failed composing the subrule '%s' due to conflicting output types.",
152112
matchNestedRule.ruleId().map(ValueString::value).orElse("")),
@@ -155,11 +115,106 @@ private RuleOptimizationResult optimizeRule(Cel cel, CelCompiledRule compiledRul
155115
}
156116
}
157117

158-
CelMutableAst result = inlineCompiledVariables(matchAst, compiledRule.variables());
118+
CelMutableAst resultExpr = output.expr;
119+
resultExpr = inlineCompiledVariables(resultExpr, compiledRule.variables());
120+
resultExpr = astMutator.renumberIdsConsecutively(resultExpr);
159121

160-
result = astMutator.renumberIdsConsecutively(result);
122+
return new Step(output.isOptional, false, newTrueAst(cel), resultExpr);
123+
}
161124

162-
return RuleOptimizationResult.create(result, isOptionalResult);
125+
static RuleComposer newInstance(
126+
CelCompiledRule compiledRule, String variablePrefix, int iterationLimit) {
127+
return new RuleComposer(compiledRule, variablePrefix, iterationLimit);
128+
}
129+
130+
// For the match's output, determine whether the output should be wrapped
131+
// into an optional value, a conditional, or both.
132+
//
133+
// Assembles two output expressions into a single output step.
134+
private Step combine(Cel cel, AstMutator astMutator, Step s, Step step) {
135+
if (step == null) {
136+
return s;
137+
}
138+
CelMutableAst trueCondition = newTrueAst(cel);
139+
140+
if (s.isOptional) {
141+
if (step.isOptional) {
142+
if (s.isConditional) {
143+
return new Step(
144+
true,
145+
false,
146+
trueCondition,
147+
astMutator.newGlobalCall(
148+
Operator.CONDITIONAL.getFunction(), s.cond, s.expr, step.expr));
149+
} else {
150+
if (!isOptionalNone(step.expr)) {
151+
// If either the nested rule or current condition output are optional then
152+
// use optional.or() to specify the combination of the first and second results
153+
// Note, the argument order is reversed due to the traversal of matches in
154+
// reverse order.
155+
return new Step(
156+
true, false, trueCondition, astMutator.newMemberCall(s.expr, "or", step.expr));
157+
}
158+
return s;
159+
}
160+
} else { // step is non-optional
161+
if (s.isConditional) {
162+
return new Step(
163+
true,
164+
false,
165+
trueCondition,
166+
astMutator.newGlobalCall(
167+
Operator.CONDITIONAL.getFunction(),
168+
s.cond,
169+
s.expr,
170+
astMutator.newGlobalCall(Function.OPTIONAL_OF.getFunction(), step.expr)));
171+
} else {
172+
return new Step(
173+
false, false, trueCondition, astMutator.newMemberCall(s.expr, "orValue", step.expr));
174+
}
175+
}
176+
} else { // s is non-optional
177+
if (step.isOptional) {
178+
if (s.isConditional) {
179+
return new Step(
180+
true,
181+
false,
182+
trueCondition,
183+
astMutator.newGlobalCall(
184+
Operator.CONDITIONAL.getFunction(),
185+
s.cond,
186+
astMutator.newGlobalCall(Function.OPTIONAL_OF.getFunction(), s.expr),
187+
step.expr));
188+
} else {
189+
// If the condition is trivially true, none of the matches in the rule causes the result
190+
// to become optional, and the rule is not the last match, then this will introduce
191+
// unreachable outputs or rules (pruning away 'step').
192+
return s;
193+
}
194+
} else { // step is non-optional
195+
return new Step(
196+
false,
197+
false,
198+
trueCondition,
199+
astMutator.newGlobalCall(
200+
Operator.CONDITIONAL.getFunction(), s.cond, s.expr, step.expr));
201+
}
202+
}
203+
}
204+
205+
private static boolean isOptionalNone(CelMutableAst ast) {
206+
CelExpr expr = ast.toParsedAst().getExpr();
207+
return expr.getKind().equals(Kind.CALL)
208+
&& expr.call().function().equals("optional.none")
209+
&& expr.call().args().isEmpty();
210+
}
211+
212+
private static CelMutableAst newTrueAst(Cel cel) {
213+
try {
214+
return CelMutableAst.fromCelAst(cel.compile("true").getAst());
215+
} catch (CelValidationException e) {
216+
throw new IllegalStateException("Failed to compile 'true'", e);
217+
}
163218
}
164219

165220
private CelMutableAst inlineCompiledVariables(
@@ -186,11 +241,6 @@ private CelMutableAst inlineCompiledVariables(
186241
return mutatedAst;
187242
}
188243

189-
static RuleComposer newInstance(
190-
CelCompiledRule compiledRule, String variablePrefix, int iterationLimit) {
191-
return new RuleComposer(compiledRule, variablePrefix, iterationLimit);
192-
}
193-
194244
private void assertComposedAstIsValid(
195245
Cel cel, CelMutableAst composedAst, String failureMessage, Long... ids) {
196246
assertComposedAstIsValid(cel, composedAst, failureMessage, Arrays.asList(ids));
@@ -206,10 +256,35 @@ private void assertComposedAstIsValid(
206256
}
207257
}
208258

209-
private RuleComposer(CelCompiledRule compiledRule, String variablePrefix, int iterationLimit) {
210-
this.compiledRule = checkNotNull(compiledRule);
211-
this.variablePrefix = variablePrefix;
212-
this.astMutator = AstMutator.newInstance(iterationLimit);
259+
// Step represents an intermediate stage of rule and match expression composition.
260+
//
261+
// The CelCompiledRule and CelCompiledMatch types are meant to represent standalone tuples of
262+
// condition and output expressions, and have no notion of how the order of combination would
263+
// impact composition since composition rules may vary based on the policy execution semantic,
264+
// e.g. first-match versus logical-or, logical-and, or accumulation.
265+
private static class Step {
266+
/**
267+
* Indicates whether the output step has an optional result. Individual conditional attributes
268+
* are not optional; however, rules and subrules can have optional output.
269+
*/
270+
private final boolean isOptional;
271+
272+
/** True if the condition expression is not trivially true. */
273+
private final boolean isConditional;
274+
275+
/** The condition associated with the output. */
276+
private final CelMutableAst cond;
277+
278+
/** The output expression for the step. */
279+
private final CelMutableAst expr;
280+
281+
private Step(
282+
boolean isOptional, boolean isConditional, CelMutableAst cond, CelMutableAst expr) {
283+
this.isOptional = isOptional;
284+
this.isConditional = isConditional;
285+
this.cond = cond;
286+
this.expr = expr;
287+
}
213288
}
214289

215290
static final class RuleCompositionException extends RuntimeException {
@@ -225,4 +300,10 @@ private RuleCompositionException(
225300
this.compileException = e;
226301
}
227302
}
303+
304+
private RuleComposer(CelCompiledRule compiledRule, String variablePrefix, int iterationLimit) {
305+
this.compiledRule = checkNotNull(compiledRule);
306+
this.variablePrefix = variablePrefix;
307+
this.astMutator = AstMutator.newInstance(iterationLimit);
308+
}
228309
}

policy/src/test/java/dev/cel/policy/CelPolicyCompilerImplTest.java

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -266,8 +266,8 @@ public void evaluateYamlPolicy_nestedRuleProducesOptionalOutput() throws Excepti
266266
CelPolicyCompilerFactory.newPolicyCompiler(cel).build().compile(policy);
267267
Optional<Object> evalResult = (Optional<Object>) cel.createProgram(compiledPolicyAst).eval();
268268

269-
// Result is Optional<Optional<Object>>
270-
assertThat(evalResult).hasValue(Optional.of(true));
269+
// Result is Optional<Object> containing true
270+
assertThat(evalResult).hasValue(true);
271271
}
272272

273273
@Test

policy/src/test/java/dev/cel/policy/PolicyTestHelper.java

Lines changed: 19 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -40,11 +40,11 @@ final class PolicyTestHelper {
4040
enum TestYamlPolicy {
4141
NESTED_RULE(
4242
"nested_rule",
43-
true,
43+
false,
4444
"cel.@block([resource.origin, @index0 in [\"us\", \"uk\", \"es\"], {\"banned\": true}],"
4545
+ " ((@index0 in {\"us\": false, \"ru\": false, \"ir\": false} && !@index1) ?"
46-
+ " optional.of(@index2) : optional.none()).or(optional.of(@index1 ? {\"banned\":"
47-
+ " false} : @index2)))"),
46+
+ " optional.of(@index2) : optional.none()).orValue(@index1 ? {\"banned\":"
47+
+ " false} : @index2))"),
4848
NESTED_RULE2(
4949
"nested_rule2",
5050
false,
@@ -61,6 +61,22 @@ enum TestYamlPolicy {
6161
+ " false, \"ru\": false, \"ir\": false} && @index1) ? {\"banned\":"
6262
+ " \"restricted_region\"} : {\"banned\": \"bad_actor\"}) : (@index1 ?"
6363
+ " optional.of({\"banned\": \"unconfigured_region\"}) : optional.none()))"),
64+
NESTED_RULE4("nested_rule4", false, "(x > 0) ? true : false"),
65+
NESTED_RULE5(
66+
"nested_rule5",
67+
true,
68+
"cel.@block([optional.of(true), optional.none()], (x > 0) ? ((x > 2) ? @index0 : @index1) :"
69+
+ " ((x > 1) ? ((x >= 2) ? @index0 : @index1) : optional.of(false)))"),
70+
NESTED_RULE6(
71+
"nested_rule6",
72+
false,
73+
"cel.@block([optional.of(true), optional.none()], ((x > 2) ? @index0 : @index1).orValue(((x"
74+
+ " > 3) ? @index0 : @index1).orValue(false)))"),
75+
NESTED_RULE7(
76+
"nested_rule7",
77+
true,
78+
"cel.@block([optional.of(true), optional.none()], ((x > 2) ? @index0 : @index1).or(((x > 3)"
79+
+ " ? @index0 : @index1).or((x > 1) ? optional.of(false) : @index1)))"),
6480
REQUIRED_LABELS(
6581
"required_labels",
6682
true,
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
# Copyright 2024 Google LLC
2+
#
3+
# Licensed under the Apache License, Version 2.0 (the "License");
4+
# you may not use this file except in compliance with the License.
5+
# You may obtain a copy of the License at
6+
#
7+
# https://www.apache.org/licenses/LICENSE-2.0
8+
#
9+
# Unless required by applicable law or agreed to in writing, software
10+
# distributed under the License is distributed on an "AS IS" BASIS,
11+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
# See the License for the specific language governing permissions and
13+
# limitations under the License.
14+
15+
name: "nested_rule4"
16+
variables:
17+
- name: x
18+
type:
19+
type_name: int

0 commit comments

Comments
 (0)