Skip to content

Commit 533df33

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

15 files changed

Lines changed: 530 additions & 83 deletions

File tree

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

Lines changed: 195 additions & 78 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,58 @@ 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;
77-
// Keep track of the last output ID that might cause type-check failure while attempting to
78-
// compose the subgraphs.
64+
Step output = null;
65+
// If the rule has an optional output, the last result in the ternary should return
66+
// `optional.none`. This output is implicit and created here to reflect the desired
67+
// last possible output of this type of rule.
68+
if (compiledRule.hasOptionalOutput()) {
69+
output =
70+
Step.newUnconditionalOptionalStep(
71+
newTrueAst(cel), astMutator.newGlobalCall(Function.OPTIONAL_NONE.getFunction()));
72+
}
73+
7974
long lastOutputId = 0;
8075
for (CelCompiledMatch match : Lists.reverse(compiledRule.matches())) {
8176
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.
8577
boolean isTriviallyTrue = match.isConditionTriviallyTrue();
78+
CelMutableAst condAst = CelMutableAst.fromCelAst(conditionAst);
8679

8780
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.
9081
case OUTPUT:
82+
// If the output is non-nil, then it is considered a non-optional output since
83+
// it is explicitly stated. If the rule itself is optional, then the base case value
84+
// of output being optional.none() will convert the non-optional value to an optional
85+
// one.
9186
OutputValue matchOutput = match.result().output();
9287
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-
}
102-
103-
matchAst =
104-
astMutator.newGlobalCall(
105-
Operator.CONDITIONAL.getFunction(),
106-
CelMutableAst.fromCelAst(conditionAst),
107-
outAst,
108-
matchAst);
88+
Step step = Step.newNonOptionalStep(!isTriviallyTrue, condAst, outAst);
89+
output = combine(cel, astMutator, step, output);
90+
10991
assertComposedAstIsValid(
11092
cel,
111-
matchAst,
93+
output.expr,
11294
"conflicting output types found.",
11395
matchOutput.sourceId(),
11496
lastOutputId);
97+
11598
lastOutputId = matchOutput.sourceId();
11699
continue;
117100
case RULE:
118101
// If the match has a nested rule, then compute the rule and whether it has
119102
// an optional return value.
120103
CelCompiledRule matchNestedRule = match.result().rule();
121-
RuleOptimizationResult nestedRule = optimizeRule(cel, matchNestedRule);
104+
Step nestedRule = optimizeRule(cel, matchNestedRule);
122105
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-
}
106+
107+
Step ruleStep =
108+
nestedHasOptional
109+
? Step.newOptionalStep(!isTriviallyTrue, condAst, nestedRule.expr)
110+
: Step.newNonOptionalStep(!isTriviallyTrue, condAst, nestedRule.expr);
111+
output = combine(cel, astMutator, ruleStep, output);
146112

147113
assertComposedAstIsValid(
148114
cel,
149-
matchAst,
115+
output.expr,
150116
String.format(
151117
"failed composing the subrule '%s' due to conflicting output types.",
152118
matchNestedRule.ruleId().map(ValueString::value).orElse("")),
@@ -155,11 +121,116 @@ private RuleOptimizationResult optimizeRule(Cel cel, CelCompiledRule compiledRul
155121
}
156122
}
157123

158-
CelMutableAst result = inlineCompiledVariables(matchAst, compiledRule.variables());
124+
CelMutableAst resultExpr = output.expr;
125+
resultExpr = inlineCompiledVariables(resultExpr, compiledRule.variables());
126+
resultExpr = astMutator.renumberIdsConsecutively(resultExpr);
127+
128+
return output.isOptional
129+
? Step.newUnconditionalOptionalStep(newTrueAst(cel), resultExpr)
130+
: Step.newUnconditionalNonOptionalStep(newTrueAst(cel), resultExpr);
131+
}
132+
133+
static RuleComposer newInstance(
134+
CelCompiledRule compiledRule, String variablePrefix, int iterationLimit) {
135+
return new RuleComposer(compiledRule, variablePrefix, iterationLimit);
136+
}
137+
138+
// Assembles two output expressions into a single output step.
139+
private Step combine(Cel cel, AstMutator astMutator, Step s, Step step) {
140+
if (step == null) {
141+
return s;
142+
}
143+
CelMutableAst trueCondition = newTrueAst(cel);
144+
145+
if (s.isOptional) {
146+
return combineWhenCurrentIsOptional(s, step, astMutator, trueCondition);
147+
} else {
148+
return combineWhenCurrentIsNonOptional(s, step, astMutator, trueCondition);
149+
}
150+
}
159151

