diff --git a/runtime/src/main/java/dev/cel/runtime/AccumulatedUnknowns.java b/runtime/src/main/java/dev/cel/runtime/AccumulatedUnknowns.java index d4d54c71f..e512b38ca 100644 --- a/runtime/src/main/java/dev/cel/runtime/AccumulatedUnknowns.java +++ b/runtime/src/main/java/dev/cel/runtime/AccumulatedUnknowns.java @@ -19,6 +19,7 @@ import java.util.ArrayList; import java.util.Arrays; import java.util.Collection; +import java.util.Collections; import java.util.HashSet; import java.util.Set; import org.jspecify.annotations.Nullable; @@ -36,12 +37,12 @@ public final class AccumulatedUnknowns { private final Set exprIds; private final Set attributes; - Set exprIds() { - return exprIds; + public Set exprIds() { + return Collections.unmodifiableSet(exprIds); } - Set attributes() { - return attributes; + public Set attributes() { + return Collections.unmodifiableSet(attributes); } /** diff --git a/runtime/src/main/java/dev/cel/runtime/CelRuntimeImpl.java b/runtime/src/main/java/dev/cel/runtime/CelRuntimeImpl.java index f934108e0..6f681bb71 100644 --- a/runtime/src/main/java/dev/cel/runtime/CelRuntimeImpl.java +++ b/runtime/src/main/java/dev/cel/runtime/CelRuntimeImpl.java @@ -235,7 +235,16 @@ public Object trace(PartialVars partialVars, CelEvaluationListener listener) @Override public Object advanceEvaluation(UnknownContext context) throws CelEvaluationException { - throw new UnsupportedOperationException("Unsupported operation."); + PlannedProgram plannedProgram = (PlannedProgram) program; + return plannedProgram.evalOrThrow( + plannedProgram.interpretable(), + context.variableResolver(), + EMPTY_FUNCTION_RESOLVER, + PartialVars.of( + (name) -> Optional.ofNullable(context.variableResolver().resolve(name)), + context.unresolvedAttributes()), + context.createAttributeResolver(), + /* listener= */ null); } }; } diff --git a/runtime/src/main/java/dev/cel/runtime/UnknownContext.java b/runtime/src/main/java/dev/cel/runtime/UnknownContext.java index c494ff252..92408e543 100644 --- a/runtime/src/main/java/dev/cel/runtime/UnknownContext.java +++ b/runtime/src/main/java/dev/cel/runtime/UnknownContext.java @@ -107,6 +107,16 @@ public GlobalResolver variableResolver() { return variableResolver; } + /** Accessor for unresolved attribute patterns. */ + ImmutableList unresolvedAttributes() { + return unresolvedAttributes; + } + + /** Accessor for resolved attribute values. */ + ImmutableMap resolvedAttributes() { + return resolvedAttributes; + } + /** * Creates a new unknown context that is a copy of the current context with the provided * additional attribute values. @@ -168,10 +178,27 @@ public Optional resolve(CelAttribute attribute) { @Override public Optional maybePartialUnknown(CelAttribute attribute) { - return unresolvedAttributes.stream() - .filter(p -> p.isPartialMatch(attribute)) - .findFirst() - .map(p -> CelUnknownSet.create(p.simplify(attribute))); + if (attribute.equals(CelAttribute.EMPTY) || attribute.qualifiers().isEmpty()) { + return Optional.empty(); + } + Optional fromUnresolved = + unresolvedAttributes.stream() + .filter(p -> p.isPartialMatch(attribute)) + .findFirst() + .map(p -> CelUnknownSet.create(p.simplify(attribute))); + if (fromUnresolved.isPresent()) { + return fromUnresolved; + } + for (CelAttribute resolved : resolvedAttributes.keySet()) { + if (resolved.qualifiers().size() > attribute.qualifiers().size() + && resolved + .qualifiers() + .subList(0, attribute.qualifiers().size()) + .equals(attribute.qualifiers())) { + return Optional.of(CelUnknownSet.create(attribute)); + } + } + return Optional.empty(); } } } diff --git a/runtime/src/main/java/dev/cel/runtime/planner/BUILD.bazel b/runtime/src/main/java/dev/cel/runtime/planner/BUILD.bazel index e05fca9b4..8d0dda680 100644 --- a/runtime/src/main/java/dev/cel/runtime/planner/BUILD.bazel +++ b/runtime/src/main/java/dev/cel/runtime/planner/BUILD.bazel @@ -89,6 +89,7 @@ java_library( "//runtime:partial_vars", "//runtime:program", "//runtime:resolved_overload", + "//runtime:unknown_attributes", "//runtime:variable_resolver", "@maven//:com_google_errorprone_error_prone_annotations", "@maven//:org_jspecify_jspecify", @@ -131,6 +132,7 @@ java_library( "//common/types:type_providers", "//common/values", "//runtime:accumulated_unknowns", + "//runtime:evaluation_exception", "//runtime:interpretable", "//runtime:interpreter_util", "//runtime:partial_vars", @@ -221,12 +223,17 @@ java_library( deps = [ ":eval_helpers", ":planned_interpretable", + "//common:operator", "//common/ast", "//common/values", "//runtime:accumulated_unknowns", "//runtime:evaluation_exception", "//runtime:interpretable", + "//runtime:interpreter_util", + "//runtime:partial_vars", "//runtime:resolved_overload", + "//runtime:unknown_attributes", + "@maven//:com_google_guava_guava", ], ) @@ -518,6 +525,7 @@ java_library( "//runtime:interpreter_util", "//runtime:partial_vars", "//runtime:resolved_overload", + "//runtime:unknown_attributes", "@maven//:com_google_errorprone_error_prone_annotations", "@maven//:org_jspecify_jspecify", ], @@ -596,6 +604,7 @@ cel_android_library( "//runtime:evaluation_exception_builder", "//runtime:interpretable_android", "//runtime:resolved_overload_android", + "//runtime:unknown_attributes_android", "//runtime:variable_resolver", "//runtime/src/main/java/dev/cel/runtime:evaluation_listener_android", "//runtime/src/main/java/dev/cel/runtime:function_resolver_android", @@ -642,6 +651,7 @@ cel_android_library( "//common/types:type_providers_android", "//common/types:types_android", "//common/values:values_android", + "//runtime:evaluation_exception", "//runtime:interpretable_android", "//runtime:unknown_attributes_android", "//runtime/src/main/java/dev/cel/runtime:accumulated_unknowns_android", @@ -733,12 +743,17 @@ cel_android_library( deps = [ ":eval_helpers_android", ":planned_interpretable_android", + "//common:operator_android", "//common/ast:ast_android", "//common/values:values_android", "//runtime:evaluation_exception", "//runtime:interpretable_android", + "//runtime:interpreter_util_android", + "//runtime:partial_vars_android", "//runtime:resolved_overload_android", + "//runtime:unknown_attributes_android", "//runtime/src/main/java/dev/cel/runtime:accumulated_unknowns_android", + "@maven_android//:com_google_guava_guava", ], ) @@ -1023,6 +1038,7 @@ cel_android_library( "//runtime:interpretable_android", "//runtime:interpreter_util_android", "//runtime:resolved_overload_android", + "//runtime:unknown_attributes_android", "//runtime/src/main/java/dev/cel/runtime:function_resolver_android", "//runtime/src/main/java/dev/cel/runtime:partial_vars_android", "@maven//:com_google_errorprone_error_prone_annotations", diff --git a/runtime/src/main/java/dev/cel/runtime/planner/EvalBinary.java b/runtime/src/main/java/dev/cel/runtime/planner/EvalBinary.java index 1713195ab..adae91806 100644 --- a/runtime/src/main/java/dev/cel/runtime/planner/EvalBinary.java +++ b/runtime/src/main/java/dev/cel/runtime/planner/EvalBinary.java @@ -17,12 +17,23 @@ import static dev.cel.runtime.planner.EvalHelpers.evalNonstrictly; import static dev.cel.runtime.planner.EvalHelpers.evalStrictly; +import com.google.common.collect.ImmutableList; +import com.google.common.primitives.UnsignedLong; +import dev.cel.common.Operator; import dev.cel.common.ast.CelExpr; import dev.cel.common.values.CelValueConverter; +import dev.cel.common.values.ErrorValue; import dev.cel.runtime.AccumulatedUnknowns; +import dev.cel.runtime.CelAttribute; +import dev.cel.runtime.CelAttributePattern; +import dev.cel.runtime.CelAttributeResolver; import dev.cel.runtime.CelEvaluationException; import dev.cel.runtime.CelResolvedOverload; import dev.cel.runtime.GlobalResolver; +import dev.cel.runtime.InterpreterUtil; +import dev.cel.runtime.PartialVars; +import java.util.Optional; +import org.jspecify.annotations.Nullable; final class EvalBinary extends PlannedInterpretable { @@ -39,7 +50,13 @@ Object evalInternal(GlobalResolver resolver, ExecutionFrame frame) throws CelEva isStrict ? evalStrictly(arg1, resolver, frame) : evalNonstrictly(arg1, resolver, frame); Object argVal2 = isStrict ? evalStrictly(arg2, resolver, frame) : evalNonstrictly(arg2, resolver, frame); + if (isStrict) { + Object indexUnknownResult = maybeEvaluateIndexUnknown(argVal1, argVal2, frame); + if (indexUnknownResult != null) { + return indexUnknownResult; + } + AccumulatedUnknowns unknowns = AccumulatedUnknowns.maybeMerge(null, argVal1); unknowns = AccumulatedUnknowns.maybeMerge(unknowns, argVal2); if (unknowns != null) { @@ -51,6 +68,78 @@ Object evalInternal(GlobalResolver resolver, ExecutionFrame frame) throws CelEva functionName, resolvedOverload, celValueConverter, argVal1, argVal2); } + private @Nullable Object maybeEvaluateIndexUnknown( + Object target, Object index, ExecutionFrame frame) throws CelEvaluationException { + if (!Operator.INDEX.getFunction().equals(functionName) + || !(target instanceof AccumulatedUnknowns) + || target instanceof ErrorValue + || index instanceof AccumulatedUnknowns + || index instanceof ErrorValue) { + return null; + } + + Optional optionalQualifier = toQualifier(index); + if (!optionalQualifier.isPresent()) { + return null; + } + CelAttribute.Qualifier qualifier = optionalQualifier.get(); + + AccumulatedUnknowns targetUnknowns = (AccumulatedUnknowns) target; + CelAttributeResolver attributeResolver = frame.attributeResolver().orElse(null); + PartialVars partialVars = frame.partialVars().orElse(null); + + ImmutableList.Builder qualifiedAttrs = ImmutableList.builder(); + for (CelAttribute attr : targetUnknowns.attributes()) { + CelAttribute qualifiedAttr = attr.qualify(qualifier); + if (attributeResolver != null) { + Optional resolved = attributeResolver.resolve(qualifiedAttr); + if (resolved.isPresent()) { + return adaptResolvedValue(resolved.get()); + } + } + qualifiedAttrs.add(simplifyAttribute(qualifiedAttr, attr, partialVars)); + } + + return AccumulatedUnknowns.create(targetUnknowns.exprIds(), qualifiedAttrs.build()); + } + + private static CelAttribute simplifyAttribute( + CelAttribute qualifiedAttr, CelAttribute fallbackAttr, @Nullable PartialVars partialVars) { + if (partialVars == null) { + return qualifiedAttr; + } + for (CelAttributePattern pattern : partialVars.unknowns()) { + if (pattern.isPartialMatch(qualifiedAttr)) { + return pattern.simplify(qualifiedAttr); + } + } + return fallbackAttr; + } + + private static Optional toQualifier(Object value) { + if (value instanceof UnsignedLong) { + return Optional.of(CelAttribute.Qualifier.ofUint((UnsignedLong) value)); + } + if (value instanceof Long) { + return Optional.of(CelAttribute.Qualifier.ofInt((Long) value)); + } + if (value instanceof Integer) { + return Optional.of(CelAttribute.Qualifier.ofInt(((Integer) value).longValue())); + } + if (value instanceof Boolean) { + return Optional.of(CelAttribute.Qualifier.ofBool((Boolean) value)); + } + if (value instanceof String) { + return Optional.of(CelAttribute.Qualifier.ofString((String) value)); + } + return Optional.empty(); + } + + private static Object adaptResolvedValue(Object resolvedVal) throws CelEvaluationException { + resolvedVal = InterpreterUtil.strict(resolvedVal); + return InterpreterUtil.maybeAdaptToAccumulatedUnknowns(resolvedVal); + } + static EvalBinary create( CelExpr expr, String functionName, diff --git a/runtime/src/main/java/dev/cel/runtime/planner/ExecutionFrame.java b/runtime/src/main/java/dev/cel/runtime/planner/ExecutionFrame.java index b67f5520c..18312a86e 100644 --- a/runtime/src/main/java/dev/cel/runtime/planner/ExecutionFrame.java +++ b/runtime/src/main/java/dev/cel/runtime/planner/ExecutionFrame.java @@ -16,6 +16,7 @@ import dev.cel.common.CelOptions; import dev.cel.common.exceptions.CelIterationLimitExceededException; +import dev.cel.runtime.CelAttributeResolver; import dev.cel.runtime.CelEvaluationException; import dev.cel.runtime.CelEvaluationListener; import dev.cel.runtime.CelFunctionResolver; @@ -31,6 +32,7 @@ final class ExecutionFrame { private final int comprehensionIterationLimit; private final CelFunctionResolver functionResolver; private final PartialVars partialVars; + private final @Nullable CelAttributeResolver attributeResolver; private final @Nullable CelEvaluationListener listener; private int iterationCount; private BlockMemoizer blockMemoizer; @@ -68,15 +70,33 @@ static ExecutionFrame create( CelFunctionResolver functionResolver, CelOptions celOptions, @Nullable PartialVars partialVars, + @Nullable CelAttributeResolver attributeResolver, @Nullable CelEvaluationListener listener) { return new ExecutionFrame( - functionResolver, celOptions.comprehensionMaxIterations(), partialVars, listener); + functionResolver, + celOptions.comprehensionMaxIterations(), + partialVars, + attributeResolver, + listener); + } + + static ExecutionFrame create( + CelFunctionResolver functionResolver, + CelOptions celOptions, + @Nullable PartialVars partialVars, + @Nullable CelEvaluationListener listener) { + return create( + functionResolver, celOptions, partialVars, /* attributeResolver= */ null, listener); } Optional partialVars() { return Optional.ofNullable(partialVars); } + Optional attributeResolver() { + return Optional.ofNullable(attributeResolver); + } + @Nullable CelEvaluationListener getListener() { return listener; } @@ -85,10 +105,12 @@ private ExecutionFrame( CelFunctionResolver functionResolver, int limit, @Nullable PartialVars partialVars, + @Nullable CelAttributeResolver attributeResolver, @Nullable CelEvaluationListener listener) { this.comprehensionIterationLimit = limit; this.functionResolver = functionResolver; this.partialVars = partialVars; + this.attributeResolver = attributeResolver; this.listener = listener; } } diff --git a/runtime/src/main/java/dev/cel/runtime/planner/NamespacedAttribute.java b/runtime/src/main/java/dev/cel/runtime/planner/NamespacedAttribute.java index 01673923d..dc49ca79c 100644 --- a/runtime/src/main/java/dev/cel/runtime/planner/NamespacedAttribute.java +++ b/runtime/src/main/java/dev/cel/runtime/planner/NamespacedAttribute.java @@ -27,6 +27,9 @@ import dev.cel.runtime.AccumulatedUnknowns; import dev.cel.runtime.CelAttribute; import dev.cel.runtime.CelAttributePattern; +import dev.cel.runtime.CelAttributeResolver; +import dev.cel.runtime.CelEvaluationException; +import dev.cel.runtime.CelUnknownSet; import dev.cel.runtime.GlobalResolver; import dev.cel.runtime.InterpreterUtil; import dev.cel.runtime.PartialVars; @@ -63,37 +66,32 @@ public Object resolve(long exprId, GlobalResolver ctx, ExecutionFrame frame) { for (Map.Entry entry : candidateAttributes.entrySet()) { String name = entry.getKey(); - CelAttribute attr = entry.getValue(); + CelAttribute candidateAttr = entry.getValue(); + GlobalResolver resolver = disambiguateNames ? inputVars : ctx; - GlobalResolver resolver = ctx; - if (disambiguateNames) { - resolver = inputVars; - } - - Object value = resolver.resolve(name); - value = InterpreterUtil.maybeAdaptToAccumulatedUnknowns(value); + Object value; + if (!isLocallyBound(resolver, name)) { + CelAttribute fullyQualifiedAttr = qualify(candidateAttr, qualifiers); - PartialVars partialVars = frame.partialVars().orElse(null); - - if (partialVars != null && !isLocallyBound(resolver, name)) { - ImmutableList patterns = partialVars.unknowns(); - // Avoid enhanced for loop to prevent UnmodifiableIterator from being allocated - for (int i = 0; i < qualifiers.size(); i++) { - attr = attr.qualify(CelAttribute.Qualifier.fromGeneric(qualifiers.get(i).value())); + // Check if the fully-qualified attribute is resolved or unknown + Object fullyQualifiedResult = maybeResolveFullyQualified(exprId, fullyQualifiedAttr, frame); + if (fullyQualifiedResult != null) { + return fullyQualifiedResult; } - CelAttributePattern partialMatch = findPartialMatchingPattern(attr, patterns).orElse(null); - if (partialMatch != null) { - return AccumulatedUnknowns.create( - ImmutableList.of(exprId), ImmutableList.of(partialMatch.simplify(attr))); - } + // Resolve the base attribute (via iterative resolver or standard variable resolver) + value = maybeResolveBaseAttribute(exprId, candidateAttr, resolver, name, frame); + } else { + // Locally bound variable (e.g. comprehension variable) + Object rawValue = resolver.resolve(name); + value = rawValue != null ? adaptResolvedValue(rawValue) : null; } if (value != null) { return applyQualifiers(value, celValueConverter, qualifiers); } - // Attempt to resolve the qualify type name if the name is not a variable identifier + // Fallback: Attempt to resolve as a qualified type name or enum value value = findIdent(name); if (value != null) { return value; @@ -103,6 +101,67 @@ public Object resolve(long exprId, GlobalResolver ctx, ExecutionFrame frame) { return MissingAttribute.newMissingAttribute(candidateAttributes.keySet()); } + private static CelAttribute qualify(CelAttribute baseAttr, ImmutableList qualifiers) { + CelAttribute attr = baseAttr; + // Avoid enhanced for loop to prevent UnmodifiableIterator from being allocated + for (int i = 0; i < qualifiers.size(); i++) { + attr = attr.qualify(CelAttribute.Qualifier.fromGeneric(qualifiers.get(i).value())); + } + return attr; + } + + private static @Nullable Object maybeResolveFullyQualified( + long exprId, CelAttribute fullyQualifiedAttr, ExecutionFrame frame) { + // Check iterative eval AttributeResolver + CelAttributeResolver attributeResolver = frame.attributeResolver().orElse(null); + if (attributeResolver != null) { + Optional resolved = attributeResolver.resolve(fullyQualifiedAttr); + if (resolved.isPresent()) { + return adaptResolvedValue(resolved.get()); + } + } + + // Check batch PartialVars unknown patterns + PartialVars partialVars = frame.partialVars().orElse(null); + if (partialVars != null) { + ImmutableList patterns = partialVars.unknowns(); + CelAttributePattern partialMatch = + findPartialMatchingPattern(fullyQualifiedAttr, patterns).orElse(null); + if (partialMatch != null) { + return AccumulatedUnknowns.create( + ImmutableList.of(exprId), ImmutableList.of(partialMatch.simplify(fullyQualifiedAttr))); + } + } + + return null; + } + + private static @Nullable Object maybeResolveBaseAttribute( + long exprId, + CelAttribute candidateAttr, + GlobalResolver resolver, + String name, + ExecutionFrame frame) { + // Check iterative eval AttributeResolver + CelAttributeResolver attributeResolver = frame.attributeResolver().orElse(null); + if (attributeResolver != null) { + Optional baseResolved = attributeResolver.resolve(candidateAttr); + if (baseResolved.isPresent()) { + return adaptResolvedValue(baseResolved.get()); + } + + Optional partialUnknown = attributeResolver.maybePartialUnknown(candidateAttr); + if (partialUnknown.isPresent()) { + return AccumulatedUnknowns.create( + ImmutableList.of(exprId), partialUnknown.get().attributes()); + } + } + + // Standard variable resolution + Object rawValue = resolver.resolve(name); + return rawValue != null ? adaptResolvedValue(rawValue) : null; + } + private @Nullable Object findIdent(String name) { CelType type = typeProvider.findType(name).orElse(null); // If the name resolves directly, this is a fully qualified type name @@ -233,6 +292,15 @@ static NamespacedAttribute create( ImmutableList.of()); } + private static Object adaptResolvedValue(Object resolvedVal) { + try { + resolvedVal = InterpreterUtil.strict(resolvedVal); + } catch (CelEvaluationException e) { + throw new RuntimeException(e); + } + return InterpreterUtil.maybeAdaptToAccumulatedUnknowns(resolvedVal); + } + private NamespacedAttribute( CelTypeProvider typeProvider, CelValueConverter celValueConverter, diff --git a/runtime/src/main/java/dev/cel/runtime/planner/PlannedProgram.java b/runtime/src/main/java/dev/cel/runtime/planner/PlannedProgram.java index 1470e4909..006ac20fe 100644 --- a/runtime/src/main/java/dev/cel/runtime/planner/PlannedProgram.java +++ b/runtime/src/main/java/dev/cel/runtime/planner/PlannedProgram.java @@ -21,6 +21,7 @@ import dev.cel.common.exceptions.CelRuntimeException; import dev.cel.common.values.ErrorValue; import dev.cel.runtime.Activation; +import dev.cel.runtime.CelAttributeResolver; import dev.cel.runtime.CelEvaluationException; import dev.cel.runtime.CelEvaluationExceptionBuilder; import dev.cel.runtime.CelEvaluationListener; @@ -134,11 +135,13 @@ public Object evalOrThrow( GlobalResolver resolver, CelFunctionResolver functionResolver, @Nullable PartialVars partialVars, + @Nullable CelAttributeResolver attributeResolver, @Nullable CelEvaluationListener listener) throws CelEvaluationException { try { ExecutionFrame frame = - ExecutionFrame.create(functionResolver, options(), partialVars, listener); + ExecutionFrame.create( + functionResolver, options(), partialVars, attributeResolver, listener); Object evalResult = interpretable.eval(resolver, frame); if (evalResult instanceof ErrorValue) { ErrorValue errorValue = (ErrorValue) evalResult; @@ -151,6 +154,22 @@ public Object evalOrThrow( } } + public Object evalOrThrow( + PlannedInterpretable interpretable, + GlobalResolver resolver, + CelFunctionResolver functionResolver, + @Nullable PartialVars partialVars, + @Nullable CelEvaluationListener listener) + throws CelEvaluationException { + return evalOrThrow( + interpretable, + resolver, + functionResolver, + partialVars, + /* attributeResolver= */ null, + listener); + } + public Object trace( GlobalResolver resolver, CelFunctionResolver functionResolver, diff --git a/runtime/src/test/java/dev/cel/runtime/async/BUILD.bazel b/runtime/src/test/java/dev/cel/runtime/async/BUILD.bazel index 29f08eb74..4ed7fc85f 100644 --- a/runtime/src/test/java/dev/cel/runtime/async/BUILD.bazel +++ b/runtime/src/test/java/dev/cel/runtime/async/BUILD.bazel @@ -23,6 +23,7 @@ java_library( "//runtime:unknown_attributes", "//runtime:unknown_options", "//runtime/async", + "//testing:cel_runtime_flavor", "@maven//:junit_junit", "@maven//:com_google_testparameterinjector_test_parameter_injector", "//:java_truth", diff --git a/runtime/src/test/java/dev/cel/runtime/async/CelAsyncRuntimeImplTest.java b/runtime/src/test/java/dev/cel/runtime/async/CelAsyncRuntimeImplTest.java index d24e99860..27b720c28 100644 --- a/runtime/src/test/java/dev/cel/runtime/async/CelAsyncRuntimeImplTest.java +++ b/runtime/src/test/java/dev/cel/runtime/async/CelAsyncRuntimeImplTest.java @@ -29,7 +29,6 @@ import com.google.testing.junit.testparameterinjector.TestParameterInjector; // import com.google.testing.testsize.MediumTest; import dev.cel.bundle.Cel; -import dev.cel.bundle.CelFactory; import dev.cel.common.CelAbstractSyntaxTree; import dev.cel.common.CelContainer; import dev.cel.common.CelOptions; @@ -40,6 +39,7 @@ import dev.cel.runtime.CelAttributePattern; import dev.cel.runtime.CelEvaluationException; import dev.cel.runtime.async.CelAsyncRuntime.AsyncProgram; +import dev.cel.testing.CelRuntimeFlavor; import java.time.Duration; import java.util.concurrent.CancellationException; import java.util.concurrent.ExecutionException; @@ -52,6 +52,14 @@ // @MediumTest public final class CelAsyncRuntimeImplTest { + private static final CelOptions CEL_OPTIONS = + CelOptions.current() + .enableUnknownTracking(true) + .enableHeterogeneousNumericComparisons(true) + .build(); + + @TestParameter private CelRuntimeFlavor celRuntimeFlavor; + @Test public void asyncProgram_basicUnknownResolution() throws Exception { // Arrange @@ -62,8 +70,9 @@ public void asyncProgram_basicUnknownResolution() throws Exception { return attr.toString(); }); Cel cel = - CelFactory.standardCelBuilder() - .setOptions(CelOptions.current().enableUnknownTracking(true).build()) + celRuntimeFlavor + .builder() + .setOptions(CEL_OPTIONS) .addMessageTypes(TestAllTypes.getDescriptor()) .addVar("com.google.var1", SimpleType.STRING) .addVar("com.google.var2", SimpleType.STRING) @@ -113,8 +122,9 @@ public void asyncProgram_sequentialUnknownResolution() throws Exception { return attr.toString(); }); Cel cel = - CelFactory.standardCelBuilder() - .setOptions(CelOptions.current().enableUnknownTracking(true).build()) + celRuntimeFlavor + .builder() + .setOptions(CEL_OPTIONS) .addMessageTypes(TestAllTypes.getDescriptor()) .addVar("com.google.var1", SimpleType.BOOL) .addVar("com.google.var2", SimpleType.STRING) @@ -161,8 +171,9 @@ public void asyncProgram_basicAsyncResolver() throws Exception { SettableFuture var3 = SettableFuture.create(); Cel cel = - CelFactory.standardCelBuilder() - .setOptions(CelOptions.current().enableUnknownTracking(true).build()) + celRuntimeFlavor + .builder() + .setOptions(CEL_OPTIONS) .addMessageTypes(TestAllTypes.getDescriptor()) .addVar("com.google.var1", SimpleType.STRING) .addVar("com.google.var2", SimpleType.STRING) @@ -213,8 +224,9 @@ public void asyncProgram_honorsCancellation() throws Exception { SettableFuture var3 = SettableFuture.create(); Cel cel = - CelFactory.standardCelBuilder() - .setOptions(CelOptions.current().enableUnknownTracking(true).build()) + celRuntimeFlavor + .builder() + .setOptions(CEL_OPTIONS) .addMessageTypes(TestAllTypes.getDescriptor()) .addVar("com.google.var1", SimpleType.STRING) .addVar("com.google.var2", SimpleType.STRING) @@ -259,13 +271,14 @@ interface ResolverFactory { @Test public void asyncProgram_concurrency( - @TestParameter(valuesProvider = RepeatedTestProvider.class) int testRunIndex) + @TestParameter(valuesProvider = RepeatedTestProvider.class) int unusedTestRunIndex) throws Exception { Duration taskDelay = Duration.ofMillis(500); // Arrange Cel cel = - CelFactory.standardCelBuilder() - .setOptions(CelOptions.current().enableUnknownTracking(true).build()) + celRuntimeFlavor + .builder() + .setOptions(CEL_OPTIONS) .addMessageTypes(TestAllTypes.getDescriptor()) .addVar("com.google.var1", SimpleType.STRING) .addVar("com.google.var2", SimpleType.STRING) @@ -317,8 +330,9 @@ public void asyncProgram_concurrency( public void asyncProgram_elementResolver() throws Exception { // Arrange Cel cel = - CelFactory.standardCelBuilder() - .setOptions(CelOptions.current().enableUnknownTracking(true).build()) + celRuntimeFlavor + .builder() + .setOptions(CEL_OPTIONS) .addMessageTypes(TestAllTypes.getDescriptor()) .addVar( "com.google.listVar", @@ -366,8 +380,9 @@ public void asyncProgram_elementResolver() throws Exception { public void asyncProgram_thrownExceptionPropagatesImmediately() throws Exception { // Arrange Cel cel = - CelFactory.standardCelBuilder() - .setOptions(CelOptions.current().enableUnknownTracking(true).build()) + celRuntimeFlavor + .builder() + .setOptions(CEL_OPTIONS) .addMessageTypes(TestAllTypes.getDescriptor()) .addVar("com.google.var1", SimpleType.STRING) .addVar("com.google.var2", SimpleType.STRING) @@ -422,8 +437,9 @@ public void asyncProgram_thrownExceptionPropagatesImmediately() throws Exception public void asyncProgram_returnedExceptionPropagatesToEvaluator() throws Exception { // Arrange Cel cel = - CelFactory.standardCelBuilder() - .setOptions(CelOptions.current().enableUnknownTracking(true).build()) + celRuntimeFlavor + .builder() + .setOptions(CEL_OPTIONS) .addMessageTypes(TestAllTypes.getDescriptor()) .addVar("com.google.var1", SimpleType.STRING) .addVar("com.google.var2", SimpleType.STRING) @@ -477,8 +493,9 @@ public void asyncProgram_returnedExceptionPropagatesToEvaluator() throws Excepti public void asyncProgram_returnedExceptionPropagatesToEvaluatorIsPruneable() throws Exception { // Arrange Cel cel = - CelFactory.standardCelBuilder() - .setOptions(CelOptions.current().enableUnknownTracking(true).build()) + celRuntimeFlavor + .builder() + .setOptions(CEL_OPTIONS) .addMessageTypes(TestAllTypes.getDescriptor()) .addVar("com.google.var1", SimpleType.STRING) .addVar("com.google.var2", SimpleType.STRING) diff --git a/runtime/src/test/java/dev/cel/runtime/planner/BUILD.bazel b/runtime/src/test/java/dev/cel/runtime/planner/BUILD.bazel index 9116818dc..898d4bcc0 100644 --- a/runtime/src/test/java/dev/cel/runtime/planner/BUILD.bazel +++ b/runtime/src/test/java/dev/cel/runtime/planner/BUILD.bazel @@ -46,6 +46,7 @@ java_library( "//runtime:partial_vars", "//runtime:program", "//runtime:runtime_equality", + "//runtime:runtime_factory", "//runtime:runtime_helpers", "//runtime:standard_functions", "//runtime:unknown_attributes", diff --git a/runtime/src/test/java/dev/cel/runtime/planner/ProgramPlannerTest.java b/runtime/src/test/java/dev/cel/runtime/planner/ProgramPlannerTest.java index 34e7831a6..2dccccba4 100644 --- a/runtime/src/test/java/dev/cel/runtime/planner/ProgramPlannerTest.java +++ b/runtime/src/test/java/dev/cel/runtime/planner/ProgramPlannerTest.java @@ -71,6 +71,8 @@ import dev.cel.runtime.CelEvaluationException; import dev.cel.runtime.CelFunctionBinding; import dev.cel.runtime.CelLateFunctionBindings; +import dev.cel.runtime.CelRuntime; +import dev.cel.runtime.CelRuntimeFactory; import dev.cel.runtime.CelStandardFunctions; import dev.cel.runtime.CelStandardFunctions.StandardFunction; import dev.cel.runtime.CelUnknownSet; @@ -81,7 +83,9 @@ import dev.cel.runtime.Program; import dev.cel.runtime.RuntimeEquality; import dev.cel.runtime.RuntimeHelpers; +import dev.cel.runtime.UnknownContext; import dev.cel.runtime.standard.TypeFunction; +import java.util.Optional; import org.junit.Test; import org.junit.runner.RunWith; @@ -986,7 +990,7 @@ public void plan_partialEval_withWildcardQualification() throws Exception { .isEqualTo( CelUnknownSet.create( ImmutableSet.of( - CelAttribute.create("unk"), + CelAttribute.create("unk").qualify(CelAttribute.Qualifier.ofString("c")), CelAttribute.create("unk").qualify(CelAttribute.Qualifier.ofString("a")), CelAttribute.create("unk").qualify(CelAttribute.Qualifier.ofString("b"))), ImmutableSet.of(2L, 5L, 7L))); @@ -1484,4 +1488,132 @@ private enum PresenceTestCase { this.expected = expected; } } + + @Test + public void advanceEvaluation_unresolvedAttribute_returnsUnknownSet() throws Exception { + CelCompiler compiler = + CelCompilerFactory.standardCelCompilerBuilder().addVar("unk", SimpleType.INT).build(); + CelAbstractSyntaxTree ast = compile(compiler, "unk + 1"); + CelRuntime celRuntime = + CelRuntimeFactory.plannerRuntimeBuilder() + .setOptions( + CelOptions.current() + .enableUnknownTracking(true) + .enableHeterogeneousNumericComparisons(true) + .build()) + .build(); + CelRuntime.Program program = celRuntime.createProgram(ast); + + UnknownContext context = + UnknownContext.create( + name -> Optional.empty(), ImmutableList.of(CelAttributePattern.create("unk"))); + + Object result = program.advanceEvaluation(context); + + assertThat(result).isInstanceOf(CelUnknownSet.class); + assertThat(((CelUnknownSet) result).attributes()).containsExactly(CelAttribute.create("unk")); + } + + @Test + public void advanceEvaluation_withResolvedAttributes_returnsResult() throws Exception { + CelCompiler compiler = + CelCompilerFactory.standardCelCompilerBuilder().addVar("unk", SimpleType.INT).build(); + CelAbstractSyntaxTree ast = compile(compiler, "unk + 1"); + CelRuntime celRuntime = + CelRuntimeFactory.plannerRuntimeBuilder() + .setOptions( + CelOptions.current() + .enableUnknownTracking(true) + .enableHeterogeneousNumericComparisons(true) + .build()) + .build(); + CelRuntime.Program program = celRuntime.createProgram(ast); + + UnknownContext context = + UnknownContext.create( + name -> Optional.empty(), ImmutableList.of(CelAttributePattern.create("unk"))); + UnknownContext resolvedContext = + context.withResolvedAttributes(ImmutableMap.of(CelAttribute.create("unk"), 41L)); + + Object result = program.advanceEvaluation(resolvedContext); + + assertThat(result).isEqualTo(42L); + } + + @Test + public void advanceEvaluation_multiVariable_incrementalResolution() throws Exception { + CelCompiler compiler = + CelCompilerFactory.standardCelCompilerBuilder() + .addVar("a", SimpleType.INT) + .addVar("b", SimpleType.INT) + .build(); + CelAbstractSyntaxTree ast = compile(compiler, "a + b"); + CelRuntime celRuntime = + CelRuntimeFactory.plannerRuntimeBuilder() + .setOptions( + CelOptions.current() + .enableUnknownTracking(true) + .enableHeterogeneousNumericComparisons(true) + .build()) + .build(); + CelRuntime.Program program = celRuntime.createProgram(ast); + + UnknownContext context = + UnknownContext.create( + name -> Optional.empty(), + ImmutableList.of(CelAttributePattern.create("a"), CelAttributePattern.create("b"))); + + // Pass 1: both unknown + Object pass1 = program.advanceEvaluation(context); + assertThat(pass1).isInstanceOf(CelUnknownSet.class); + assertThat(((CelUnknownSet) pass1).attributes()) + .containsExactly(CelAttribute.create("a"), CelAttribute.create("b")); + + // Pass 2: resolve 'a' + UnknownContext pass2Context = + context.withResolvedAttributes(ImmutableMap.of(CelAttribute.create("a"), 10L)); + Object pass2 = program.advanceEvaluation(pass2Context); + assertThat(pass2).isInstanceOf(CelUnknownSet.class); + assertThat(((CelUnknownSet) pass2).attributes()).containsExactly(CelAttribute.create("b")); + + // Pass 3: resolve 'b' + UnknownContext pass3Context = + pass2Context.withResolvedAttributes(ImmutableMap.of(CelAttribute.create("b"), 20L)); + Object pass3 = program.advanceEvaluation(pass3Context); + assertThat(pass3).isEqualTo(30L); + } + + @Test + public void advanceEvaluation_qualifiedAttributeResolution() throws Exception { + CelCompiler compiler = + CelCompilerFactory.standardCelCompilerBuilder() + .addVar("msg", MapType.create(SimpleType.STRING, SimpleType.STRING)) + .build(); + CelAbstractSyntaxTree ast = compile(compiler, "msg.field"); + CelRuntime celRuntime = + CelRuntimeFactory.plannerRuntimeBuilder() + .setOptions( + CelOptions.current() + .enableUnknownTracking(true) + .enableHeterogeneousNumericComparisons(true) + .build()) + .build(); + CelRuntime.Program program = celRuntime.createProgram(ast); + + UnknownContext context = + UnknownContext.create( + name -> Optional.empty(), + ImmutableList.of( + CelAttributePattern.create("msg").qualify(CelAttribute.Qualifier.ofWildCard()))); + + Object pass1 = program.advanceEvaluation(context); + assertThat(pass1).isInstanceOf(CelUnknownSet.class); + + CelAttribute qualifiedAttr = + CelAttribute.create("msg").qualify(CelAttribute.Qualifier.ofString("field")); + UnknownContext pass2Context = + context.withResolvedAttributes(ImmutableMap.of(qualifiedAttr, "hello")); + Object pass2 = program.advanceEvaluation(pass2Context); + assertThat(pass2).isEqualTo("hello"); + } }