diff --git a/core/src/main/java/org/projectnessie/cel/interpreter/Interpretable.java b/core/src/main/java/org/projectnessie/cel/interpreter/Interpretable.java index 3e332db8..78aa621e 100644 --- a/core/src/main/java/org/projectnessie/cel/interpreter/Interpretable.java +++ b/core/src/main/java/org/projectnessie/cel/interpreter/Interpretable.java @@ -19,9 +19,11 @@ import static org.projectnessie.cel.common.types.BoolT.True; import static org.projectnessie.cel.common.types.Err.isError; import static org.projectnessie.cel.common.types.Err.newErr; +import static org.projectnessie.cel.common.types.Err.noSuchAttributeException; import static org.projectnessie.cel.common.types.Err.noSuchOverload; import static org.projectnessie.cel.common.types.Err.valOrErr; import static org.projectnessie.cel.common.types.UnknownT.isUnknown; +import static org.projectnessie.cel.common.types.UnknownT.unknownOf; import static org.projectnessie.cel.common.types.Util.isUnknownOrError; import static org.projectnessie.cel.interpreter.Activation.emptyActivation; import static org.projectnessie.cel.interpreter.Coster.Cost.OneOne; @@ -54,6 +56,7 @@ import org.projectnessie.cel.common.types.traits.Receiver; import org.projectnessie.cel.common.types.traits.Sizer; import org.projectnessie.cel.common.types.traits.Trait; +import org.projectnessie.cel.interpreter.Activation.PartialActivation; import org.projectnessie.cel.interpreter.Activation.VarActivation; import org.projectnessie.cel.interpreter.AttributeFactory.Attribute; import org.projectnessie.cel.interpreter.AttributeFactory.ConditionalAttribute; @@ -143,6 +146,48 @@ interface InterpretableCall extends Interpretable { // Core Interpretable implementations used during the program planning phase. + /** evalIdent evaluates a checked top-level variable directly from the activation. */ + final class EvalIdent extends AbstractEval implements Coster { + private final String name; + private final TypeAdapter adapter; + + EvalIdent(long id, String name, TypeAdapter adapter) { + super(id); + this.name = Objects.requireNonNull(name); + this.adapter = Objects.requireNonNull(adapter); + } + + /** Eval implements the Interpretable interface method. */ + @Override + public Val eval(Activation ctx) { + if (ctx instanceof PartialActivation) { + for (AttributePattern pattern : ((PartialActivation) ctx).unknownAttributePatterns()) { + if (pattern.variableMatches(name)) { + return unknownOf(id); + } + } + } + + ResolvedValue value = ctx.resolveName(name); + if (!value.present()) { + RuntimeException err = noSuchAttributeException("id: " + id + ", names: [" + name + "]"); + return newErr(err, err.toString()); + } + return adapter.nativeToValue(value.value()); + } + + /** Cost implements the Coster interface method. */ + @Override + public Cost cost() { + return OneOne; + } + + @Override + public String toString() { + return "EvalIdent{" + "id=" + id + ", name='" + name + '\'' + '}'; + } + } + final class EvalTestOnly implements Interpretable, Coster { private final long id; private final Interpretable op; diff --git a/core/src/main/java/org/projectnessie/cel/interpreter/InterpretablePlanner.java b/core/src/main/java/org/projectnessie/cel/interpreter/InterpretablePlanner.java index 25c522ed..6fee2a25 100644 --- a/core/src/main/java/org/projectnessie/cel/interpreter/InterpretablePlanner.java +++ b/core/src/main/java/org/projectnessie/cel/interpreter/InterpretablePlanner.java @@ -55,6 +55,7 @@ import org.projectnessie.cel.interpreter.Interpretable.EvalBinary; import org.projectnessie.cel.interpreter.Interpretable.EvalEq; import org.projectnessie.cel.interpreter.Interpretable.EvalFold; +import org.projectnessie.cel.interpreter.Interpretable.EvalIdent; import org.projectnessie.cel.interpreter.Interpretable.EvalList; import org.projectnessie.cel.interpreter.Interpretable.EvalListFold; import org.projectnessie.cel.interpreter.Interpretable.EvalMap; @@ -238,7 +239,11 @@ Interpretable planCheckedIdent(long id, Reference identRef) { return newConstValue(id, cVal); } - // Otherwise, return the attribute for the resolved identifier name. + // Otherwise, evaluate the checked top-level variable directly for ordinary plans. Decorated + // programs keep the attribute shape because custom decorators may inspect attributes. + if (decorators.length == 0) { + return new EvalIdent(id, identRef.getName(), adapter); + } return new EvalAttr(adapter, attrFactory.absoluteAttribute(id, identRef.getName())); } @@ -261,7 +266,8 @@ Interpretable planSelect(Expr expr) { Select sel = expr.getSelectExpr(); // Plan the operand evaluation. - Interpretable op = plan(sel.getOperand()); + Interpretable op = + decorators.length == 0 ? planSelectOperand(sel.getOperand()) : plan(sel.getOperand()); // Determine the field type if this is a proto message type. FieldType fieldType = null; @@ -314,6 +320,25 @@ Interpretable planSelect(Expr expr) { return relAttr; } + private Interpretable planSelectOperand(Expr operand) { + if (operand.getExprKindCase() != Expr.ExprKindCase.IDENT_EXPR) { + return plan(operand); + } + + Reference identRef = refMap.get(operand.getId()); + if (identRef == null || identRef.getValue() != Reference.getDefaultInstance().getValue()) { + return plan(operand); + } + + Type cType = typeMap.get(operand.getId()); + if (cType != null && cType.getType() != Type.getDefaultInstance()) { + return plan(operand); + } + + return new EvalAttr( + adapter, attrFactory.absoluteAttribute(operand.getId(), identRef.getName())); + } + private FieldType findFieldType(String messageType, String fieldName) { String key = messageType + '\n' + fieldName; FieldType ft = fieldTypes.get(key); diff --git a/core/src/test/java/org/projectnessie/cel/interpreter/InterpreterTest.java b/core/src/test/java/org/projectnessie/cel/interpreter/InterpreterTest.java index beb18580..9cc589bb 100644 --- a/core/src/test/java/org/projectnessie/cel/interpreter/InterpreterTest.java +++ b/core/src/test/java/org/projectnessie/cel/interpreter/InterpreterTest.java @@ -1579,6 +1579,54 @@ void missingIdentInSelect() { assertThat(result).isInstanceOf(Err.class); } + @Test + void checkedTopLevelIdentPreservesActivationSemantics() { + Source src = newTextSource("x"); + ParseResult parsed = Parser.parseAllMacros(src); + assertThat(parsed.hasErrors()).withFailMessage(parsed.getErrors()::toDisplayString).isFalse(); + + Container cont = testContainer("test"); + TypeRegistry reg = newRegistry(); + CheckerEnv env = newStandardCheckerEnv(cont, reg); + env.add(Decls.newVar("x", Decls.Bool)); + CheckResult checkResult = Checker.Check(parsed, src, env); + + AttributeFactory attrs = newPartialAttributeFactory(cont, reg, reg); + Interpreter interp = newStandardInterpreter(cont, reg, reg, attrs); + Interpretable i = interp.newInterpretable(checkResult.getCheckedExpr()); + + assertThat(i.eval(newActivation(mapOf("x", true)))).isSameAs(True); + assertThat(i.eval(emptyActivation())).isInstanceOf(Err.class); + assertThat(i.eval(newPartialActivation(mapOf("x", true), newAttributePattern("x")))) + .isInstanceOf(UnknownT.class); + } + + @Test + void checkedTopLevelIdentSelectPreservesTrackedState() { + Source src = newTextSource("a.b"); + ParseResult parsed = Parser.parseAllMacros(src); + assertThat(parsed.hasErrors()).withFailMessage(parsed.getErrors()::toDisplayString).isFalse(); + + Container cont = testContainer("test"); + TypeRegistry reg = newRegistry(); + CheckerEnv env = newStandardCheckerEnv(cont, reg); + env.add(Decls.newVar("a", Decls.newMapType(Decls.String, Decls.Bool))); + CheckResult checkResult = Checker.Check(parsed, src, env); + + Expr select = checkResult.getCheckedExpr().getExpr(); + long selectId = select.getId(); + long operandId = select.getSelectExpr().getOperand().getId(); + EvalState state = newEvalState(); + AttributeFactory attrs = newAttributeFactory(cont, reg, reg); + Interpreter interp = newStandardInterpreter(cont, reg, reg, attrs); + Interpretable i = interp.newInterpretable(checkResult.getCheckedExpr(), trackState(state)); + + assertThat(i.eval(newActivation(mapOf("a", mapOf("b", true))))).isSameAs(True); + assertThat(state.ids()).containsExactlyInAnyOrder(operandId, selectId); + assertThat(state.value(operandId)).isSameAs(True); + assertThat(state.value(selectId)).isSameAs(True); + } + @Test void equalityWithUnknownOperandStaysUnknown() { assertThat(evalWithUnknownStringX("x == 'foo'")).isInstanceOf(UnknownT.class);