160-
result = astMutator.renumberIdsConsecutively(result);
152+
private Step combineWhenCurrentIsOptional(
153+
Step s, Step step, AstMutator astMutator, CelMutableAst trueCondition) {
154+
// optional.combine(optional) // optional
155+
// (optional && conditional).combine(non-optional) // optional
156+
// (optional && unconditional).combine(non-optional) // non-optional
157+
if (step.isOptional) {
158+
if (s.isConditional) {
159+
return Step.newUnconditionalOptionalStep(
160+
trueCondition,
161+
astMutator.newGlobalCall(
162+
Operator.CONDITIONAL.getFunction(), s.cond, s.expr, step.expr));
163+
} else {
164+
if (!isOptionalNone(step.expr)) {
165+
// If either the nested rule or current condition output are optional then
166+
// use optional.or() to specify the combination of the first and second results
167+
// Note, the argument order is reversed due to the traversal of matches in
168+
// reverse order.
169+
return Step.newUnconditionalOptionalStep(
170+
trueCondition, astMutator.newMemberCall(s.expr, "or", step.expr));
171+
}
172+
return s;
173+
}
174+
} else { // step is non-optional
175+
if (s.isConditional) {
176+
return Step.newUnconditionalOptionalStep(
177+
trueCondition,
178+
astMutator.newGlobalCall(
179+
Operator.CONDITIONAL.getFunction(),
180+
s.cond,
181+
s.expr,
182+
astMutator.newGlobalCall(Function.OPTIONAL_OF.getFunction(), step.expr)));
183+
} else {
184+
return Step.newUnconditionalNonOptionalStep(
185+
trueCondition, astMutator.newMemberCall(s.expr, "orValue", step.expr));
186+
}
187+
}
188+
}
161189

162-
return RuleOptimizationResult.create(result, isOptionalResult);
190+
private Step combineWhenCurrentIsNonOptional(
191+
Step s, Step step, AstMutator astMutator, CelMutableAst trueCondition) {
192+
// non-optional.combine(non-optional) // non-optional
193+
// (non-optional && conditional).combine(optional) // optional
194+
// (non-optional && unconditional).combine(optional) // non-optional
195+
//
196+
// The last combination case is unusual, but effectively it means that the non-optional value
197+
// prunes away
198+
// the potential optional output.
199+
if (step.isOptional) {
200+
if (s.isConditional) {
201+
return Step.newUnconditionalOptionalStep(
202+
trueCondition,
203+
astMutator.newGlobalCall(
204+
Operator.CONDITIONAL.getFunction(),
205+
s.cond,
206+
astMutator.newGlobalCall(Function.OPTIONAL_OF.getFunction(), s.expr),
207+
step.expr));
208+
} else {
209+
// If the condition is trivially true, none of the matches in the rule causes the result
210+
// to become optional, and the rule is not the last match, then this will introduce
211+
// unreachable outputs or rules (pruning away 'step').
212+
return s;
213+
}
214+
} else { // step is non-optional
215+
return Step.newUnconditionalNonOptionalStep(
216+
trueCondition,
217+
astMutator.newGlobalCall(Operator.CONDITIONAL.getFunction(), s.cond, s.expr, step.expr));
218+
}
219+
}
220+
221+
private static boolean isOptionalNone(CelMutableAst ast) {
222+
CelExpr expr = ast.toParsedAst().getExpr();
223+
return expr.getKind().equals(Kind.CALL)
224+
&& expr.call().function().equals("optional.none")
225+
&& expr.call().args().isEmpty();
226+
}
227+
228+
private static CelMutableAst newTrueAst(Cel cel) {
229+
try {
230+
return CelMutableAst.fromCelAst(cel.compile("true").getAst());
231+
} catch (CelValidationException e) {
232+
throw new IllegalStateException("Failed to compile 'true'", e);
233+
}
163234
}
164235

