diff --git a/common/src/main/java/dev/cel/common/values/SelectField.java b/common/src/main/java/dev/cel/common/values/SelectField.java index c883d315f..e94c2d817 100644 --- a/common/src/main/java/dev/cel/common/values/SelectField.java +++ b/common/src/main/java/dev/cel/common/values/SelectField.java @@ -82,7 +82,7 @@ public abstract class SelectField { * Map entry decoding specification. Non-null if and only if this is a map field ({@link * #CEL_MAP_TYPE_CODE}). */ - abstract @Nullable MapEntrySpec mapEntrySpec(); + public abstract @Nullable MapEntrySpec mapEntrySpec(); // The lite runtime's FieldLiteDescriptor for this field, built from this field alone on first // use so descriptorless hops don't rebuild it. Typed Object to keep this target free of protobuf @@ -176,7 +176,8 @@ public static SelectField create( } /** - * Creates a field selection hop for a map field with scalar values. + * Creates a field selection hop for a map field with scalar values, or with message values of an + * unspecified type. * * @param fieldNumber Protobuf field number. * @param fieldName Protobuf field name. @@ -193,15 +194,15 @@ public static SelectField createMap( * @param fieldNumber Protobuf field number. * @param fieldName Protobuf field name. * @param mapEntrySpec Specification for decoding map entries. - * @param protoTypeName Protobuf message type name of the map value. Must be non-empty if and only - * if the map value is a message. + * @param protoTypeName Protobuf message type name of the map value, or empty string if + * unspecified. Must be empty if the map value is not a message. */ public static SelectField createMap( long fieldNumber, String fieldName, MapEntrySpec mapEntrySpec, String protoTypeName) { checkNotNull(mapEntrySpec); checkNotNull(protoTypeName); checkArgument( - (mapEntrySpec.valueTypeCode() == MESSAGE_TYPE_CODE) != protoTypeName.isEmpty(), + protoTypeName.isEmpty() || mapEntrySpec.valueTypeCode() == MESSAGE_TYPE_CODE, "Map value proto type name '%s' is inconsistent with value type code %s", protoTypeName, mapEntrySpec.valueTypeCode()); diff --git a/common/src/test/java/dev/cel/common/values/SelectFieldTest.java b/common/src/test/java/dev/cel/common/values/SelectFieldTest.java index 7e1dfbe4f..b725300cf 100644 --- a/common/src/test/java/dev/cel/common/values/SelectFieldTest.java +++ b/common/src/test/java/dev/cel/common/values/SelectFieldTest.java @@ -163,8 +163,9 @@ public void create_mapTypeCode_throwsIllegalArgumentException() { } @Test - public void createMap_threeArguments_success() { - SelectField.MapEntrySpec mapEntrySpec = SelectField.MapEntrySpec.create(9, 9); + public void createMap_threeArguments_success( + @TestParameter({"9", "11"}) long valueTypeCode) { // STRING, MESSAGE of unspecified type + SelectField.MapEntrySpec mapEntrySpec = SelectField.MapEntrySpec.create(9, valueTypeCode); SelectField field = SelectField.createMap(3L, "map_field", mapEntrySpec); @@ -282,30 +283,16 @@ public void equalsAndHashCode_testedProperly() { } @Test - public void createMap_protoTypeNameInconsistentWithValueTypeCode_throws( - @TestParameter InconsistentMapValueTestCase testCase) { - SelectField.MapEntrySpec spec = SelectField.MapEntrySpec.create(9, testCase.valueTypeCode); + public void createMap_scalarValueWithProtoTypeName_throws() { + SelectField.MapEntrySpec spec = SelectField.MapEntrySpec.create(9, 9); IllegalArgumentException thrown = assertThrows( IllegalArgumentException.class, - () -> SelectField.createMap(1L, "map_field", spec, testCase.protoTypeName)); + () -> SelectField.createMap(1L, "map_field", spec, "proto.Name")); assertThat(thrown) .hasMessageThat() - .contains("Map value proto type name '" + testCase.protoTypeName + "' is inconsistent"); - } - - private enum InconsistentMapValueTestCase { - MESSAGE_VALUE_WITHOUT_TYPE_NAME(SelectField.MESSAGE_TYPE_CODE, ""), - SCALAR_VALUE_WITH_TYPE_NAME(9, "cel.expr.conformance.proto3.TestAllTypes"); - - private final long valueTypeCode; - private final String protoTypeName; - - InconsistentMapValueTestCase(long valueTypeCode, String protoTypeName) { - this.valueTypeCode = valueTypeCode; - this.protoTypeName = protoTypeName; - } + .contains("Map value proto type name 'proto.Name' is inconsistent with value type code 9"); } } diff --git a/optimizer/src/main/java/dev/cel/optimizer/optimizers/SelectOptimizer.java b/optimizer/src/main/java/dev/cel/optimizer/optimizers/SelectOptimizer.java index 2ce52ac31..71a7aed38 100644 --- a/optimizer/src/main/java/dev/cel/optimizer/optimizers/SelectOptimizer.java +++ b/optimizer/src/main/java/dev/cel/optimizer/optimizers/SelectOptimizer.java @@ -23,6 +23,7 @@ import com.google.auto.value.AutoValue; import com.google.common.annotations.VisibleForTesting; import com.google.common.collect.ImmutableList; +import com.google.common.collect.ImmutableSet; import com.google.common.primitives.UnsignedLong; import com.google.errorprone.annotations.CanIgnoreReturnValue; import com.google.protobuf.ByteString; @@ -121,9 +122,13 @@ * // Presence tests (2-tuples) * has(request.user.age) -> cel.@hasField(request, * [[user_num, "user"], [age_num, "age"]]) + * + * // Constant list indices and map keys between fields are inlined as bare constants + * request.users[0].age -> cel.@attribute(request, + * [[users_num, "users", 11], 0, [age_num, "age", type_code]], 0) * * - *

Map indexing and non-protobuf selects pass through untouched. + *

Dynamic or trailing indices and non-protobuf selects pass through untouched. * *

Limitations: * @@ -153,6 +158,9 @@ public final class SelectOptimizer implements CelAstOptimizer { private static final TypeParamType TYPE_PARAM_T = TypeParamType.create("T"); + private static final ImmutableSet> STANDARD_INDEX_OVERLOADS = + ImmutableSet.of(ImmutableList.of("index_list"), ImmutableList.of("index_map")); + /** * Declaration for {@code cel.@attribute(operand, qualifiers, dummyOrDefVal) -> T}. * @@ -261,72 +269,37 @@ private void rewriteSelectChain( boolean isHasField = topNode.expr().select().testOnly(); astToModify.source().getMacroCalls().remove(topNode.expr().id()); - List fields = new ArrayList<>(); FieldDescriptor topField = getOptimizableField(navAst, topNode) .orElseThrow( () -> new IllegalStateException("Expected optimizable field on select node")); - fields.add(topField); + // Walks from the leaf. Fields before an index are selected by value, so they carry their type + // codes even in presence tests. + boolean isSelectedByValue = !isHasField; + List qualifierLists = new ArrayList<>(); + qualifierLists.add(newFieldQualifier(topField, isSelectedByValue, idGenerator)); CelMutableExpr currentExpr = topNode.expr().select().operand(); - while (currentExpr.getKind() == Kind.SELECT) { - CelMutableSelect select = currentExpr.select(); - FieldDescriptor field = getOptimizableFieldForExpr(navAst, select).orElse(null); - if (field == null) { - break; - } - fields.add(field); - currentExpr = select.operand(); - } - - Collections.reverse(fields); - - List qualifierLists = new ArrayList<>(fields.size()); - for (FieldDescriptor field : fields) { - if (field.getType() == FieldDescriptor.Type.GROUP) { - throw new UnsupportedOperationException( - "Optimization of Group fields is unsupported: " + field.getFullName()); - } - // TODO: Support Any. - if (field.getType() == FieldDescriptor.Type.MESSAGE - && field.getMessageType().getFullName().equals(CelTypes.ANY_MESSAGE)) { - throw new UnsupportedOperationException( - "Optimization of Any fields is currently unimplemented: " + field.getFullName()); - } - - CelMutableList qualifierElements = - CelMutableList.create( - CelMutableExpr.ofConstant( - idGenerator.nextExprId(), CelConstant.ofValue((long) field.getNumber())), - CelMutableExpr.ofConstant( - idGenerator.nextExprId(), CelConstant.ofValue(field.getName()))); - if (!isHasField) { - qualifierElements - .elements() - .add( - CelMutableExpr.ofConstant( - idGenerator.nextExprId(), CelConstant.ofValue(resolveTypeCode(field)))); - if (field.isMapField()) { - Descriptor mapEntryDesc = field.getMessageType(); - FieldDescriptor keyField = mapEntryDesc.findFieldByName("key"); - FieldDescriptor valField = mapEntryDesc.findFieldByName("value"); - qualifierElements - .elements() - .add( - CelMutableExpr.ofList( - idGenerator.nextExprId(), - CelMutableList.create( - CelMutableExpr.ofConstant( - idGenerator.nextExprId(), - CelConstant.ofValue(resolveTypeCode(keyField))), - CelMutableExpr.ofConstant( - idGenerator.nextExprId(), - CelConstant.ofValue(resolveTypeCode(valField)))))); + while (true) { + if (currentExpr.getKind() == Kind.SELECT) { + FieldDescriptor field = + getOptimizableFieldForExpr(navAst, currentExpr.select()).orElse(null); + if (field != null) { + qualifierLists.add(newFieldQualifier(field, isSelectedByValue, idGenerator)); + currentExpr = currentExpr.select().operand(); + continue; } } - qualifierLists.add(CelMutableExpr.ofList(idGenerator.nextExprId(), qualifierElements)); + CelMutableExpr indexedField = getIndexedField(navAst, currentExpr).orElse(null); + if (indexedField == null) { + break; + } + qualifierLists.add(currentExpr.call().args().get(1)); + isSelectedByValue = true; + currentExpr = indexedField; } + Collections.reverse(qualifierLists); CelMutableExpr qualifiersExpr = CelMutableExpr.ofList(idGenerator.nextExprId(), CelMutableList.create(qualifierLists)); if (isHasField) { @@ -343,6 +316,53 @@ private void rewriteSelectChain( } } + /** Returns the qualifier hop for {@code field}, with its type codes if {@code withTypeCode}. */ + private static CelMutableExpr newFieldQualifier( + FieldDescriptor field, boolean withTypeCode, MonotonicIdGenerator idGenerator) { + if (field.getType() == FieldDescriptor.Type.GROUP) { + throw new UnsupportedOperationException( + "Optimization of Group fields is unsupported: " + field.getFullName()); + } + // TODO: Support Any. + if (field.getType() == FieldDescriptor.Type.MESSAGE + && field.getMessageType().getFullName().equals(CelTypes.ANY_MESSAGE)) { + throw new UnsupportedOperationException( + "Optimization of Any fields is currently unimplemented: " + field.getFullName()); + } + + CelMutableList qualifierElements = + CelMutableList.create( + CelMutableExpr.ofConstant( + idGenerator.nextExprId(), CelConstant.ofValue((long) field.getNumber())), + CelMutableExpr.ofConstant( + idGenerator.nextExprId(), CelConstant.ofValue(field.getName()))); + if (withTypeCode) { + qualifierElements + .elements() + .add( + CelMutableExpr.ofConstant( + idGenerator.nextExprId(), CelConstant.ofValue(resolveTypeCode(field)))); + if (field.isMapField()) { + Descriptor mapEntryDesc = field.getMessageType(); + FieldDescriptor keyField = mapEntryDesc.findFieldByName("key"); + FieldDescriptor valField = mapEntryDesc.findFieldByName("value"); + qualifierElements + .elements() + .add( + CelMutableExpr.ofList( + idGenerator.nextExprId(), + CelMutableList.create( + CelMutableExpr.ofConstant( + idGenerator.nextExprId(), + CelConstant.ofValue(resolveTypeCode(keyField))), + CelMutableExpr.ofConstant( + idGenerator.nextExprId(), + CelConstant.ofValue(resolveTypeCode(valField)))))); + } + } + return CelMutableExpr.ofList(idGenerator.nextExprId(), qualifierElements); + } + private static long resolveTypeCode(FieldDescriptor field) { if (field.isMapField()) { return CEL_MAP_TYPE_CODE; @@ -351,8 +371,39 @@ private static long resolveTypeCode(FieldDescriptor field) { } private boolean isTopOfSelectChain(CelNavigableMutableAst navAst, CelNavigableMutableExpr node) { - return getOptimizableField(navAst, node).isPresent() - && !node.parent().flatMap(parent -> getOptimizableField(navAst, parent)).isPresent(); + if (!getOptimizableField(navAst, node).isPresent()) { + return false; + } + Optional parent = node.parent(); + // A constant index into this field continues the chain into a field of the element. + if (parent.isPresent() && getIndexedField(navAst, parent.get().expr()).isPresent()) { + parent = parent.get().parent(); + } + return !parent.flatMap(p -> getOptimizableField(navAst, p)).isPresent(); + } + + /** + * Returns the protobuf field select that {@code expr} indexes by a constant through a standard + * {@code _[_]} overload, if the field is optimizable. + */ + private Optional getIndexedField( + CelNavigableMutableAst navAst, CelMutableExpr expr) { + // `field.key` on a map isn't inlined: its missing-key error differs from `field['key']`'s. + boolean isStandardIndex = + navAst + .getAst() + .getReference(expr.id()) + .map(reference -> STANDARD_INDEX_OVERLOADS.contains(reference.overloadIds())) + .orElse(false); + if (!isStandardIndex || expr.call().args().get(1).getKind() != Kind.CONSTANT) { + return Optional.empty(); + } + CelMutableExpr fieldExpr = expr.call().args().get(0); + if (fieldExpr.getKind() != Kind.SELECT + || !getOptimizableFieldForExpr(navAst, fieldExpr.select()).isPresent()) { + return Optional.empty(); + } + return Optional.of(fieldExpr); } private Optional getOptimizableField( diff --git a/optimizer/src/test/java/dev/cel/optimizer/optimizers/SelectOptimizerTest.java b/optimizer/src/test/java/dev/cel/optimizer/optimizers/SelectOptimizerTest.java index 4f85a426e..ce5472ebf 100644 --- a/optimizer/src/test/java/dev/cel/optimizer/optimizers/SelectOptimizerTest.java +++ b/optimizer/src/test/java/dev/cel/optimizer/optimizers/SelectOptimizerTest.java @@ -66,8 +66,12 @@ import dev.cel.parser.CelStandardMacro; import dev.cel.parser.CelUnparser; import dev.cel.parser.CelUnparserFactory; +import dev.cel.runtime.CelAttribute; +import dev.cel.runtime.CelAttributePattern; import dev.cel.runtime.CelEvaluationException; import dev.cel.runtime.CelRuntime.Program; +import dev.cel.runtime.CelUnknownSet; +import dev.cel.runtime.PartialVars; import dev.cel.testing.CelRuntimeFlavor; import java.time.Duration; import java.time.Instant; @@ -337,10 +341,49 @@ private enum RewriteTestCase { + " {\"\": google.protobuf.Duration{}})"), MAP_FIELD_INDEXING( "msg.map_int64_message[1].bb", - "cel.@attribute(" - + "cel.@attribute(msg, [[95, \"map_int64_message\", -1, [3, 11]]]," - + " {0: cel.expr.conformance.proto3.TestAllTypes.NestedMessage{}})[1], " + "cel.@attribute(msg, [[95, \"map_int64_message\", -1, [3, 11]], 1, [1, \"bb\", 5]], 0)"), + REPEATED_FIELD_INDEXING( + "msg.repeated_nested_message[0].bb", + "cel.@attribute(msg, [[51, \"repeated_nested_message\", 11], 0, [1, \"bb\", 5]], 0)"), + MAP_AND_REPEATED_FIELD_INDEXING( + "msg.map_int64_nested_type[1].child.payload.repeated_nested_message[0].bb", + "cel.@attribute(msg, [[62, \"map_int64_nested_type\", -1, [3, 11]], 1, " + + "[1, \"child\", 11], [2, \"payload\", 11], " + + "[51, \"repeated_nested_message\", 11], 0, [1, \"bb\", 5]], 0)"), + MAP_FIELD_UINT_KEY_INDEXING( + "msg.map_uint64_message[1u].bb", + "cel.@attribute(msg, [[217, \"map_uint64_message\", -1, [4, 11]], 1u, " + + "[1, \"bb\", 5]], 0)"), + MAP_FIELD_BOOL_KEY_INDEXING( + "msg.map_bool_message[true].bb", + "cel.@attribute(msg, [[73, \"map_bool_message\", -1, [8, 11]], true, " + + "[1, \"bb\", 5]], 0)"), + REPEATED_FIELD_HAS_THROUGH_INDEX( + "has(msg.repeated_nested_message[0].bb)", + "cel.@hasField(msg, [[51, \"repeated_nested_message\", 11], 0, [1, \"bb\"]])"), + MAP_FIELD_HAS_THROUGH_INDEX( + "has(msg.map_int64_nested_type[1].child.payload.single_int64)", + "cel.@hasField(msg, [[62, \"map_int64_nested_type\", -1, [3, 11]], 1, " + + "[1, \"child\"], [2, \"payload\"], [2, \"single_int64\"]])"), + HAS_THROUGH_TWO_INDICES( + "has(msg.map_int64_nested_type[1].child.payload.repeated_nested_message[0].bb)", + "cel.@hasField(msg, [[62, \"map_int64_nested_type\", -1, [3, 11]], 1, " + + "[1, \"child\", 11], [2, \"payload\", 11], " + + "[51, \"repeated_nested_message\", 11], 0, [1, \"bb\"]])"), + REPEATED_FIELD_DYNAMIC_INDEX_STOPS_AT_INDEX( + "msg.repeated_nested_message[x].bb", + "cel.@attribute(cel.@attribute(msg, [[51, \"repeated_nested_message\", 11]]," + + " [cel.expr.conformance.proto3.TestAllTypes.NestedMessage{}])[x], " + "[[1, \"bb\", 5]], 0)"), + REPEATED_FIELD_TRAILING_INDEX_STOPS_AT_INDEX( + "msg.repeated_nested_message[0]", + "cel.@attribute(msg, [[51, \"repeated_nested_message\", 11]]," + + " [cel.expr.conformance.proto3.TestAllTypes.NestedMessage{}])[0]"), + REPEATED_FIELD_INDEX_HOLDING_SELECT( + "msg.repeated_nested_message[msg.single_int64].bb", + "cel.@attribute(cel.@attribute(msg, [[51, \"repeated_nested_message\", 11]]," + + " [cel.expr.conformance.proto3.TestAllTypes.NestedMessage{}])" + + "[cel.@attribute(msg, [[2, \"single_int64\", 3]], 0)], [[1, \"bb\", 5]], 0)"), MAP_FIELD_SELECT_CHAIN_STOPS_AT_MAP_BOUNDARY( "map_var_msg.key.single_nested_message.bb", "cel.@attribute(map_var_msg.key, " @@ -355,6 +398,9 @@ private enum RewriteTestCase { MAP_FIELD_HAS_CHAIN_STOPS_AT_MAP_BOUNDARY( "has(map_var_msg.key.single_nested_message.bb)", "cel.@hasField(map_var_msg.key, [[21, \"single_nested_message\"], [1, \"bb\"]])"), + MAP_VALUE_INDEX_STOPS_AT_MAP_BOUNDARY( + "{'a': [msg]}.a[0].single_int64", + "cel.@attribute({\"a\": [msg]}.a[0], [[2, \"single_int64\", 3]], 0)"), PROTO_MAP_FIELD_SELECT_STOPS_AT_MAP_BOUNDARY( "msg.map_string_message.key.bb", "cel.@attribute(" @@ -907,6 +953,66 @@ private enum NativeSelectEvaluationTestCase { ImmutableMap.of( "proto2_msg", dev.cel.expr.conformance.proto2.TestAllTypes.getDefaultInstance()), false), + REPEATED_FIELD_INDEX( + "msg.repeated_nested_message[1].bb", + ImmutableMap.of("msg", newTestAllTypesWithRepeatedNestedMessages(10, 20)), + 20L), + MAP_FIELD_INDEX( + "msg.map_int64_message[2].bb", + ImmutableMap.of( + "msg", + TestAllTypes.newBuilder() + .putMapInt64Message(2, TestAllTypes.NestedMessage.newBuilder().setBb(42).build()) + .build()), + 42L), + MAP_FIELD_UINT_KEY_INDEX( + "msg.map_uint64_message[2u].bb", + ImmutableMap.of( + "msg", + TestAllTypes.newBuilder() + .putMapUint64Message(2, TestAllTypes.NestedMessage.newBuilder().setBb(42).build()) + .build()), + 42L), + MAP_AND_REPEATED_FIELD_INDEX( + "msg.map_int64_nested_type[1].child.payload.repeated_nested_message[1].bb", + ImmutableMap.of( + "msg", + TestAllTypes.newBuilder() + .putMapInt64NestedType( + 1, + dev.cel.expr.conformance.proto3.NestedTestAllTypes.newBuilder() + .setChild( + dev.cel.expr.conformance.proto3.NestedTestAllTypes.newBuilder() + .setPayload(newTestAllTypesWithRepeatedNestedMessages(10, 20))) + .build()) + .build()), + 20L), + INDEXED_ELEMENT_UNSET_FIELD( + "msg.repeated_nested_message[0].bb", + ImmutableMap.of("msg", newTestAllTypesWithRepeatedNestedMessages(0)), + 0L), + HAS_FIELD_THROUGH_INDEX_PRESENT( + "has(msg.repeated_nested_message[1].bb)", + ImmutableMap.of("msg", newTestAllTypesWithRepeatedNestedMessages(0, 20)), + true), + HAS_FIELD_THROUGH_INDEX_ABSENT( + "has(msg.repeated_nested_message[0].bb)", + ImmutableMap.of("msg", newTestAllTypesWithRepeatedNestedMessages(0, 20)), + false), + HAS_FIELD_THROUGH_TWO_INDICES( + "has(msg.map_int64_nested_type[1].child.payload.repeated_nested_message[1].bb)", + ImmutableMap.of( + "msg", + TestAllTypes.newBuilder() + .putMapInt64NestedType( + 1, + dev.cel.expr.conformance.proto3.NestedTestAllTypes.newBuilder() + .setChild( + dev.cel.expr.conformance.proto3.NestedTestAllTypes.newBuilder() + .setPayload(newTestAllTypesWithRepeatedNestedMessages(10, 20))) + .build()) + .build()), + true), SELECT_ON_MAP_VALUE_POPULATED( "map_var_msg.key.single_nested_message.bb", ImmutableMap.of( @@ -1066,6 +1172,104 @@ public void optimizeAndEvaluate_nativeSelectAndHasField_matchesUnoptimized( assertThat(optimizedResult).isEqualTo(unoptimizedResult); } + @Test + public void optimize_constantIndexThroughCustomOverload_leavesIndexNested() throws Exception { + StructTypeReference nestedMessageType = + StructTypeReference.create(TestAllTypes.NestedMessage.getDescriptor().getFullName()); + Cel celWithCustomIndex = + cel.toCelBuilder() + .addFunctionDeclarations( + CelFunctionDecl.newFunctionDeclaration( + "_[_]", + CelOverloadDecl.newGlobalOverload( + "custom_index_list_string", + nestedMessageType, + ListType.create(nestedMessageType), + SimpleType.STRING))) + .build(); + CelAbstractSyntaxTree ast = + celWithCustomIndex.compile("msg.repeated_nested_message['0'].bb").getAst(); + + CelAbstractSyntaxTree optimizedAst = newSelectOptimizer(celWithCustomIndex).optimize(ast); + + assertThat(CEL_UNPARSER.unparse(optimizedAst)) + .isEqualTo( + "cel.@attribute(cel.@attribute(msg, [[51, \"repeated_nested_message\", 11]]," + + " [cel.expr.conformance.proto3.TestAllTypes.NestedMessage{}])[\"0\"]," + + " [[1, \"bb\", 5]], 0)"); + } + + @Test + public void optimizeAndEvaluate_missingIndexedElement_throwsSameErrorAsUnoptimized( + @TestParameter({ + "msg.repeated_nested_message[1].bb", + "msg.repeated_nested_message[5000000000].bb", + "msg.map_int64_message[2].bb", + "has(msg.repeated_nested_message[1].bb)", + "has(msg.map_int64_message[2].bb)" + }) + String expression) + throws Exception { + assumeTrue(runtimeFlavor == CelRuntimeFlavor.PLANNER); + CelAbstractSyntaxTree ast = cel.compile(expression).getAst(); + Program unoptimizedProgram = cel.createProgram(ast); + Program optimizedProgram = cel.createProgram(celOptimizer.optimize(ast)); + ImmutableMap input = + ImmutableMap.of("msg", newTestAllTypesWithRepeatedNestedMessages(10)); + + CelEvaluationException unoptimizedError = + assertThrows(CelEvaluationException.class, () -> unoptimizedProgram.eval(input)); + CelEvaluationException optimizedError = + assertThrows(CelEvaluationException.class, () -> optimizedProgram.eval(input)); + + assertThat(optimizedError.getErrorCode()).isEqualTo(unoptimizedError.getErrorCode()); + assertThat(optimizedError) + .hasCauseThat() + .hasMessageThat() + .isEqualTo(unoptimizedError.getCause().getMessage()); + } + + @Test + public void optimizeAndEvaluate_partialVarsOnIndexedElement_returnsSameUnknownAsUnoptimized() + throws Exception { + assumeTrue(runtimeFlavor == CelRuntimeFlavor.PLANNER); + CelAbstractSyntaxTree ast = cel.compile("msg.repeated_nested_message[0].bb").getAst(); + CelAbstractSyntaxTree optimizedAst = celOptimizer.optimize(ast); + PartialVars partialVars = newRepeatedNestedMessagePartialVars(/* unknownIndex= */ 0); + + CelUnknownSet unoptimizedResult = (CelUnknownSet) cel.createProgram(ast).eval(partialVars); + CelUnknownSet optimizedResult = + (CelUnknownSet) cel.createProgram(optimizedAst).eval(partialVars); + + assertThat(optimizedResult.attributes()).isEqualTo(unoptimizedResult.attributes()); + assertThat(optimizedResult.attributes()) + .containsExactly( + CelAttribute.fromQualifiedIdentifier("msg.repeated_nested_message") + .qualify(CelAttribute.Qualifier.ofInt(0))); + } + + @Test + public void optimizeAndEvaluate_partialVarsOnOtherElement_returnsSameValueAsUnoptimized() + throws Exception { + assumeTrue(runtimeFlavor == CelRuntimeFlavor.PLANNER); + CelAbstractSyntaxTree ast = cel.compile("msg.repeated_nested_message[0].bb").getAst(); + CelAbstractSyntaxTree optimizedAst = celOptimizer.optimize(ast); + PartialVars partialVars = newRepeatedNestedMessagePartialVars(/* unknownIndex= */ 1); + + Object unoptimizedResult = cel.createProgram(ast).eval(partialVars); + Object optimizedResult = cel.createProgram(optimizedAst).eval(partialVars); + + assertThat(optimizedResult).isEqualTo(unoptimizedResult); + assertThat(optimizedResult).isEqualTo(10L); + } + + private static PartialVars newRepeatedNestedMessagePartialVars(long unknownIndex) { + return PartialVars.of( + ImmutableMap.of("msg", newTestAllTypesWithRepeatedNestedMessages(10, 20)), + CelAttributePattern.fromQualifiedIdentifier("msg.repeated_nested_message") + .qualify(CelAttribute.Qualifier.ofInt(unknownIndex))); + } + @Test public void optimize_comprehension_rewritesSelectInLoopStep() throws Exception { CelAbstractSyntaxTree ast = cel.compile("[msg].map(m, m.single_int64)").getAst(); @@ -1517,6 +1721,14 @@ private static CelOptimizer newSelectOptimizer(Cel cel) { .build(); } + private static TestAllTypes newTestAllTypesWithRepeatedNestedMessages(int... bbs) { + TestAllTypes.Builder builder = TestAllTypes.newBuilder(); + for (int bb : bbs) { + builder.addRepeatedNestedMessage(TestAllTypes.NestedMessage.newBuilder().setBb(bb)); + } + return builder.build(); + } + private static NestedTestAllTypes newNestedTestAllTypes(long singleInt64) { // Proto2 TestAllTypes requires FQN due to simple-name collision with proto3 TestAllTypes. return NestedTestAllTypes.newBuilder() diff --git a/runtime/src/main/java/dev/cel/runtime/CelAttributePattern.java b/runtime/src/main/java/dev/cel/runtime/CelAttributePattern.java index ff5f3f5bf..9b9f70112 100644 --- a/runtime/src/main/java/dev/cel/runtime/CelAttributePattern.java +++ b/runtime/src/main/java/dev/cel/runtime/CelAttributePattern.java @@ -19,6 +19,7 @@ import com.google.auto.value.AutoValue; import com.google.common.base.Preconditions; import com.google.common.collect.ImmutableList; +import com.google.common.primitives.UnsignedLong; import com.google.errorprone.annotations.Immutable; /** @@ -124,9 +125,7 @@ public boolean isMatch(CelAttribute attribute) { int qualifierCount = min(qualifiers().size(), attribute.qualifiers().size()); int i = 0; for (; i < qualifierCount; i++) { - CelAttribute.Qualifier pattern = qualifiers().get(i); - CelAttribute.Qualifier qualifier = attribute.qualifiers().get(i); - if (pattern.kind() != CelAttribute.Qualifier.Kind.WILD_CARD && !pattern.equals(qualifier)) { + if (!matches(qualifiers().get(i), attribute.qualifiers().get(i))) { return false; } } @@ -154,9 +153,7 @@ public boolean isPartialMatch(CelAttribute attribute) { } int qualifierCount = min(qualifiers().size(), attribute.qualifiers().size()); for (int i = 0; i < qualifierCount; i++) { - CelAttribute.Qualifier pattern = qualifiers().get(i); - CelAttribute.Qualifier qualifier = attribute.qualifiers().get(i); - if (pattern.kind() != CelAttribute.Qualifier.Kind.WILD_CARD && !pattern.equals(qualifier)) { + if (!matches(qualifiers().get(i), attribute.qualifiers().get(i))) { return false; } } @@ -165,6 +162,24 @@ public boolean isPartialMatch(CelAttribute attribute) { return true; } + // Numeric qualifiers match by value across int and uint, as map keys and list indices do. + private static boolean matches(CelAttribute.Qualifier pattern, CelAttribute.Qualifier qualifier) { + if (pattern.kind() == CelAttribute.Qualifier.Kind.WILD_CARD || pattern.equals(qualifier)) { + return true; + } + if (pattern.kind() == CelAttribute.Qualifier.Kind.AS_INT + && qualifier.kind() == CelAttribute.Qualifier.Kind.AS_UINT) { + return intEqualsUint(pattern.asInt(), qualifier.asUint()); + } + return pattern.kind() == CelAttribute.Qualifier.Kind.AS_UINT + && qualifier.kind() == CelAttribute.Qualifier.Kind.AS_INT + && intEqualsUint(qualifier.asInt(), pattern.asUint()); + } + + private static boolean intEqualsUint(long intValue, UnsignedLong uintValue) { + return intValue >= 0 && UnsignedLong.valueOf(intValue).equals(uintValue); + } + /** * Return an appropriate attribute for a pattern match. * diff --git a/runtime/src/main/java/dev/cel/runtime/RuntimeHelpers.java b/runtime/src/main/java/dev/cel/runtime/RuntimeHelpers.java index 6dbba80cd..7444ffef1 100644 --- a/runtime/src/main/java/dev/cel/runtime/RuntimeHelpers.java +++ b/runtime/src/main/java/dev/cel/runtime/RuntimeHelpers.java @@ -133,11 +133,11 @@ public static A indexList(List list, Number index) { .map(v -> indexList(list, v)) .orElseThrow(() -> new CelIndexOutOfBoundsException(index.doubleValue())); } - int castIndex = Ints.checkedCast(index.longValue()); - if (castIndex < 0 || castIndex >= list.size()) { - throw new CelIndexOutOfBoundsException(castIndex); + long longIndex = index.longValue(); + if (longIndex < 0 || longIndex >= list.size()) { + throw new CelIndexOutOfBoundsException(index); } - return list.get(castIndex); + return list.get((int) longIndex); } // Integer Arithmetic 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 c25bd9ca0..7258ffe34 100644 --- a/runtime/src/main/java/dev/cel/runtime/planner/BUILD.bazel +++ b/runtime/src/main/java/dev/cel/runtime/planner/BUILD.bazel @@ -320,6 +320,9 @@ java_library( name = "eval_index", srcs = ["EvalIndex.java"], deps = [ + ":attribute", + ":eval_attribute", + ":eval_const", ":eval_helpers", ":planned_interpretable", "//common/ast", @@ -329,6 +332,7 @@ java_library( "//runtime:interpretable", "//runtime:resolved_overload", "@maven//:com_google_errorprone_error_prone_annotations", + "@maven//:com_google_guava_guava", ], ) @@ -552,8 +556,11 @@ java_library( "//common/values:cel_byte_string", "//common/values:optimized_select_traversal", "//common/values:select_field", + "//runtime:runtime_equality", + "//runtime:runtime_helpers", "@maven//:com_google_errorprone_error_prone_annotations", "@maven//:com_google_guava_guava", + "@maven//:org_jspecify_jspecify", ], ) @@ -985,6 +992,9 @@ cel_android_library( name = "eval_index_android", srcs = ["EvalIndex.java"], deps = [ + ":attribute_android", + ":eval_attribute_android", + ":eval_const_android", ":eval_helpers_android", ":planned_interpretable_android", "//common/ast:ast_android", @@ -994,6 +1004,7 @@ cel_android_library( "//runtime:interpretable_android", "//runtime:resolved_overload_android", "@maven//:com_google_errorprone_error_prone_annotations", + "@maven_android//:com_google_guava_guava", ], ) @@ -1217,7 +1228,10 @@ cel_android_library( "//common/values:optimized_select_traversal_android", "//common/values:select_field_android", "//common/values:values_android", + "//runtime:runtime_equality_android", + "//runtime:runtime_helpers_android", "@maven//:com_google_errorprone_error_prone_annotations", + "@maven//:org_jspecify_jspecify", "@maven_android//:com_google_guava_guava", ], ) diff --git a/runtime/src/main/java/dev/cel/runtime/planner/EvalHelpers.java b/runtime/src/main/java/dev/cel/runtime/planner/EvalHelpers.java index 012432f45..bca40c235 100644 --- a/runtime/src/main/java/dev/cel/runtime/planner/EvalHelpers.java +++ b/runtime/src/main/java/dev/cel/runtime/planner/EvalHelpers.java @@ -59,16 +59,8 @@ static Object evalStrictly( Object val; try { val = interpretable.eval(resolver, frame); - } catch (LocalizedEvaluationException e) { - // Already localized - propagate as-is to preserve inner expression ID - throw e; - } catch (CelRuntimeException e) { - // Wrap with current interpretable's location - throw new LocalizedEvaluationException(e, interpretable.expr().id()); } catch (Exception e) { - // Wrap generic exceptions with location - throw new LocalizedEvaluationException( - e, CelErrorCode.INTERNAL_ERROR, interpretable.expr().id()); + throw localize(e, interpretable.expr().id()); } if (val instanceof ErrorValue) { throw ((ErrorValue) val).value(); @@ -76,6 +68,17 @@ static Object evalStrictly( return val; } + /** Localizes {@code e} to {@code exprId} unless an inner expression already did. */ + static LocalizedEvaluationException localize(Exception e, long exprId) { + if (e instanceof LocalizedEvaluationException) { + return (LocalizedEvaluationException) e; + } + if (e instanceof CelRuntimeException) { + return new LocalizedEvaluationException((CelRuntimeException) e, exprId); + } + return new LocalizedEvaluationException(e, CelErrorCode.INTERNAL_ERROR, exprId); + } + static Object dispatch( String functionName, CelResolvedOverload overload, diff --git a/runtime/src/main/java/dev/cel/runtime/planner/EvalIndex.java b/runtime/src/main/java/dev/cel/runtime/planner/EvalIndex.java index d7fec0088..d012cea03 100644 --- a/runtime/src/main/java/dev/cel/runtime/planner/EvalIndex.java +++ b/runtime/src/main/java/dev/cel/runtime/planner/EvalIndex.java @@ -17,6 +17,7 @@ import static dev.cel.runtime.planner.EvalHelpers.evalNonstrictly; import static dev.cel.runtime.planner.EvalHelpers.evalStrictly; +import com.google.common.primitives.UnsignedLong; import com.google.errorprone.annotations.Immutable; import dev.cel.common.ast.CelExpr; import dev.cel.common.values.CelValueConverter; @@ -53,16 +54,79 @@ Object evalInternal(GlobalResolver resolver, ExecutionFrame frame) throws CelEva functionName, resolvedOverload, celValueConverter, targetVal, indexVal); } - static EvalIndex create( + /** Plans {@code target[index]}. */ + static PlannedInterpretable create( CelExpr expr, String functionName, CelResolvedOverload resolvedOverload, PlannedInterpretable target, PlannedInterpretable index, CelValueConverter celValueConverter) { + // Qualifies the attribute so unknown patterns match through constant indices, as in CEL-C++. + if (target instanceof EvalAttribute + && index instanceof EvalConstant + && resolvedOverload.isStrict()) { + Object constant = ((EvalConstant) index).constantValue(); + // Kinds representable as a CelAttribute.Qualifier. + if (constant instanceof Long + || constant instanceof UnsignedLong + || constant instanceof String + || constant instanceof Boolean) { + return ((EvalAttribute) target) + .addQualifier( + expr, + new ConstantIndexQualifier( + expr.id(), resolvedOverload, constant, celValueConverter)); + } + } return new EvalIndex(expr, functionName, resolvedOverload, target, index, celValueConverter); } + /** Indexes an attribute's value by a constant through the {@code _[_]} overload. */ + @Immutable + private static final class ConstantIndexQualifier implements Qualifier { + private final long exprId; + private final CelResolvedOverload resolvedOverload; + + @SuppressWarnings("Immutable") // Immutable CEL constant. + private final Object index; + + private final CelValueConverter celValueConverter; + + @Override + public Object value() { + return index; + } + + @Override + public Object qualify(Object operand) { + // Mirrors EvalIndex: the overload receives the same adapted target, and failures are + // localized to the index expression. + try { + return celValueConverter.toTraversalTarget( + EvalHelpers.dispatch( + resolvedOverload.getFunctionName(), + resolvedOverload, + celValueConverter, + EvalHelpers.convertAndAdaptResult(celValueConverter, operand), + index)); + } catch (Exception e) { + throw EvalHelpers.localize(e, exprId); + } + } + + private ConstantIndexQualifier( + long exprId, + CelResolvedOverload resolvedOverload, + Object index, + CelValueConverter celValueConverter) { + this.exprId = exprId; + this.resolvedOverload = resolvedOverload; + this.index = index; + this.celValueConverter = celValueConverter; + } + } + private EvalIndex( CelExpr expr, String functionName, 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 d83e74f00..16e036559 100644 --- a/runtime/src/main/java/dev/cel/runtime/planner/NamespacedAttribute.java +++ b/runtime/src/main/java/dev/cel/runtime/planner/NamespacedAttribute.java @@ -114,9 +114,9 @@ public Object resolve(long exprId, GlobalResolver ctx, ExecutionFrame frame) { } // The name itself could be a fully qualified reference to an enum value - // (e.g: my.enum_type.BAR) + // (e.g: my.enum_type.BAR), which can't be qualified further. int lastDotIndex = name.lastIndexOf('.'); - if (lastDotIndex > 0) { + if (lastDotIndex > 0 && qualifiers.isEmpty()) { String enumTypeName = name.substring(0, lastDotIndex); String enumValueQualifier = name.substring(lastDotIndex + 1); diff --git a/runtime/src/main/java/dev/cel/runtime/planner/OptimizedSelectPlanner.java b/runtime/src/main/java/dev/cel/runtime/planner/OptimizedSelectPlanner.java index 6846e8d74..033ab1f48 100644 --- a/runtime/src/main/java/dev/cel/runtime/planner/OptimizedSelectPlanner.java +++ b/runtime/src/main/java/dev/cel/runtime/planner/OptimizedSelectPlanner.java @@ -31,9 +31,14 @@ import dev.cel.common.values.OptimizedSelectTraversal; import dev.cel.common.values.OptionalValue; import dev.cel.common.values.SelectField; +import dev.cel.runtime.RuntimeEquality; +import dev.cel.runtime.RuntimeHelpers; import java.time.Duration; import java.time.Instant; +import java.util.List; +import java.util.Map; import java.util.Optional; +import org.jspecify.annotations.Nullable; /** * Plans optimizer-rewritten select chains ({@code cel.@attribute}) and presence tests ({@code @@ -46,56 +51,87 @@ final class OptimizedSelectPlanner { static final String CEL_ATTRIBUTE_FUNCTION_NAME = "cel.@attribute"; static final String CEL_HAS_FIELD_FUNCTION_NAME = "cel.@hasField"; + // A list index is an int, as typed by an INT64 (3) map key. + private static final long LIST_INDEX_TYPE_CODE = 3; + private final AttributeFactory attributeFactory; private final CelValueConverter celValueConverter; + private final RuntimeEquality runtimeEquality; static OptimizedSelectPlanner create( - AttributeFactory attributeFactory, CelValueConverter celValueConverter) { - return new OptimizedSelectPlanner(attributeFactory, celValueConverter); + AttributeFactory attributeFactory, + CelValueConverter celValueConverter, + RuntimeEquality runtimeEquality) { + return new OptimizedSelectPlanner(attributeFactory, celValueConverter, runtimeEquality); } PlannedInterpretable plan(CelExpr expr, String functionName, OperandPlanner operandPlanner) { ImmutableList args = expr.call().args(); - ImmutableList selectFields; - boolean isPresenceTest; - switch (functionName) { - case CEL_ATTRIBUTE_FUNCTION_NAME: - { - checkArgument( - args.size() == 3, "Expected 3 arguments for %s, found %s", functionName, args.size()); - selectFields = unpackAttributeFields(args.get(1), args.get(2)); - isPresenceTest = false; - break; - } - case CEL_HAS_FIELD_FUNCTION_NAME: - { - checkArgument( - args.size() == 2, "Expected 2 arguments for %s, found %s", functionName, args.size()); - selectFields = unpackHasFieldFields(args.get(1)); - isPresenceTest = true; - break; - } - default: - throw new IllegalArgumentException( - "Unsupported optimized select function: " + functionName); + boolean isPresenceTest = functionName.equals(CEL_HAS_FIELD_FUNCTION_NAME); + int expectedArgCount = isPresenceTest ? 2 : 3; + checkArgument( + args.size() == expectedArgCount, + "Expected %s arguments for %s, found %s", + expectedArgCount, + functionName, + args.size()); + + // Constant index hops split the qualifiers into field segments, each but the last ending at the + // repeated or map field that the following index selects from. + ImmutableList hops = unpackQualifierHops(args.get(1)); + ImmutableList.Builder qualifiers = ImmutableList.builder(); + int segmentStart = 0; + for (int i = 0; i < hops.size(); i++) { + CelExpr hop = hops.get(i); + if (hop.getKind() != Kind.CONSTANT) { + continue; + } + checkArgument( + i > segmentStart && i < hops.size() - 1, + "Index qualifier hop must be between field hops: %s", + hop); + ImmutableList fields = + unpackAttributeFields(hops.subList(segmentStart, i), /* dummyOrDefExpr= */ null); + addSegmentQualifiers(qualifiers, fields, /* isPresenceTest= */ false); + SelectField.MapEntrySpec mapEntrySpec = Iterables.getLast(fields).mapEntrySpec(); + long keyTypeCode = mapEntrySpec != null ? mapEntrySpec.keyTypeCode() : LIST_INDEX_TYPE_CODE; + qualifiers.add( + new IndexQualifier( + resolveScalarDefaultValue(keyTypeCode, hop), runtimeEquality, celValueConverter)); + segmentStart = i + 1; } + ImmutableList leafSegment = hops.subList(segmentStart, hops.size()); + addSegmentQualifiers( + qualifiers, + isPresenceTest + ? unpackHasFieldFields(leafSegment) + : unpackAttributeFields(leafSegment, args.get(2)), + isPresenceTest); PlannedInterpretable operand = operandPlanner.plan(args.get(0)); Attribute qualified = PlannerHelpers.resolveBaseAttribute(operand, attributeFactory); - - int lastIndex = selectFields.size() - 1; - for (int i = 0; i < lastIndex; i++) { - qualified = qualified.addQualifier(new PassthroughQualifier(selectFields.get(i).fieldName())); + for (Qualifier qualifier : qualifiers.build()) { + qualified = qualified.addQualifier(qualifier); } - qualified = - qualified.addQualifier( - new OptimizedSelectQualifier(selectFields, celValueConverter, isPresenceTest)); return EvalAttribute.create(expr, qualified); } + private void addSegmentQualifiers( + ImmutableList.Builder qualifiers, + ImmutableList fields, + boolean isPresenceTest) { + for (int i = 0; i < fields.size() - 1; i++) { + qualifiers.add(new PassthroughQualifier(fields.get(i).fieldName())); + } + qualifiers.add(new OptimizedSelectQualifier(fields, celValueConverter, isPresenceTest)); + } + + /** + * Unpacks the field hops of a {@code cel.@attribute} segment. The last hop is the leaf typed by + * {@code dummyOrDefExpr}, or, if null, the repeated or map field indexed by the next hop. + */ private static ImmutableList unpackAttributeFields( - CelExpr qualifiersExpr, CelExpr dummyOrDefExpr) { - ImmutableList elements = unpackQualifierHops(qualifiersExpr); + ImmutableList elements, @Nullable CelExpr dummyOrDefExpr) { ImmutableList.Builder fieldsBuilder = ImmutableList.builderWithExpectedSize(elements.size()); for (int i = 0; i < elements.size(); i++) { @@ -124,6 +160,8 @@ private static ImmutableList unpackAttributeFields( "Non-leaf qualifier hop must have MESSAGE type code (11), found: %s", rawTypeCode); fieldsBuilder.add(SelectField.create(fieldNumber, fieldName, rawTypeCode)); + } else if (dummyOrDefExpr == null) { + fieldsBuilder.add(parseIndexedField(fieldNumber, fieldName, rawTypeCode, hopElements)); } else if (rawTypeCode == SelectField.CEL_MAP_TYPE_CODE) { checkArgument( hopElements.size() == 4, @@ -163,6 +201,25 @@ private static SelectField parseMapLeaf( return SelectField.createMap(fieldNumber, fieldName, mapEntrySpec); } + /** Parses a repeated message or a message-valued map field hop that an index hop follows. */ + private static SelectField parseIndexedField( + long fieldNumber, String fieldName, long typeCode, ImmutableList hopElements) { + if (typeCode == SelectField.CEL_MAP_TYPE_CODE && hopElements.size() == 4) { + SelectField.MapEntrySpec mapEntrySpec = parseMapEntrySpec(hopElements.get(3)); + checkArgument( + mapEntrySpec.valueTypeCode() == SelectField.MESSAGE_TYPE_CODE, + "Indexed map qualifier hop must have MESSAGE values (11), found: %s", + mapEntrySpec.valueTypeCode()); + return SelectField.createMap(fieldNumber, fieldName, mapEntrySpec); + } + checkArgument( + typeCode == SelectField.MESSAGE_TYPE_CODE && hopElements.size() == 3, + "Indexed qualifier hop must be a repeated message or a message-valued map field: %s", + hopElements); + return SelectField.create( + fieldNumber, fieldName, typeCode, /* defaultValue= */ ImmutableList.of()); + } + private static SelectField parseRepeatedLeaf( long fieldNumber, String fieldName, long typeCode, CelExpr dummyExpr) { checkArgument( @@ -199,8 +256,7 @@ private static SelectField parseSingularLeaf( return SelectField.create(fieldNumber, fieldName, typeCode, defaultValue, protoTypeName); } - private static ImmutableList unpackHasFieldFields(CelExpr qualifiersExpr) { - ImmutableList elements = unpackQualifierHops(qualifiersExpr); + private static ImmutableList unpackHasFieldFields(ImmutableList elements) { ImmutableList.Builder fieldsBuilder = ImmutableList.builderWithExpectedSize(elements.size()); for (CelExpr hopExpr : elements) { @@ -429,9 +485,46 @@ private OptimizedSelectQualifier( } } + /** Indexes a repeated or map field's value by a constant list index or map key. */ + @Immutable + private static final class IndexQualifier implements Qualifier { + @SuppressWarnings("Immutable") // Immutable CEL constant. + private final Object index; + + private final RuntimeEquality runtimeEquality; + private final CelValueConverter celValueConverter; + + @Override + public Object value() { + return index; + } + + // The operand is the protobuf field's list or map traversal target, so its elements are indexed + // in place rather than unwrapped first as the generic index operator's arguments are. + @Override + @SuppressWarnings("unchecked") // Map keys are compared as CEL values. + public Object qualify(Object operand) { + Object element = + operand instanceof Map + ? runtimeEquality.indexMap((Map) operand, index) + : RuntimeHelpers.indexList((List) operand, (Long) index); + return celValueConverter.toTraversalTarget(element); + } + + private IndexQualifier( + Object index, RuntimeEquality runtimeEquality, CelValueConverter celValueConverter) { + this.index = checkNotNull(index); + this.runtimeEquality = checkNotNull(runtimeEquality); + this.celValueConverter = checkNotNull(celValueConverter); + } + } + private OptimizedSelectPlanner( - AttributeFactory attributeFactory, CelValueConverter celValueConverter) { + AttributeFactory attributeFactory, + CelValueConverter celValueConverter, + RuntimeEquality runtimeEquality) { this.attributeFactory = checkNotNull(attributeFactory); this.celValueConverter = checkNotNull(celValueConverter); + this.runtimeEquality = checkNotNull(runtimeEquality); } } diff --git a/runtime/src/main/java/dev/cel/runtime/planner/ProgramPlanner.java b/runtime/src/main/java/dev/cel/runtime/planner/ProgramPlanner.java index 3782d1860..4314e66ad 100644 --- a/runtime/src/main/java/dev/cel/runtime/planner/ProgramPlanner.java +++ b/runtime/src/main/java/dev/cel/runtime/planner/ProgramPlanner.java @@ -935,6 +935,6 @@ private ProgramPlanner( this.attributeFactory = AttributeFactory.newAttributeFactory(container, typeProvider, celValueConverter); this.optimizedSelectPlanner = - OptimizedSelectPlanner.create(this.attributeFactory, celValueConverter); + OptimizedSelectPlanner.create(this.attributeFactory, celValueConverter, runtimeEquality); } } diff --git a/runtime/src/test/java/dev/cel/runtime/CelAttributePatternTest.java b/runtime/src/test/java/dev/cel/runtime/CelAttributePatternTest.java index de265cc77..bb7eab9a1 100644 --- a/runtime/src/test/java/dev/cel/runtime/CelAttributePatternTest.java +++ b/runtime/src/test/java/dev/cel/runtime/CelAttributePatternTest.java @@ -18,6 +18,7 @@ import static org.junit.Assert.assertThrows; import com.google.common.collect.ImmutableList; +import com.google.common.primitives.UnsignedLong; import dev.cel.runtime.CelAttribute.Qualifier; import org.junit.Test; import org.junit.runner.RunWith; @@ -244,6 +245,36 @@ public void isPartialMatch_doesNotMatch() { .isFalse(); } + @Test + public void isMatch_intAndUintQualifiersWithSameValue_match() { + CelAttributePattern intPattern = + CelAttributePattern.create("identifier").qualify(Qualifier.ofInt(1)); + CelAttributePattern uintPattern = + CelAttributePattern.create("identifier").qualify(Qualifier.ofUint(1)); + CelAttribute intAttribute = CelAttribute.create("identifier").qualify(Qualifier.ofInt(1)); + CelAttribute uintAttribute = CelAttribute.create("identifier").qualify(Qualifier.ofUint(1)); + + assertThat(intPattern.isMatch(uintAttribute)).isTrue(); + assertThat(uintPattern.isMatch(intAttribute)).isTrue(); + assertThat(intPattern.isPartialMatch(uintAttribute)).isTrue(); + assertThat(uintPattern.isPartialMatch(intAttribute)).isTrue(); + } + + @Test + public void isMatch_negativeIntAndUintWithSameBits_doNotMatch() { + CelAttributePattern intPattern = + CelAttributePattern.create("identifier").qualify(Qualifier.ofInt(-1)); + CelAttributePattern uintPattern = + CelAttributePattern.create("identifier") + .qualify(Qualifier.ofUint(UnsignedLong.fromLongBits(-1))); + CelAttribute intAttribute = CelAttribute.create("identifier").qualify(Qualifier.ofInt(-1)); + CelAttribute uintAttribute = + CelAttribute.create("identifier").qualify(Qualifier.ofUint(UnsignedLong.fromLongBits(-1))); + + assertThat(intPattern.isMatch(uintAttribute)).isFalse(); + assertThat(uintPattern.isMatch(intAttribute)).isFalse(); + } + @Test public void simplify_simplifies() { assertThat( diff --git a/runtime/src/test/java/dev/cel/runtime/CelLiteRuntimeVersionSkewTest.java b/runtime/src/test/java/dev/cel/runtime/CelLiteRuntimeVersionSkewTest.java index 5fbf0b1a6..bfec9551c 100644 --- a/runtime/src/test/java/dev/cel/runtime/CelLiteRuntimeVersionSkewTest.java +++ b/runtime/src/test/java/dev/cel/runtime/CelLiteRuntimeVersionSkewTest.java @@ -46,6 +46,7 @@ import com.google.protobuf.Value; import com.google.testing.junit.testparameterinjector.TestParameter; import com.google.testing.junit.testparameterinjector.TestParameterInjector; +import com.google.testing.junit.testparameterinjector.TestParameters; import dev.cel.bundle.Cel; import dev.cel.bundle.CelFactory; import dev.cel.common.CelAbstractSyntaxTree; @@ -762,6 +763,19 @@ public void select_renamedMapField_indexing() throws Exception { assertThat(result).isEqualTo(100L); } + @Test + @TestParameters({ + "{expression: 'has(msg.repeated_nested_message[1].bb)', expected: true}", + "{expression: 'has(msg.map_int64_nested_type[1].child.payload)', expected: true}", + "{expression: 'has(msg.map_int64_nested_type[2].child.payload)', expected: false}" + }) + public void has_throughUnknownRepeatedAndMapIndices_decodesFromWireBytes( + String expression, boolean expected) throws Exception { + Object result = eval(expression, POPULATED_SERVER_MESSAGE); + + assertThat(result).isEqualTo(expected); + } + @Test public void has_renamedRepeatedField_emptyPackedWireBytes_returnsFalse() throws Exception { // Evaluated via newOlderClassV1Runtime() on nested_msg.payload so payload is a wire-backed diff --git a/runtime/src/test/java/dev/cel/runtime/planner/OptimizedSelectPlannerTest.java b/runtime/src/test/java/dev/cel/runtime/planner/OptimizedSelectPlannerTest.java index 9cf457cfe..ecb6d2052 100644 --- a/runtime/src/test/java/dev/cel/runtime/planner/OptimizedSelectPlannerTest.java +++ b/runtime/src/test/java/dev/cel/runtime/planner/OptimizedSelectPlannerTest.java @@ -783,15 +783,6 @@ private enum ScalarDummyMismatchTestCase { private final ImmutableList hopElements; private final CelExpr dummy; - private static CelExpr mapEntrySpec(long keyTypeCode, long valueTypeCode) { - return CelExpr.ofList( - 8L, - ImmutableList.of( - CelExpr.ofConstant(9L, CelConstant.ofValue(keyTypeCode)), - CelExpr.ofConstant(10L, CelConstant.ofValue(valueTypeCode))), - ImmutableList.of()); - } - ScalarDummyMismatchTestCase(ImmutableList hopElements, CelExpr dummy) { this.hopElements = hopElements; this.dummy = dummy; @@ -930,6 +921,105 @@ public void plan_invalidAst_messageStructWithEmptyMessageName_throwsEvaluationEx assertThat(e).hasMessageThat().contains("Protobuf message type name must not be empty"); } + @SuppressWarnings("ImmutableEnumChecker") // Test only + private enum MalformedIndexHopTestCase { + LEADING_INDEX( + ImmutableList.of(constantHop(0L), fieldHop(1L, "bb", 5L)), + "Index qualifier hop must be between field hops"), + TRAILING_INDEX( + ImmutableList.of(fieldHop(51L, "repeated_nested_message", 11L), constantHop(0L)), + "Index qualifier hop must be between field hops"), + CONSECUTIVE_INDICES( + ImmutableList.of( + fieldHop(51L, "repeated_nested_message", 11L), + constantHop(0L), + constantHop(0L), + fieldHop(1L, "bb", 5L)), + "Index qualifier hop must be between field hops"), + SCALAR_FIELD_INDEXED( + ImmutableList.of(fieldHop(2L, "single_int64", 3L), constantHop(0L), fieldHop(1L, "bb", 5L)), + "Indexed qualifier hop must be a repeated message or a message-valued map field"), + SCALAR_VALUED_MAP_INDEXED( + ImmutableList.of( + mapHop(61L, "map_string_string", 9L, 9L), constantHop("k"), fieldHop(1L, "bb", 5L)), + "Indexed map qualifier hop must have MESSAGE values (11), found: 9"), + NON_INT_LIST_INDEX( + ImmutableList.of( + fieldHop(51L, "repeated_nested_message", 11L), + constantHop("0"), + fieldHop(1L, "bb", 5L)), + "is incompatible with type code 3"), + MAP_KEY_KIND_MISMATCH( + ImmutableList.of( + mapHop(95L, "map_int64_message", 3L, 11L), + constantHop(UnsignedLong.ONE), + fieldHop(1L, "bb", 5L)), + "is incompatible with type code 3"); + + private final ImmutableList hops; + private final String expectedMessageSubstring; + + MalformedIndexHopTestCase(ImmutableList hops, String expectedMessageSubstring) { + this.hops = hops; + this.expectedMessageSubstring = expectedMessageSubstring; + } + } + + @Test + public void plan_invalidAst_malformedIndexHop_throwsEvaluationException( + @TestParameter MalformedIndexHopTestCase testCase) { + CelAbstractSyntaxTree ast = + CelAbstractSyntaxTree.newParsedAst( + CelExpr.ofCall( + 1L, + OptimizedSelectPlanner.CEL_ATTRIBUTE_FUNCTION_NAME, + ImmutableList.of( + CelExpr.ofIdent(2L, "msg"), + CelExpr.ofList(3L, testCase.hops, ImmutableList.of()), + CelExpr.ofConstant(4L, CelConstant.ofValue(0L)))), + SELECT_OPTIMIZED_SOURCE); + + CelEvaluationException e = assertThrows(CelEvaluationException.class, () -> PLANNER.plan(ast)); + + assertThat(e).hasCauseThat().isInstanceOf(IllegalArgumentException.class); + assertThat(e).hasMessageThat().contains(testCase.expectedMessageSubstring); + } + + private static CelExpr fieldHop(long fieldNumber, String fieldName, long typeCode) { + return CelExpr.ofList( + 0L, + ImmutableList.of( + CelExpr.ofConstant(0L, CelConstant.ofValue(fieldNumber)), + CelExpr.ofConstant(0L, CelConstant.ofValue(fieldName)), + CelExpr.ofConstant(0L, CelConstant.ofValue(typeCode))), + ImmutableList.of()); + } + + private static CelExpr mapHop( + long fieldNumber, String fieldName, long keyTypeCode, long valueTypeCode) { + return CelExpr.ofList( + 0L, + ImmutableList.of( + CelExpr.ofConstant(0L, CelConstant.ofValue(fieldNumber)), + CelExpr.ofConstant(0L, CelConstant.ofValue(fieldName)), + CelExpr.ofConstant(0L, CelConstant.ofValue(-1L)), + mapEntrySpec(keyTypeCode, valueTypeCode)), + ImmutableList.of()); + } + + private static CelExpr mapEntrySpec(long keyTypeCode, long valueTypeCode) { + return CelExpr.ofList( + 8L, + ImmutableList.of( + CelExpr.ofConstant(9L, CelConstant.ofValue(keyTypeCode)), + CelExpr.ofConstant(10L, CelConstant.ofValue(valueTypeCode))), + ImmutableList.of()); + } + + private static CelExpr constantHop(Object index) { + return CelExpr.ofConstant(0L, CelConstant.ofObjectValue(index)); + } + private static CelAbstractSyntaxTree optimizeSelectAst(String expression) throws Exception { CelAbstractSyntaxTree ast = CEL.compile(expression).getAst(); CelAbstractSyntaxTree optimizedAst = SELECT_OPTIMIZER.optimize(ast); 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 4e006cdd4..dbe0a2f1d 100644 --- a/runtime/src/test/java/dev/cel/runtime/planner/ProgramPlannerTest.java +++ b/runtime/src/test/java/dev/cel/runtime/planner/ProgramPlannerTest.java @@ -47,6 +47,7 @@ import dev.cel.common.ast.CelExpr.CelComprehension; import dev.cel.common.exceptions.CelDivideByZeroException; import dev.cel.common.exceptions.CelInvalidArgumentException; +import dev.cel.common.exceptions.CelRuntimeException; import dev.cel.common.internal.CelDescriptorPool; import dev.cel.common.internal.DefaultDescriptorPool; import dev.cel.common.internal.DefaultMessageFactory; @@ -769,7 +770,7 @@ public void plan_call_index_withUnknownTarget() throws Exception { assertThat(result) .isEqualTo( CelUnknownSet.create( - ImmutableSet.of(CelAttribute.create("unk_list")), ImmutableSet.of(1L))); + ImmutableSet.of(CelAttribute.create("unk_list")), ImmutableSet.of(2L))); } @Test @@ -811,6 +812,114 @@ public void plan_call_index_withMultipleUnknowns_mergesUnknowns() throws Excepti ImmutableSet.of(1L, 3L))); } + @Test + public void plan_call_constantIndex_withPartialVarsOnOtherElement_returnsValue() + throws Exception { + CelAbstractSyntaxTree ast = compile("msg.repeated_nested_message[0].bb"); + Program program = PLANNER.plan(ast); + TestAllTypes msg = + TestAllTypes.newBuilder() + .addRepeatedNestedMessage(NestedMessage.newBuilder().setBb(1)) + .addRepeatedNestedMessage(NestedMessage.newBuilder().setBb(2)) + .build(); + + Object result = + program.eval( + PartialVars.of( + ImmutableMap.of("msg", msg), + CelAttributePattern.fromQualifiedIdentifier("msg.repeated_nested_message") + .qualify(CelAttribute.Qualifier.ofInt(1)))); + + assertThat(result).isEqualTo(1L); + } + + @Test + public void plan_call_constantIndex_withPartialVarsOnIndexedElement_returnsUnknown() + throws Exception { + CelAbstractSyntaxTree ast = compile("msg.repeated_nested_message[0].bb"); + Program program = PLANNER.plan(ast); + CelAttribute indexedElement = + CelAttribute.fromQualifiedIdentifier("msg.repeated_nested_message") + .qualify(CelAttribute.Qualifier.ofInt(0)); + + Object result = + program.eval( + PartialVars.of( + CelAttributePattern.fromQualifiedIdentifier("msg.repeated_nested_message") + .qualify(CelAttribute.Qualifier.ofInt(0)))); + + assertThat(result) + .isEqualTo(CelUnknownSet.create(ImmutableSet.of(indexedElement), ImmutableSet.of(5L))); + } + + @SuppressWarnings("ImmutableEnumChecker") // Test only + private enum ConstantIndexUnknownTestCase { + UINT_INDEX_MATCHES_INT_PATTERN( + "dyn_var[1u]", + CelAttribute.Qualifier.ofInt(1), + CelAttribute.create("dyn_var").qualify(CelAttribute.Qualifier.ofUint(1))), + INT_INDEX_MATCHES_UINT_PATTERN( + "dyn_var[1]", + CelAttribute.Qualifier.ofUint(1), + CelAttribute.create("dyn_var").qualify(CelAttribute.Qualifier.ofInt(1))), + BOOL_INDEX( + "dyn_var[true]", + CelAttribute.Qualifier.ofBool(true), + CelAttribute.create("dyn_var").qualify(CelAttribute.Qualifier.ofBool(true))), + // A double isn't an attribute qualifier, so the index isn't folded into the attribute. + DOUBLE_INDEX_STOPS_AT_INDEX( + "dyn_var[1.0]", CelAttribute.Qualifier.ofInt(1), CelAttribute.create("dyn_var")); + + private final String expression; + private final CelAttribute.Qualifier patternIndex; + private final CelAttribute expectedAttribute; + + ConstantIndexUnknownTestCase( + String expression, CelAttribute.Qualifier patternIndex, CelAttribute expectedAttribute) { + this.expression = expression; + this.patternIndex = patternIndex; + this.expectedAttribute = expectedAttribute; + } + } + + @Test + public void plan_call_constantIndex_withPartialVars_returnsUnknown( + @TestParameter ConstantIndexUnknownTestCase testCase) throws Exception { + CelAbstractSyntaxTree ast = compile(testCase.expression); + Program program = PLANNER.plan(ast); + + CelUnknownSet result = + (CelUnknownSet) + program.eval( + PartialVars.of( + CelAttributePattern.create("dyn_var").qualify(testCase.patternIndex))); + + assertThat(result.attributes()).containsExactly(testCase.expectedAttribute); + } + + @Test + public void plan_call_constantIndexOnEnumConstant_throws() throws Exception { + CelAbstractSyntaxTree ast = CEL_COMPILER.parse("GlobalEnum.GAR[0]").getAst(); + Program program = PLANNER.plan(ast); + + assertThrows(CelEvaluationException.class, program::eval); + } + + @Test + public void plan_call_constantIndex_outOfBounds_localizesErrorToIndex() throws Exception { + CelAbstractSyntaxTree ast = compile("msg.repeated_nested_message[1].bb"); + Program program = PLANNER.plan(ast); + + CelEvaluationException e = + assertThrows( + CelEvaluationException.class, + () -> program.eval(ImmutableMap.of("msg", TestAllTypes.getDefaultInstance()))); + + assertThat(e) + .hasMessageThat() + .isEqualTo("evaluation error at :27: Index out of bounds: 1"); + } + @Test public void plan_call_noMatchingOverload_throws() throws Exception { CelAbstractSyntaxTree ast = compile("concat(b'abc', dyn_var)"); @@ -1286,10 +1395,10 @@ 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))); + ImmutableSet.of(2L, 5L, 8L))); } @Test @@ -2377,7 +2486,8 @@ public void plan_comprehension_iterRangeIsMutableAccumulator_iteratesElements() } @Test - public void plan_nonStrictFunction_withErrorArg_adaptsToException() throws Exception { + public void plan_nonStrictFunction_withErrorArg_adaptsToExceptionWithErrorCode() + throws Exception { CelCompiler compiler = CelCompilerFactory.standardCelCompilerBuilder() .addFunctionDeclarations( @@ -2396,7 +2506,9 @@ public void plan_nonStrictFunction_withErrorArg_adaptsToException() throws Excep "is_error_int", ImmutableList.of(Long.class), /* isStrict= */ false, - args -> args[0] instanceof Exception); + args -> + args[0] instanceof CelRuntimeException + && ((CelRuntimeException) args[0]).getErrorCode() == CelErrorCode.DIVIDE_BY_ZERO); ProgramPlanner planner = ProgramPlanner.newPlanner( TYPE_PROVIDER, diff --git a/runtime/src/test/resources/planner_unknownFieldSelection.baseline b/runtime/src/test/resources/planner_unknownFieldSelection.baseline index 0cbc75299..51eb23b49 100644 --- a/runtime/src/test/resources/planner_unknownFieldSelection.baseline +++ b/runtime/src/test/resources/planner_unknownFieldSelection.baseline @@ -36,7 +36,7 @@ declare x { } =====> bindings: {unknown_attributes=[x]} -result: CelUnknownSet{attributes=[x], unknownExprIds=[2]} +result: CelUnknownSet{attributes=[x], unknownExprIds=[3]} Source: x.map_int32_int64[22] declare x { @@ -44,7 +44,7 @@ declare x { } =====> bindings: {unknown_attributes=[x.map_int32_int64]} -result: CelUnknownSet{attributes=[x.map_int32_int64], unknownExprIds=[2]} +result: CelUnknownSet{attributes=[x.map_int32_int64], unknownExprIds=[3]} Source: x.repeated_nested_message[1] declare x { @@ -52,7 +52,7 @@ declare x { } =====> bindings: {unknown_attributes=[x]} -result: CelUnknownSet{attributes=[x], unknownExprIds=[2]} +result: CelUnknownSet{attributes=[x], unknownExprIds=[3]} Source: x.repeated_nested_message[1] declare x { @@ -60,7 +60,7 @@ declare x { } =====> bindings: {unknown_attributes=[x.repeated_nested_message]} -result: CelUnknownSet{attributes=[x.repeated_nested_message], unknownExprIds=[2]} +result: CelUnknownSet{attributes=[x.repeated_nested_message], unknownExprIds=[3]} Source: x.single_nested_message.bb declare x {