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 1e067f03c..5d15ad311 100644 --- a/optimizer/src/main/java/dev/cel/optimizer/optimizers/SelectOptimizer.java +++ b/optimizer/src/main/java/dev/cel/optimizer/optimizers/SelectOptimizer.java @@ -98,10 +98,9 @@ * *

The 3rd argument of {@code cel.@attribute} is a typed expression that binds the result type * {@code T}. For singular scalar leaves it is also the field's default value (e.g. {@code 0} or a - * proto2 custom default). For repeated, map, and message leaves (including {@code - * google.protobuf.Duration}, {@code google.protobuf.Timestamp}, and wrapper types) it is a - * type-only dummy (e.g. {@code [0]}, {@code {"": 0}}, {@code Msg{}}) that the runtime never - * evaluates. + * proto2 custom default). For repeated, map, and message leaves (including well-known types such as + * {@code google.protobuf.Duration} and {@code google.protobuf.Struct}) it is a type-only dummy + * (e.g. {@code [0]}, {@code {"": 0}}, {@code Msg{}}) that the runtime never evaluates. * *

Field presence paths ({@code cel.@hasField}) represent each step as a 2-tuple: {@code * [field_num, field_name]}. @@ -280,8 +279,11 @@ private void rewriteSelectChain( throw new UnsupportedOperationException( "Optimization of Group fields is unsupported: " + field.getFullName()); } - if (field.getType() == FieldDescriptor.Type.MESSAGE) { - checkUnsupportedMessageType(field.getMessageType().getFullName(), 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 = @@ -345,26 +347,6 @@ private boolean isTopOfSelectChain(CelNavigableMutableAst navAst, CelNavigableMu && !node.parent().flatMap(parent -> getOptimizableField(navAst, parent)).isPresent(); } - // TODO: Support the remaining well-known types below. - private static void checkUnsupportedMessageType(String messageFullName, String fieldFullName) { - if (messageFullName.equals(CelTypes.STRUCT_MESSAGE)) { - throw new UnsupportedOperationException( - "Optimization of Struct fields is currently unimplemented: " + fieldFullName); - } - if (messageFullName.equals(CelTypes.LIST_VALUE_MESSAGE)) { - throw new UnsupportedOperationException( - "Optimization of ListValue fields is currently unimplemented: " + fieldFullName); - } - if (messageFullName.equals(CelTypes.VALUE_MESSAGE)) { - throw new UnsupportedOperationException( - "Optimization of Value fields is currently unimplemented: " + fieldFullName); - } - if (messageFullName.equals(CelTypes.ANY_MESSAGE)) { - throw new UnsupportedOperationException( - "Optimization of Any fields is currently unimplemented: " + fieldFullName); - } - } - private Optional getOptimizableField( CelNavigableMutableAst navAst, CelNavigableMutableExpr node) { if (node.getKind() != Kind.SELECT) { 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 2b3808a30..94567f167 100644 --- a/optimizer/src/test/java/dev/cel/optimizer/optimizers/SelectOptimizerTest.java +++ b/optimizer/src/test/java/dev/cel/optimizer/optimizers/SelectOptimizerTest.java @@ -51,7 +51,6 @@ import dev.cel.common.navigation.CelNavigableMutableAst; import dev.cel.common.types.ListType; import dev.cel.common.types.MapType; -import dev.cel.common.types.NullableType; import dev.cel.common.types.SimpleType; import dev.cel.common.types.StructTypeReference; import dev.cel.common.values.CelByteString; @@ -309,6 +308,15 @@ private enum RewriteTestCase { PROTO3_WRAPPER( "msg.single_int64_wrapper", "cel.@attribute(msg, [[105, \"single_int64_wrapper\", 11]], google.protobuf.Int64Value{})"), + PROTO3_STRUCT( + "msg.single_struct.key", + "cel.@attribute(msg, [[103, \"single_struct\", 11]], google.protobuf.Struct{}).key"), + PROTO3_VALUE( + "msg.single_value", + "cel.@attribute(msg, [[104, \"single_value\", 11]], google.protobuf.Value{})"), + PROTO3_LIST_VALUE( + "msg.list_value[0]", + "cel.@attribute(msg, [[114, \"list_value\", 11]], google.protobuf.ListValue{})[0]"), // Map selects MAP_STRING_STRING( @@ -795,6 +803,24 @@ private enum NativeSelectEvaluationTestCase { "msg.single_int64_wrapper", ImmutableMap.of("msg", TestAllTypes.getDefaultInstance()), NullValue.NULL_VALUE), + PROTO3_STRUCT_CONVERTS_TO_MAP( + "msg.single_struct", + ImmutableMap.of( + "msg", + TestAllTypes.newBuilder() + .setSingleStruct( + Struct.newBuilder() + .putFields("a", Value.newBuilder().setNumberValue(1.5).build())) + .build()), + ImmutableMap.of("a", 1.5)), + PROTO3_VALUE_UNSET_IS_NULL( + "msg.single_value", + ImmutableMap.of("msg", TestAllTypes.getDefaultInstance()), + NullValue.NULL_VALUE), + PROTO3_LIST_VALUE_UNSET_IS_EMPTY( + "msg.list_value", + ImmutableMap.of("msg", TestAllTypes.getDefaultInstance()), + ImmutableList.of()), PROTO3_MAP_OF_ANY_UNPACKS_VALUE( "msg.map_string_any['k']", ImmutableMap.of( @@ -1083,31 +1109,18 @@ public void optimize_iterationLimitReached_throws() throws Exception { assertThat(e).hasMessageThat().isEqualTo("Max iteration count reached."); } - private enum UnsupportedWellKnownFieldTestCase { - STRUCT("msg.single_struct", "Optimization of Struct fields is currently unimplemented"), - LIST_VALUE("msg.list_value", "Optimization of ListValue fields is currently unimplemented"), - VALUE("msg.single_value", "Optimization of Value fields is currently unimplemented"), - ANY("msg.single_any", "Optimization of Any fields is currently unimplemented"); - - private final String expression; - private final String expectedMessageSubstring; - - UnsupportedWellKnownFieldTestCase(String expression, String expectedMessageSubstring) { - this.expression = expression; - this.expectedMessageSubstring = expectedMessageSubstring; - } - } - @Test - public void optimize_unsupportedWellKnownField_throwsUnsupportedOperationException( - @TestParameter UnsupportedWellKnownFieldTestCase testCase) throws Exception { - CelAbstractSyntaxTree ast = cel.compile(testCase.expression).getAst(); + public void optimize_unsupportedWellKnownField_throwsUnsupportedOperationException() + throws Exception { + CelAbstractSyntaxTree ast = cel.compile("msg.single_any").getAst(); SelectOptimizer optimizer = SelectOptimizer.newInstance(TestAllTypes.getDescriptor().getFile()); UnsupportedOperationException e = assertThrows(UnsupportedOperationException.class, () -> optimizer.optimize(ast, cel)); - assertThat(e).hasMessageThat().contains(testCase.expectedMessageSubstring); + assertThat(e) + .hasMessageThat() + .contains("Optimization of Any fields is currently unimplemented"); } @Test @@ -1349,12 +1362,20 @@ public void optimize_toParsedExpr_matchesExpectedSerializedProto() throws Except } @Test - public void optimize_wrapperFieldSelect_preservesNullableResultType() throws Exception { - CelAbstractSyntaxTree ast = cel.compile("msg.single_int64_wrapper").getAst(); + public void optimize_wellKnownTypeFieldSelect_preservesResultType( + @TestParameter({ + "msg.single_int64_wrapper", + "msg.single_struct", + "msg.single_value", + "msg.list_value" + }) + String expression) + throws Exception { + CelAbstractSyntaxTree ast = cel.compile(expression).getAst(); CelAbstractSyntaxTree optimizedAst = celOptimizer.optimize(ast); - assertThat(optimizedAst.getResultType()).isEqualTo(NullableType.create(SimpleType.INT)); + assertThat(optimizedAst.getResultType()).isEqualTo(ast.getResultType()); } @Test 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 2691c171b..6846e8d74 100644 --- a/runtime/src/main/java/dev/cel/runtime/planner/OptimizedSelectPlanner.java +++ b/runtime/src/main/java/dev/cel/runtime/planner/OptimizedSelectPlanner.java @@ -18,7 +18,6 @@ import static com.google.common.base.Preconditions.checkNotNull; import com.google.common.collect.ImmutableList; -import com.google.common.collect.ImmutableSet; import com.google.common.collect.Iterables; import com.google.common.primitives.UnsignedLong; import com.google.errorprone.annotations.Immutable; @@ -47,17 +46,6 @@ final class OptimizedSelectPlanner { static final String CEL_ATTRIBUTE_FUNCTION_NAME = "cel.@attribute"; static final String CEL_HAS_FIELD_FUNCTION_NAME = "cel.@hasField"; - /** - * Well-known message types whose CEL semantics (Any unpacking, JSON value conversion) are not - * implemented by the optimized traversal. - */ - private static final ImmutableSet UNSUPPORTED_WELL_KNOWN_TYPE_IDENTS = - ImmutableSet.of( - CelTypes.ANY_MESSAGE, - CelTypes.STRUCT_MESSAGE, - CelTypes.VALUE_MESSAGE, - CelTypes.LIST_VALUE_MESSAGE); - private final AttributeFactory attributeFactory; private final CelValueConverter celValueConverter; @@ -299,8 +287,9 @@ private static String parseMessageProtoTypeName(CelExpr expr) { private static String parseLeafMessageProtoTypeName(CelExpr expr) { String protoTypeName = parseMessageProtoTypeName(expr); + // TODO: Support Any. checkArgument( - !UNSUPPORTED_WELL_KNOWN_TYPE_IDENTS.contains(protoTypeName), + !protoTypeName.equals(CelTypes.ANY_MESSAGE), "Leaf well-known type '%s' is not supported by the select-optimized runtime", protoTypeName); return protoTypeName; diff --git a/runtime/src/test/java/dev/cel/runtime/CelLiteRuntimeVersionSkewTest.java b/runtime/src/test/java/dev/cel/runtime/CelLiteRuntimeVersionSkewTest.java index 176703cac..e9f9e0d67 100644 --- a/runtime/src/test/java/dev/cel/runtime/CelLiteRuntimeVersionSkewTest.java +++ b/runtime/src/test/java/dev/cel/runtime/CelLiteRuntimeVersionSkewTest.java @@ -27,7 +27,10 @@ import com.google.protobuf.CodedOutputStream; import com.google.protobuf.ExtensionRegistryLite; import com.google.protobuf.Int64Value; +import com.google.protobuf.NullValue; +import com.google.protobuf.Struct; import com.google.protobuf.UnknownFieldSet; +import com.google.protobuf.Value; import com.google.testing.junit.testparameterinjector.TestParameter; import com.google.testing.junit.testparameterinjector.TestParameterInjector; import dev.cel.bundle.Cel; @@ -2201,6 +2204,16 @@ private enum DescriptorlessEvaluationTestCase { .addRepeatedInt64Wrapper(Int64Value.of(0)) .build(), true), + JSON_TYPES( + "msg.single_struct.a == 1.0 && msg.single_value == null && has(msg.single_value)" + + " && msg.list_value == [] && !has(msg.list_value) && msg.repeated_value == [null]", + TestAllTypes.newBuilder() + .setSingleStruct( + Struct.newBuilder().putFields("a", Value.newBuilder().setNumberValue(1).build())) + .setSingleValue(Value.newBuilder().setNullValue(NullValue.NULL_VALUE)) + .addRepeatedValue(Value.newBuilder().setNullValue(NullValue.NULL_VALUE)) + .build(), + true), ; private final String expression; 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 8428091e2..9b8fb45d1 100644 --- a/runtime/src/test/java/dev/cel/runtime/planner/OptimizedSelectPlannerTest.java +++ b/runtime/src/test/java/dev/cel/runtime/planner/OptimizedSelectPlannerTest.java @@ -837,14 +837,8 @@ public void plan_invalidAst_mapFieldWithEmptyMapDummy_throwsEvaluationException( } @Test - public void plan_invalidAst_repeatedMessageWithUnsupportedWellKnownType_throwsEvaluationException( - @TestParameter({ - "google.protobuf.Any", - "google.protobuf.Struct", - "google.protobuf.Value", - "google.protobuf.ListValue" - }) - String unsupportedProtoTypeName) { + public void + plan_invalidAst_repeatedMessageWithUnsupportedWellKnownType_throwsEvaluationException() { CelAbstractSyntaxTree ast = CelAbstractSyntaxTree.newParsedAst( CelExpr.ofCall( @@ -867,7 +861,7 @@ public void plan_invalidAst_repeatedMessageWithUnsupportedWellKnownType_throwsEv CelExpr.ofList( 8L, ImmutableList.of( - CelExpr.ofStruct(9L, unsupportedProtoTypeName, ImmutableList.of())), + CelExpr.ofStruct(9L, "google.protobuf.Any", ImmutableList.of())), ImmutableList.of()))), CelSource.newBuilder().build()); @@ -876,7 +870,7 @@ public void plan_invalidAst_repeatedMessageWithUnsupportedWellKnownType_throwsEv assertThat(e).hasCauseThat().isInstanceOf(IllegalArgumentException.class); assertThat(e) .hasMessageThat() - .contains("Leaf well-known type '" + unsupportedProtoTypeName + "' is not supported"); + .contains("Leaf well-known type 'google.protobuf.Any' is not supported"); } @Test