165236
private CelMutableAst inlineCompiledVariables(
@@ -186,11 +257,6 @@ private CelMutableAst inlineCompiledVariables(
186257
return mutatedAst;
187258
}
188259

189-
static RuleComposer newInstance(
190-
CelCompiledRule compiledRule, String variablePrefix, int iterationLimit) {
191-
return new RuleComposer(compiledRule, variablePrefix, iterationLimit);
192-
}
193-
194260
private void assertComposedAstIsValid(
195261
Cel cel, CelMutableAst composedAst, String failureMessage, Long... ids) {
196262
assertComposedAstIsValid(cel, composedAst, failureMessage, Arrays.asList(ids));
@@ -206,10 +272,55 @@ private void assertComposedAstIsValid(
206272
}
207273
}
208274

209-
private RuleComposer(CelCompiledRule compiledRule, String variablePrefix, int iterationLimit) {
210-
this.compiledRule = checkNotNull(compiledRule);
211-
this.variablePrefix = variablePrefix;
212-
this.astMutator = AstMutator.newInstance(iterationLimit);
275+
// Step represents an intermediate stage of rule and match expression composition.
276+
//
277+
// The CelCompiledRule and CelCompiledMatch types are meant to represent standalone tuples of
278+
// condition and output expressions, and have no notion of how the order of combination would
279+
// impact composition since composition rules may vary based on the policy execution semantic,
280+
// e.g. first-match versus logical-or, logical-and, or accumulation.
281+
private static class Step {
282+
/**
283+
* Indicates whether the output step has an optional result. Individual conditional attributes
284+
* are not optional; however, rules and subrules can have optional output.
285+
*/
286+
private final boolean isOptional;
287+
288+
/** True if the condition expression is not trivially true. */
289+
private final boolean isConditional;
290+
291+
/** The condition associated with the output. */
292+
private final CelMutableAst cond;
293+
294+
/** The output expression for the step. */
295+
private final CelMutableAst expr;
296+
297+
private Step(
298+
boolean isOptional, boolean isConditional, CelMutableAst cond, CelMutableAst expr) {
299+
this.isOptional = isOptional;
300+
this.isConditional = isConditional;
301+
this.cond = cond;
302+
this.expr = expr;
303+
}
304+
305+
private static Step newOptionalStep(
306+
boolean isConditional, CelMutableAst cond, CelMutableAst expr) {
307+
return new Step(/* isOptional= */ true, isConditional, cond, expr);
308+
}
309+
310+
private static Step newNonOptionalStep(
311+
boolean isConditional, CelMutableAst cond, CelMutableAst expr) {
312+
return new Step(/* isOptional= */ false, isConditional, cond, expr);
313+
}
314+
315+
private static Step newUnconditionalOptionalStep(
316+
CelMutableAst trueCondition, CelMutableAst expr) {
317+
return new Step(/* isOptional= */ true, /* isConditional= */ false, trueCondition, expr);
318+
}
319+
320+
private static Step newUnconditionalNonOptionalStep(
321+
CelMutableAst trueCondition, CelMutableAst expr) {
322+
return new Step(/* isOptional= */ false, /* isConditional= */ false, trueCondition, expr);
323+
}
213324
}
214325

215326
static final class RuleCompositionException extends RuntimeException {
@@ -225,4 +336,10 @@ private RuleCompositionException(
225336
this.compileException = e;
226337
}
227338
}
339+
340+
private RuleComposer(CelCompiledRule compiledRule, String variablePrefix, int iterationLimit) {
341+
this.compiledRule = checkNotNull(compiledRule);
342+
this.variablePrefix = variablePrefix;
343+
this.astMutator = AstMutator.newInstance(iterationLimit);
344+
}
228345
}

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

0 commit comments

Comments
 (0)