From 361c017c355c7c45bb1f6a438638577d7a88b5c8 Mon Sep 17 00:00:00 2001 From: Dmitri Plotnikov Date: Wed, 28 Oct 2026 16:39:23 -0700 Subject: [PATCH] Split CelOptionalLibrary into CelOptionalCompilerLibrary and CelOptionalRuntimeLibrary to allow its usage in Lite runtime. PiperOrigin-RevId: 1009262722 --- .../test/java/dev/cel/conformance/BUILD.bazel | 17 +- .../dev/cel/conformance/ConformanceTest.java | 2 + extensions/BUILD.bazel | 17 + .../main/java/dev/cel/extensions/BUILD.bazel | 72 +- .../dev/cel/extensions/CelExtensions.java | 24 + .../CelOptionalCompilerLibrary.java | 492 +++++++++++ .../cel/extensions/CelOptionalLibrary.java | 657 ++------------- .../extensions/CelOptionalRuntimeLibrary.java | 338 ++++++++ .../test/java/dev/cel/extensions/BUILD.bazel | 10 + .../extensions/CelOptionalLibraryTest.java | 795 +++++++++++++++--- .../dev/cel/runtime/CelLiteRuntimeImpl.java | 49 +- .../dev/cel/runtime/FunctionBindingImpl.java | 4 + .../src/test/java/dev/cel/runtime/BUILD.bazel | 1 + .../runtime/CelLiteRuntimeAndroidTest.java | 63 +- .../java/dev/cel/testing/compiled/BUILD.bazel | 7 + 15 files changed, 1801 insertions(+), 747 deletions(-) create mode 100644 extensions/src/main/java/dev/cel/extensions/CelOptionalCompilerLibrary.java create mode 100644 extensions/src/main/java/dev/cel/extensions/CelOptionalRuntimeLibrary.java diff --git a/conformance/src/test/java/dev/cel/conformance/BUILD.bazel b/conformance/src/test/java/dev/cel/conformance/BUILD.bazel index 589263715..db0aea5cc 100644 --- a/conformance/src/test/java/dev/cel/conformance/BUILD.bazel +++ b/conformance/src/test/java/dev/cel/conformance/BUILD.bazel @@ -39,6 +39,7 @@ java_library( "//extensions:lists_runtime_library", "//extensions:math_runtime_library", "//extensions:optional_library", + "//extensions:optional_runtime_library", "//extensions:sets_runtime_library", "//extensions:strings_runtime_library", "//optimizer", @@ -273,11 +274,7 @@ _TESTS_TO_SKIP_PLANNER_SELECT_OPT = _TESTS_TO_SKIP_PLANNER + [ "optionals", ] -_TESTS_TO_SKIP_LITE_SELECT_OPT = _TESTS_TO_SKIP_PLANNER + [ - # Optionals are not supported in CelLiteRuntime. - "optionals", - "block_ext/basic/optional_list,optional_map,optional_map_chained,optional_message", - +_TESTS_TO_SKIP_LITE_BASE = _TESTS_TO_SKIP_PLANNER + [ # Proto2 extensions are not supported in CelLiteRuntime. "proto2_ext", "proto2/extensions_has", @@ -291,6 +288,7 @@ _TESTS_TO_SKIP_LITE_SELECT_OPT = _TESTS_TO_SKIP_PLANNER + [ # TODO: Message creation with prepopulated fields is not yet supported in # CelLiteRuntime. + "block_ext/basic/optional_message", "comparisons/eq_wrapper/eq_bool,eq_bytes,eq_double,eq_float,eq_int32,eq_int64,eq_string,eq_uint32,eq_uint64", "comparisons/eq_wrapper/eq_proto2,eq_proto3,eq_proto2_missing_fields_neq,eq_proto3_missing_fields_neq,eq_proto_nan_equal", "comparisons/eq_wrapper/eq_proto2_any_unpack_equal,eq_proto2_any_unpack_not_equal,eq_proto2_any_unpack_bytewise_fallback_not_equal,eq_proto2_any_unpack_bytewise_fallback_equal,eq_proto3_any_unpack_equal,eq_proto3_any_unpack_not_equal,eq_proto3_any_unpack_bytewise_fallback_not_equal,eq_proto3_any_unpack_bytewise_fallback_equal", @@ -317,6 +315,7 @@ _TESTS_TO_SKIP_LITE_SELECT_OPT = _TESTS_TO_SKIP_PLANNER + [ "dynamic/complex", "enums/legacy_proto2/assign_standalone_name,assign_standalone_int,assign_standalone_int_too_big,assign_standalone_int_too_neg,assign_repeated_enum,list_enum_as_list_int,enum_as_int", "enums/legacy_proto3/assign_standalone_name,assign_standalone_int,assign_standalone_int_big,assign_standalone_int_neg,assign_standalone_int_too_big,assign_standalone_int_too_neg,assign_repeated_enum,list_enum_as_list_int,enum_as_int", + "optionals/optionals/struct_map_optindex_field,struct_map_optindex_field_nested,struct_list_optindex_field,struct_optindex_value,optional_struct_optindex_value,optional_struct_optindex_index_value", "parse/nest/message_literal", "parse/repeat/message_literal", "parse/whitespace", @@ -337,7 +336,13 @@ _TESTS_TO_SKIP_LITE_SELECT_OPT = _TESTS_TO_SKIP_PLANNER + [ "wrappers", ] -_TESTS_TO_SKIP_LITE = _TESTS_TO_SKIP_LITE_SELECT_OPT + [ +_TESTS_TO_SKIP_LITE_SELECT_OPT = _TESTS_TO_SKIP_LITE_BASE + [ + # TODO: Optional field selection on proto messages is not yet supported by + # SelectOptimizer. + "optionals", +] + +_TESTS_TO_SKIP_LITE = _TESTS_TO_SKIP_LITE_BASE + [ # Custom proto2 default values on unset fields require SelectOptimization in CelLiteRuntime. "proto2/empty_field/scalar_with_default", ] diff --git a/conformance/src/test/java/dev/cel/conformance/ConformanceTest.java b/conformance/src/test/java/dev/cel/conformance/ConformanceTest.java index be65911ba..42c40bd48 100644 --- a/conformance/src/test/java/dev/cel/conformance/ConformanceTest.java +++ b/conformance/src/test/java/dev/cel/conformance/ConformanceTest.java @@ -60,6 +60,7 @@ import dev.cel.extensions.CelListsRuntimeLibrary; import dev.cel.extensions.CelMathRuntimeLibrary; import dev.cel.extensions.CelOptionalLibrary; +import dev.cel.extensions.CelOptionalRuntimeLibrary; import dev.cel.extensions.CelSetsRuntimeLibrary; import dev.cel.extensions.CelStringRuntimeLibrary; import dev.cel.optimizer.CelOptimizerFactory; @@ -126,6 +127,7 @@ public final class ConformanceTest extends Statement { CelEncoderRuntimeLibrary.encoders(), CelListsRuntimeLibrary.lists(), CelMathRuntimeLibrary.math(), + CelOptionalRuntimeLibrary.optional(), CelSetsRuntimeLibrary.sets(), CelStringRuntimeLibrary.strings()); diff --git a/extensions/BUILD.bazel b/extensions/BUILD.bazel index a494fe82e..1f9f792f0 100644 --- a/extensions/BUILD.bazel +++ b/extensions/BUILD.bazel @@ -65,6 +65,23 @@ java_library( exports = ["//extensions/src/main/java/dev/cel/extensions:optional_library"], ) +java_library( + name = "optional_compiler_library", + visibility = ["//:internal"], + exports = ["//extensions/src/main/java/dev/cel/extensions:optional_compiler_library"], +) + +java_library( + name = "optional_runtime_library", + visibility = ["//:internal"], + exports = ["//extensions/src/main/java/dev/cel/extensions:optional_runtime_library"], +) + +cel_android_library( + name = "optional_runtime_library_android", + exports = ["//extensions/src/main/java/dev/cel/extensions:optional_runtime_library_android"], +) + java_library( name = "sets", exports = ["//extensions/src/main/java/dev/cel/extensions:sets"], diff --git a/extensions/src/main/java/dev/cel/extensions/BUILD.bazel b/extensions/src/main/java/dev/cel/extensions/BUILD.bazel index ef23c1603..e36c526fa 100644 --- a/extensions/src/main/java/dev/cel/extensions/BUILD.bazel +++ b/extensions/src/main/java/dev/cel/extensions/BUILD.bazel @@ -307,27 +307,87 @@ java_library( tags = [ ], deps = [ + ":extension_library", + ":optional_compiler_library", + ":optional_runtime_library", + "//checker:checker_builder", + "//common:cel_function_decl", + "//common:cel_var_decl", + "//common:options", + "//compiler:compiler_builder", + "//parser:macro", + "//parser:parser_builder", + "//runtime", + "//runtime:runtime_equality", + "@maven//:com_google_errorprone_error_prone_annotations", + "@maven//:com_google_guava_guava", + ], +) + +java_library( + name = "optional_compiler_library", + srcs = ["CelOptionalCompilerLibrary.java"], + tags = [ + ], + deps = [ + ":extension_library", "//checker:checker_builder", + "//common:cel_function_decl", + "//common:cel_issue", + "//common:cel_overload_decl", + "//common:cel_var_decl", "//common:compiler_common", "//common:operator", - "//common:options", "//common/ast", "//common/types", - "//common/values", - "//common/values:cel_byte_string", - "//common/values:cel_value", "//compiler:compiler_builder", - "//extensions:extension_library", "//parser:macro", "//parser:parser_builder", - "//runtime", + "@maven//:com_google_errorprone_error_prone_annotations", + "@maven//:com_google_guava_guava", + ], +) + +java_library( + name = "optional_runtime_library", + srcs = ["CelOptionalRuntimeLibrary.java"], + tags = [ + ], + deps = [ + "//common:operator", + "//common:options", + "//common/values", + "//common/values:cel_byte_string", + "//common/values:cel_value", "//runtime:function_binding", + "//runtime:lite_runtime", "//runtime:runtime_equality", + "@maven//:com_google_errorprone_error_prone_annotations", "@maven//:com_google_guava_guava", "@maven//:com_google_protobuf_protobuf_java", ], ) +cel_android_library( + name = "optional_runtime_library_android", + srcs = ["CelOptionalRuntimeLibrary.java"], + tags = [ + ], + deps = [ + "//common:operator_android", + "//common:options", + "//common/values:cel_byte_string", + "//common/values:cel_value_android", + "//common/values:values_android", + "//runtime:function_binding_android", + "//runtime:lite_runtime_android", + "//runtime:runtime_equality_android", + "@maven//:com_google_errorprone_error_prone_annotations", + "@maven_android//:com_google_guava_guava", + "@maven_android//:com_google_protobuf_protobuf_javalite", + ], +) + java_library( name = "sets", srcs = ["CelSetsExtensions.java"], diff --git a/extensions/src/main/java/dev/cel/extensions/CelExtensions.java b/extensions/src/main/java/dev/cel/extensions/CelExtensions.java index 7a6fc248e..e86531171 100644 --- a/extensions/src/main/java/dev/cel/extensions/CelExtensions.java +++ b/extensions/src/main/java/dev/cel/extensions/CelExtensions.java @@ -58,6 +58,30 @@ public static CelOptionalLibrary optional(int version) { return CelOptionalLibrary.library().version(version); } + /** + * Implementation of optional values. + * + *

Refer to README.md for available functions. + * + *

This will include only the specific functions denoted by {@link + * CelOptionalLibrary.Function}. + */ + public static CelOptionalLibrary optional(CelOptionalLibrary.Function... functions) { + return optional(ImmutableSet.copyOf(functions)); + } + + /** + * Implementation of optional values. + * + *

Refer to README.md for available functions. + * + *

This will include only the specific functions denoted by {@link + * CelOptionalLibrary.Function}. + */ + public static CelOptionalLibrary optional(Set functions) { + return new CelOptionalLibrary(functions); + } + /** * Extended functions for string manipulation. * diff --git a/extensions/src/main/java/dev/cel/extensions/CelOptionalCompilerLibrary.java b/extensions/src/main/java/dev/cel/extensions/CelOptionalCompilerLibrary.java new file mode 100644 index 000000000..766fbb8cc --- /dev/null +++ b/extensions/src/main/java/dev/cel/extensions/CelOptionalCompilerLibrary.java @@ -0,0 +1,492 @@ +// Copyright 2026 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package dev.cel.extensions; + +import static com.google.common.base.Preconditions.checkArgument; +import static com.google.common.base.Preconditions.checkNotNull; +import static com.google.common.collect.ImmutableSet.toImmutableSet; +import static dev.cel.extensions.CelOptionalCompilerLibrary.Function.HAS_VALUE; +import static dev.cel.extensions.CelOptionalCompilerLibrary.Function.OPTIONAL_NONE; +import static dev.cel.extensions.CelOptionalCompilerLibrary.Function.OPTIONAL_OF; +import static dev.cel.extensions.CelOptionalCompilerLibrary.Function.OPTIONAL_OF_NON_ZERO_VALUE; +import static dev.cel.extensions.CelOptionalCompilerLibrary.Function.OPTIONAL_UNWRAP; +import static dev.cel.extensions.CelOptionalCompilerLibrary.Function.OR; +import static dev.cel.extensions.CelOptionalCompilerLibrary.Function.OR_VALUE; +import static dev.cel.extensions.CelOptionalCompilerLibrary.Function.VALUE; + +import com.google.common.collect.ImmutableList; +import com.google.common.collect.ImmutableSet; +import com.google.errorprone.annotations.Immutable; +import dev.cel.checker.CelCheckerBuilder; +import dev.cel.common.CelFunctionDecl; +import dev.cel.common.CelIssue; +import dev.cel.common.CelOverloadDecl; +import dev.cel.common.CelVarDecl; +import dev.cel.common.Operator; +import dev.cel.common.ast.CelExpr; +import dev.cel.common.types.ListType; +import dev.cel.common.types.MapType; +import dev.cel.common.types.OptionalType; +import dev.cel.common.types.SimpleType; +import dev.cel.common.types.TypeParamType; +import dev.cel.common.types.TypeType; +import dev.cel.compiler.CelCompilerLibrary; +import dev.cel.parser.CelMacro; +import dev.cel.parser.CelMacroExprFactory; +import dev.cel.parser.CelParserBuilder; +import java.util.Optional; +import java.util.Set; + +/** Internal implementation of CEL optional compile-time extensions. */ +@Immutable +public final class CelOptionalCompilerLibrary + implements CelCompilerLibrary, CelExtensionLibrary.FeatureSet { + + private static final class Types { + private static final TypeParamType PARAM_TYPE_K = TypeParamType.create("K"); + private static final TypeParamType PARAM_TYPE_V = TypeParamType.create("V"); + private static final OptionalType OPTIONAL_TYPE_V = OptionalType.create(PARAM_TYPE_V); + private static final ListType LIST_TYPE_V = ListType.create(PARAM_TYPE_V); + private static final MapType MAP_TYPE_KV = MapType.create(PARAM_TYPE_K, PARAM_TYPE_V); + } + + /** Declarations for the optional extension library. */ + public enum OptionalDeclaration implements CelFunctionDecl.Declarer { + OPTIONAL_OF( + CelFunctionDecl.newFunctionDeclaration( + "optional.of", + CelOverloadDecl.newGlobalOverload( + "optional_of", Types.OPTIONAL_TYPE_V, Types.PARAM_TYPE_V))), + OPTIONAL_OF_NON_ZERO_VALUE( + CelFunctionDecl.newFunctionDeclaration( + "optional.ofNonZeroValue", + CelOverloadDecl.newGlobalOverload( + "optional_ofNonZeroValue", Types.OPTIONAL_TYPE_V, Types.PARAM_TYPE_V))), + OPTIONAL_NONE( + CelFunctionDecl.newFunctionDeclaration( + "optional.none", + CelOverloadDecl.newGlobalOverload("optional_none", Types.OPTIONAL_TYPE_V))), + OPTIONAL_VALUE( + CelFunctionDecl.newFunctionDeclaration( + "value", + CelOverloadDecl.newMemberOverload( + "optional_value", Types.PARAM_TYPE_V, Types.OPTIONAL_TYPE_V))), + OPTIONAL_HAS_VALUE( + CelFunctionDecl.newFunctionDeclaration( + "hasValue", + CelOverloadDecl.newMemberOverload( + "optional_hasValue", SimpleType.BOOL, Types.OPTIONAL_TYPE_V))), + OPTIONAL_UNWRAP( + CelFunctionDecl.newFunctionDeclaration( + "optional.unwrap", + CelOverloadDecl.newGlobalOverload( + "optional_unwrap_list", + Types.LIST_TYPE_V, + ListType.create(Types.OPTIONAL_TYPE_V)))), + OPTIONAL_OR( + CelFunctionDecl.newFunctionDeclaration( + "or", + CelOverloadDecl.newMemberOverload( + "optional_or_optional", + Types.OPTIONAL_TYPE_V, + Types.OPTIONAL_TYPE_V, + Types.OPTIONAL_TYPE_V))), + OPTIONAL_OR_VALUE( + CelFunctionDecl.newFunctionDeclaration( + "orValue", + CelOverloadDecl.newMemberOverload( + "optional_orValue_value", + Types.PARAM_TYPE_V, + Types.OPTIONAL_TYPE_V, + Types.PARAM_TYPE_V))), + OPTIONAL_SELECT( + CelFunctionDecl.newFunctionDeclaration( + Operator.OPTIONAL_SELECT.getFunction(), + CelOverloadDecl.newGlobalOverload( + "select_optional_field", + Types.OPTIONAL_TYPE_V, + SimpleType.DYN, + SimpleType.STRING))), + OPTIONAL_INDEX( + CelFunctionDecl.newFunctionDeclaration( + Operator.OPTIONAL_INDEX.getFunction(), + CelOverloadDecl.newGlobalOverload( + "list_optindex_optional_int", + Types.OPTIONAL_TYPE_V, + Types.LIST_TYPE_V, + SimpleType.INT), + CelOverloadDecl.newGlobalOverload( + "optional_list_optindex_optional_int", + Types.OPTIONAL_TYPE_V, + OptionalType.create(Types.LIST_TYPE_V), + SimpleType.INT), + CelOverloadDecl.newGlobalOverload( + "map_optindex_optional_value", + Types.OPTIONAL_TYPE_V, + Types.MAP_TYPE_KV, + Types.PARAM_TYPE_K), + CelOverloadDecl.newGlobalOverload( + "optional_map_optindex_optional_value", + Types.OPTIONAL_TYPE_V, + OptionalType.create(Types.MAP_TYPE_KV), + Types.PARAM_TYPE_K))), + OPTIONAL_INDEX_OPERAND( + CelFunctionDecl.newFunctionDeclaration( + Operator.INDEX.getFunction(), + CelOverloadDecl.newGlobalOverload( + "optional_list_index_int", + Types.OPTIONAL_TYPE_V, + OptionalType.create(Types.LIST_TYPE_V), + SimpleType.INT), + CelOverloadDecl.newGlobalOverload( + "optional_map_index_value", + Types.OPTIONAL_TYPE_V, + OptionalType.create(Types.MAP_TYPE_KV), + Types.PARAM_TYPE_K))); + + private final CelFunctionDecl celFunctionDecl; + + OptionalDeclaration(CelFunctionDecl celFunctionDecl) { + this.celFunctionDecl = celFunctionDecl; + } + + @Override + public CelFunctionDecl functionDecl() { + return celFunctionDecl; + } + } + + /** Enumerations of function names used for supporting optionals. */ + public enum Function { + VALUE(OptionalDeclaration.OPTIONAL_VALUE.functionDecl()), + HAS_VALUE(OptionalDeclaration.OPTIONAL_HAS_VALUE.functionDecl()), + OPTIONAL_NONE(OptionalDeclaration.OPTIONAL_NONE.functionDecl()), + OPTIONAL_OF(OptionalDeclaration.OPTIONAL_OF.functionDecl()), + OPTIONAL_UNWRAP(OptionalDeclaration.OPTIONAL_UNWRAP.functionDecl()), + OPTIONAL_OF_NON_ZERO_VALUE(OptionalDeclaration.OPTIONAL_OF_NON_ZERO_VALUE.functionDecl()), + OR(OptionalDeclaration.OPTIONAL_OR.functionDecl()), + OR_VALUE(OptionalDeclaration.OPTIONAL_OR_VALUE.functionDecl()), + FIRST( + CelFunctionDecl.newFunctionDeclaration( + "first", + CelOverloadDecl.newMemberOverload( + "optional_list_first", + "Return the first value in a list if present, otherwise optional.none()", + Types.OPTIONAL_TYPE_V, + Types.LIST_TYPE_V))), + LAST( + CelFunctionDecl.newFunctionDeclaration( + "last", + CelOverloadDecl.newMemberOverload( + "optional_list_last", + "Return the last value in a list if present, otherwise optional.none()", + Types.OPTIONAL_TYPE_V, + Types.LIST_TYPE_V))); + + private final CelFunctionDecl functionDecl; + + public String getFunction() { + return functionDecl.name(); + } + + CelFunctionDecl getFunctionDecl() { + return functionDecl; + } + + Function(CelFunctionDecl functionDecl) { + this.functionDecl = functionDecl; + } + } + + private static final String UNUSED_ITER_VAR = "#unused"; + private static final String OPTIONAL_MAP_VAR = "@target"; + + private static final ImmutableSet OPERATOR_FUNCTION_DECLS = + ImmutableSet.of( + OptionalDeclaration.OPTIONAL_SELECT.functionDecl(), + OptionalDeclaration.OPTIONAL_INDEX.functionDecl(), + OptionalDeclaration.OPTIONAL_INDEX_OPERAND.functionDecl()); + + private static final class Library implements CelExtensionLibrary { + private final CelOptionalCompilerLibrary version0 = + new CelOptionalCompilerLibrary( + 0, + ImmutableSet.of( + OPTIONAL_OF, + OPTIONAL_OF_NON_ZERO_VALUE, + OPTIONAL_NONE, + VALUE, + HAS_VALUE, + OPTIONAL_UNWRAP, + OR, + OR_VALUE), + ImmutableSet.of( + CelMacro.newReceiverMacro("optMap", 2, CelOptionalCompilerLibrary::expandOptMap)), + ImmutableSet.of( + CelVarDecl.newVarDeclaration( + OptionalType.NAME, TypeType.create(Types.OPTIONAL_TYPE_V)))); + + private final CelOptionalCompilerLibrary version1 = + new CelOptionalCompilerLibrary( + 1, + ImmutableSet.of( + OPTIONAL_OF, + OPTIONAL_OF_NON_ZERO_VALUE, + OPTIONAL_NONE, + VALUE, + HAS_VALUE, + OPTIONAL_UNWRAP, + OR, + OR_VALUE), + ImmutableSet.builder() + .addAll(version0.macros) + .add( + CelMacro.newReceiverMacro( + "optFlatMap", 2, CelOptionalCompilerLibrary::expandOptFlatMap)) + .build(), + version0.variables); + + private final CelOptionalCompilerLibrary version2 = + new CelOptionalCompilerLibrary( + 2, ImmutableSet.copyOf(Function.values()), version1.macros, version1.variables); + + @Override + public String name() { + return "optional"; + } + + @Override + public ImmutableSet versions() { + return ImmutableSet.of(version0, version1, version2); + } + } + + private static final Library LIBRARY = new Library(); + + public static CelExtensionLibrary library() { + return LIBRARY; + } + + /** Returns the latest version of the 'optional' compiler extension. */ + public static CelOptionalCompilerLibrary optional() { + return library().latest(); + } + + /** Returns the specified version of the 'optional' compiler extension. */ + public static CelOptionalCompilerLibrary optional(int version) { + return library().version(version); + } + + /** Returns the 'optional' compiler extension with only the specified functions. */ + public static CelOptionalCompilerLibrary optional(Function... functions) { + return optional(ImmutableSet.copyOf(functions)); + } + + /** Returns the 'optional' compiler extension with only the specified functions. */ + public static CelOptionalCompilerLibrary optional(Set functions) { + return new CelOptionalCompilerLibrary(functions); + } + + private final int version; + private final ImmutableSet functions; + private final ImmutableSet macros; + private final ImmutableSet variables; + + CelOptionalCompilerLibrary(Set functions) { + this( + -1, + functions, + ImmutableSet.of( + CelMacro.newReceiverMacro("optMap", 2, CelOptionalCompilerLibrary::expandOptMap), + CelMacro.newReceiverMacro( + "optFlatMap", 2, CelOptionalCompilerLibrary::expandOptFlatMap)), + ImmutableSet.of( + CelVarDecl.newVarDeclaration( + OptionalType.NAME, TypeType.create(Types.OPTIONAL_TYPE_V)))); + } + + private CelOptionalCompilerLibrary( + int version, Set functions, Set macros, Set variables) { + this.version = version; + this.functions = + ImmutableSet.builder() + .addAll(functions.stream().map(Function::getFunctionDecl).collect(toImmutableSet())) + .addAll(OPERATOR_FUNCTION_DECLS) + .build(); + this.macros = ImmutableSet.copyOf(macros); + this.variables = ImmutableSet.copyOf(variables); + } + + @Override + public int version() { + return version; + } + + @Override + public ImmutableSet functions() { + return functions; + } + + @Override + public ImmutableSet variables() { + return variables; + } + + @Override + public ImmutableSet macros() { + return macros; + } + + @Override + public void setParserOptions(CelParserBuilder parserBuilder) { + if (!parserBuilder.getOptions().enableOptionalSyntax()) { + // Enable optional syntax by default if the optional library has been added to the + // environment + parserBuilder.setOptions( + parserBuilder.getOptions().toBuilder().enableOptionalSyntax(true).build()); + } + parserBuilder.addMacros(macros()); + } + + @Override + public void setCheckerOptions(CelCheckerBuilder checkerBuilder) { + checkerBuilder.addVarDeclarations(variables()); + checkerBuilder.addFunctionDeclarations(functions()); + } + + private static Optional expandOptMap( + CelMacroExprFactory exprFactory, CelExpr target, ImmutableList arguments) { + checkNotNull(exprFactory); + checkNotNull(target); + checkArgument(arguments.size() == 2); + + CelExpr varIdent = checkNotNull(arguments.get(0)); + if (varIdent.exprKind().getKind() != CelExpr.ExprKind.Kind.IDENT) { + return Optional.of( + exprFactory.reportError( + CelIssue.formatError( + exprFactory.getSourceLocation(varIdent), + "optMap() variable name must be a simple identifier"))); + } + CelExpr mapExpr = checkNotNull(arguments.get(1)); + String varName = varIdent.ident().name(); + + if (target.exprKind().getKind() == CelExpr.ExprKind.Kind.IDENT) { + return Optional.of( + exprFactory.newGlobalCall( + Operator.CONDITIONAL.getFunction(), + exprFactory.newReceiverCall(HAS_VALUE.getFunction(), target), + exprFactory.newGlobalCall( + OPTIONAL_OF.getFunction(), + exprFactory.fold( + UNUSED_ITER_VAR, + exprFactory.newList(), + varName, + exprFactory.newReceiverCall(VALUE.getFunction(), exprFactory.copy(target)), + exprFactory.newBoolLiteral(true), + exprFactory.newIdentifier(varName), + mapExpr)), + exprFactory.newGlobalCall(OPTIONAL_NONE.getFunction()))); + } + + CelExpr localVar = exprFactory.newIdentifier(OPTIONAL_MAP_VAR); + CelExpr localVarCopy = exprFactory.copy(localVar); + CelExpr conditionalExpr = + exprFactory.newGlobalCall( + Operator.CONDITIONAL.getFunction(), + exprFactory.newReceiverCall(HAS_VALUE.getFunction(), localVar), + exprFactory.newGlobalCall( + OPTIONAL_OF.getFunction(), + exprFactory.fold( + UNUSED_ITER_VAR, + exprFactory.newList(), + varName, + exprFactory.newReceiverCall(VALUE.getFunction(), localVarCopy), + exprFactory.newBoolLiteral(true), + exprFactory.newIdentifier(varName), + mapExpr)), + exprFactory.newGlobalCall(OPTIONAL_NONE.getFunction())); + + return Optional.of( + exprFactory.fold( + UNUSED_ITER_VAR, + exprFactory.newList(), + OPTIONAL_MAP_VAR, + target, + exprFactory.newBoolLiteral(false), + exprFactory.newIdentifier(OPTIONAL_MAP_VAR), + conditionalExpr)); + } + + private static Optional expandOptFlatMap( + CelMacroExprFactory exprFactory, CelExpr target, ImmutableList arguments) { + checkNotNull(exprFactory); + checkNotNull(target); + checkArgument(arguments.size() == 2); + + CelExpr varIdent = checkNotNull(arguments.get(0)); + if (varIdent.exprKind().getKind() != CelExpr.ExprKind.Kind.IDENT) { + return Optional.of( + exprFactory.reportError( + CelIssue.formatError( + exprFactory.getSourceLocation(varIdent), + "optFlatMap() variable name must be a simple identifier"))); + } + CelExpr mapExpr = checkNotNull(arguments.get(1)); + String varName = varIdent.ident().name(); + + if (target.exprKind().getKind() == CelExpr.ExprKind.Kind.IDENT) { + return Optional.of( + exprFactory.newGlobalCall( + Operator.CONDITIONAL.getFunction(), + exprFactory.newReceiverCall(HAS_VALUE.getFunction(), target), + exprFactory.fold( + UNUSED_ITER_VAR, + exprFactory.newList(), + varName, + exprFactory.newReceiverCall(VALUE.getFunction(), exprFactory.copy(target)), + exprFactory.newBoolLiteral(true), + exprFactory.newIdentifier(varName), + mapExpr), + exprFactory.newGlobalCall(OPTIONAL_NONE.getFunction()))); + } + + CelExpr localVar = exprFactory.newIdentifier(OPTIONAL_MAP_VAR); + CelExpr localVarCopy = exprFactory.copy(localVar); + CelExpr conditionalExpr = + exprFactory.newGlobalCall( + Operator.CONDITIONAL.getFunction(), + exprFactory.newReceiverCall(HAS_VALUE.getFunction(), localVar), + exprFactory.fold( + UNUSED_ITER_VAR, + exprFactory.newList(), + varName, + exprFactory.newReceiverCall(VALUE.getFunction(), localVarCopy), + exprFactory.newBoolLiteral(true), + exprFactory.newIdentifier(varName), + mapExpr), + exprFactory.newGlobalCall(OPTIONAL_NONE.getFunction())); + + return Optional.of( + exprFactory.fold( + UNUSED_ITER_VAR, + exprFactory.newList(), + OPTIONAL_MAP_VAR, + target, + exprFactory.newBoolLiteral(false), + exprFactory.newIdentifier(OPTIONAL_MAP_VAR), + conditionalExpr)); + } +} diff --git a/extensions/src/main/java/dev/cel/extensions/CelOptionalLibrary.java b/extensions/src/main/java/dev/cel/extensions/CelOptionalLibrary.java index 85ee9c756..a83130457 100644 --- a/extensions/src/main/java/dev/cel/extensions/CelOptionalLibrary.java +++ b/extensions/src/main/java/dev/cel/extensions/CelOptionalLibrary.java @@ -14,280 +14,115 @@ package dev.cel.extensions; -import static com.google.common.base.Preconditions.checkArgument; -import static com.google.common.base.Preconditions.checkNotNull; -import static com.google.common.collect.ImmutableList.toImmutableList; -import static dev.cel.common.Operator.INDEX; -import static dev.cel.common.Operator.OPTIONAL_INDEX; -import static dev.cel.common.Operator.OPTIONAL_SELECT; -import static dev.cel.extensions.CelOptionalLibrary.Function.FIRST; -import static dev.cel.extensions.CelOptionalLibrary.Function.HAS_VALUE; -import static dev.cel.extensions.CelOptionalLibrary.Function.LAST; -import static dev.cel.extensions.CelOptionalLibrary.Function.OPTIONAL_NONE; -import static dev.cel.extensions.CelOptionalLibrary.Function.OPTIONAL_OF; -import static dev.cel.extensions.CelOptionalLibrary.Function.OPTIONAL_OF_NON_ZERO_VALUE; -import static dev.cel.extensions.CelOptionalLibrary.Function.OPTIONAL_UNWRAP; -import static dev.cel.extensions.CelOptionalLibrary.Function.VALUE; -import static dev.cel.runtime.CelFunctionBinding.fromOverloads; +import static com.google.common.collect.ImmutableSet.toImmutableSet; -import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableSet; -import com.google.common.collect.Iterables; -import com.google.common.primitives.Ints; -import com.google.common.primitives.UnsignedLong; -import com.google.protobuf.Duration; -import com.google.protobuf.Message; -import com.google.protobuf.Timestamp; +import com.google.errorprone.annotations.Immutable; import dev.cel.checker.CelCheckerBuilder; import dev.cel.common.CelFunctionDecl; -import dev.cel.common.CelIssue; import dev.cel.common.CelOptions; -import dev.cel.common.CelOverloadDecl; import dev.cel.common.CelVarDecl; -import dev.cel.common.Operator; -import dev.cel.common.ast.CelExpr; -import dev.cel.common.types.ListType; -import dev.cel.common.types.MapType; -import dev.cel.common.types.OptionalType; -import dev.cel.common.types.SimpleType; -import dev.cel.common.types.TypeParamType; -import dev.cel.common.types.TypeType; -import dev.cel.common.values.CelByteString; -import dev.cel.common.values.CelValue; -import dev.cel.common.values.NullValue; import dev.cel.compiler.CelCompilerLibrary; import dev.cel.parser.CelMacro; -import dev.cel.parser.CelMacroExprFactory; import dev.cel.parser.CelParserBuilder; -import dev.cel.runtime.CelFunctionBinding; import dev.cel.runtime.CelInternalRuntimeLibrary; import dev.cel.runtime.CelRuntimeBuilder; import dev.cel.runtime.RuntimeEquality; -import java.time.Instant; -import java.util.Collection; -import java.util.List; -import java.util.Map; -import java.util.Optional; +import java.util.Set; /** Internal implementation of CEL optional values. */ +@Immutable public final class CelOptionalLibrary implements CelCompilerLibrary, CelInternalRuntimeLibrary, CelExtensionLibrary.FeatureSet { /** Enumerations of function names used for supporting optionals. */ public enum Function { - VALUE("value"), - HAS_VALUE("hasValue"), - OPTIONAL_NONE("optional.none"), - OPTIONAL_OF("optional.of"), - OPTIONAL_UNWRAP("optional.unwrap"), - OPTIONAL_OF_NON_ZERO_VALUE("optional.ofNonZeroValue"), - OR("or"), - OR_VALUE("orValue"), - FIRST("first"), - LAST("last"); + VALUE(CelOptionalCompilerLibrary.Function.VALUE, CelOptionalRuntimeLibrary.Function.VALUE), + HAS_VALUE( + CelOptionalCompilerLibrary.Function.HAS_VALUE, + CelOptionalRuntimeLibrary.Function.HAS_VALUE), + OPTIONAL_NONE( + CelOptionalCompilerLibrary.Function.OPTIONAL_NONE, + CelOptionalRuntimeLibrary.Function.OPTIONAL_NONE), + OPTIONAL_OF( + CelOptionalCompilerLibrary.Function.OPTIONAL_OF, + CelOptionalRuntimeLibrary.Function.OPTIONAL_OF), + OPTIONAL_UNWRAP( + CelOptionalCompilerLibrary.Function.OPTIONAL_UNWRAP, + CelOptionalRuntimeLibrary.Function.OPTIONAL_UNWRAP), + OPTIONAL_OF_NON_ZERO_VALUE( + CelOptionalCompilerLibrary.Function.OPTIONAL_OF_NON_ZERO_VALUE, + CelOptionalRuntimeLibrary.Function.OPTIONAL_OF_NON_ZERO_VALUE), + OR(CelOptionalCompilerLibrary.Function.OR, CelOptionalRuntimeLibrary.Function.OR), + OR_VALUE( + CelOptionalCompilerLibrary.Function.OR_VALUE, CelOptionalRuntimeLibrary.Function.OR_VALUE), + FIRST(CelOptionalCompilerLibrary.Function.FIRST, CelOptionalRuntimeLibrary.Function.FIRST), + LAST(CelOptionalCompilerLibrary.Function.LAST, CelOptionalRuntimeLibrary.Function.LAST); - private final String functionName; + private final CelOptionalCompilerLibrary.Function compilerFunction; + private final CelOptionalRuntimeLibrary.Function runtimeFunction; public String getFunction() { - return functionName; + return compilerFunction.getFunction(); } - Function(String functionName) { - this.functionName = functionName; + Function( + CelOptionalCompilerLibrary.Function compilerFunction, + CelOptionalRuntimeLibrary.Function runtimeFunction) { + this.compilerFunction = compilerFunction; + this.runtimeFunction = runtimeFunction; } } - private static final class Types { - private static final TypeParamType PARAM_TYPE_K = TypeParamType.create("K"); - private static final TypeParamType PARAM_TYPE_V = TypeParamType.create("V"); - private static final OptionalType OPTIONAL_TYPE_V = OptionalType.create(PARAM_TYPE_V); - private static final ListType LIST_TYPE_V = ListType.create(PARAM_TYPE_V); - private static final MapType MAP_TYPE_KV = MapType.create(PARAM_TYPE_K, PARAM_TYPE_V); - } - /** Declarations for the optional extension library. */ public enum OptionalDeclaration implements CelFunctionDecl.Declarer { - OPTIONAL_OF( - CelFunctionDecl.newFunctionDeclaration( - Function.OPTIONAL_OF.getFunction(), - CelOverloadDecl.newGlobalOverload( - "optional_of", Types.OPTIONAL_TYPE_V, Types.PARAM_TYPE_V))), + OPTIONAL_OF(CelOptionalCompilerLibrary.OptionalDeclaration.OPTIONAL_OF), OPTIONAL_OF_NON_ZERO_VALUE( - CelFunctionDecl.newFunctionDeclaration( - Function.OPTIONAL_OF_NON_ZERO_VALUE.getFunction(), - CelOverloadDecl.newGlobalOverload( - "optional_ofNonZeroValue", Types.OPTIONAL_TYPE_V, Types.PARAM_TYPE_V))), - OPTIONAL_NONE( - CelFunctionDecl.newFunctionDeclaration( - Function.OPTIONAL_NONE.getFunction(), - CelOverloadDecl.newGlobalOverload("optional_none", Types.OPTIONAL_TYPE_V))), - OPTIONAL_VALUE( - CelFunctionDecl.newFunctionDeclaration( - Function.VALUE.getFunction(), - CelOverloadDecl.newMemberOverload( - "optional_value", Types.PARAM_TYPE_V, Types.OPTIONAL_TYPE_V))), - OPTIONAL_HAS_VALUE( - CelFunctionDecl.newFunctionDeclaration( - Function.HAS_VALUE.getFunction(), - CelOverloadDecl.newMemberOverload( - "optional_hasValue", SimpleType.BOOL, Types.OPTIONAL_TYPE_V))), - OPTIONAL_UNWRAP( - CelFunctionDecl.newFunctionDeclaration( - Function.OPTIONAL_UNWRAP.getFunction(), - CelOverloadDecl.newGlobalOverload( - "optional_unwrap_list", - Types.LIST_TYPE_V, - ListType.create(Types.OPTIONAL_TYPE_V)))), - OPTIONAL_OR( - CelFunctionDecl.newFunctionDeclaration( - "or", - CelOverloadDecl.newMemberOverload( - "optional_or_optional", - Types.OPTIONAL_TYPE_V, - Types.OPTIONAL_TYPE_V, - Types.OPTIONAL_TYPE_V))), - OPTIONAL_OR_VALUE( - CelFunctionDecl.newFunctionDeclaration( - "orValue", - CelOverloadDecl.newMemberOverload( - "optional_orValue_value", - Types.PARAM_TYPE_V, - Types.OPTIONAL_TYPE_V, - Types.PARAM_TYPE_V))), - OPTIONAL_SELECT( - CelFunctionDecl.newFunctionDeclaration( - Operator.OPTIONAL_SELECT.getFunction(), - CelOverloadDecl.newGlobalOverload( - "select_optional_field", - Types.OPTIONAL_TYPE_V, - SimpleType.DYN, - SimpleType.STRING))), - OPTIONAL_INDEX( - CelFunctionDecl.newFunctionDeclaration( - Operator.OPTIONAL_INDEX.getFunction(), - CelOverloadDecl.newGlobalOverload( - "list_optindex_optional_int", - Types.OPTIONAL_TYPE_V, - Types.LIST_TYPE_V, - SimpleType.INT), - CelOverloadDecl.newGlobalOverload( - "optional_list_optindex_optional_int", - Types.OPTIONAL_TYPE_V, - OptionalType.create(Types.LIST_TYPE_V), - SimpleType.INT), - CelOverloadDecl.newGlobalOverload( - "map_optindex_optional_value", - Types.OPTIONAL_TYPE_V, - Types.MAP_TYPE_KV, - Types.PARAM_TYPE_K), - CelOverloadDecl.newGlobalOverload( - "optional_map_optindex_optional_value", - Types.OPTIONAL_TYPE_V, - OptionalType.create(Types.MAP_TYPE_KV), - Types.PARAM_TYPE_K))), - OPTIONAL_INDEX_OPERAND( - CelFunctionDecl.newFunctionDeclaration( - Operator.INDEX.getFunction(), - CelOverloadDecl.newGlobalOverload( - "optional_list_index_int", - Types.OPTIONAL_TYPE_V, - OptionalType.create(Types.LIST_TYPE_V), - SimpleType.INT), - CelOverloadDecl.newGlobalOverload( - "optional_map_index_value", - Types.OPTIONAL_TYPE_V, - OptionalType.create(Types.MAP_TYPE_KV), - Types.PARAM_TYPE_K))); + CelOptionalCompilerLibrary.OptionalDeclaration.OPTIONAL_OF_NON_ZERO_VALUE), + OPTIONAL_NONE(CelOptionalCompilerLibrary.OptionalDeclaration.OPTIONAL_NONE), + OPTIONAL_VALUE(CelOptionalCompilerLibrary.OptionalDeclaration.OPTIONAL_VALUE), + OPTIONAL_HAS_VALUE(CelOptionalCompilerLibrary.OptionalDeclaration.OPTIONAL_HAS_VALUE), + OPTIONAL_UNWRAP(CelOptionalCompilerLibrary.OptionalDeclaration.OPTIONAL_UNWRAP), + OPTIONAL_OR(CelOptionalCompilerLibrary.OptionalDeclaration.OPTIONAL_OR), + OPTIONAL_OR_VALUE(CelOptionalCompilerLibrary.OptionalDeclaration.OPTIONAL_OR_VALUE), + OPTIONAL_SELECT(CelOptionalCompilerLibrary.OptionalDeclaration.OPTIONAL_SELECT), + OPTIONAL_INDEX(CelOptionalCompilerLibrary.OptionalDeclaration.OPTIONAL_INDEX), + OPTIONAL_INDEX_OPERAND(CelOptionalCompilerLibrary.OptionalDeclaration.OPTIONAL_INDEX_OPERAND); - private final CelFunctionDecl celFunctionDecl; + private final CelOptionalCompilerLibrary.OptionalDeclaration declaration; - OptionalDeclaration(CelFunctionDecl celFunctionDecl) { - this.celFunctionDecl = celFunctionDecl; + OptionalDeclaration(CelOptionalCompilerLibrary.OptionalDeclaration declaration) { + this.declaration = declaration; } @Override public CelFunctionDecl functionDecl() { - return celFunctionDecl; + return declaration.functionDecl(); } } - private static final CelExtensionLibrary LIBRARY = - new CelExtensionLibrary() { - private final CelOptionalLibrary version0 = - new CelOptionalLibrary( - 0, - ImmutableSet.of( - OptionalDeclaration.OPTIONAL_OF.functionDecl(), - OptionalDeclaration.OPTIONAL_OF_NON_ZERO_VALUE.functionDecl(), - OptionalDeclaration.OPTIONAL_NONE.functionDecl(), - OptionalDeclaration.OPTIONAL_VALUE.functionDecl(), - OptionalDeclaration.OPTIONAL_HAS_VALUE.functionDecl(), - OptionalDeclaration.OPTIONAL_UNWRAP.functionDecl(), - // Note: Implementation of "or" and "orValue" are special-cased inside the - // interpreter. Hence, their bindings are not provided here. - OptionalDeclaration.OPTIONAL_OR.functionDecl(), - OptionalDeclaration.OPTIONAL_OR_VALUE.functionDecl(), - // Note: Function bindings for optional field selection and indexer is defined - // in {@code StandardFunctions}. - OptionalDeclaration.OPTIONAL_SELECT.functionDecl(), - OptionalDeclaration.OPTIONAL_INDEX.functionDecl(), - // Index overloads to accommodate using an optional value as the operand - OptionalDeclaration.OPTIONAL_INDEX_OPERAND.functionDecl()), - ImmutableSet.of( - CelMacro.newReceiverMacro("optMap", 2, CelOptionalLibrary::expandOptMap)), - ImmutableSet.of( - // Type declaration for optional_type -> type(optional_type(V)) - CelVarDecl.newVarDeclaration( - OptionalType.NAME, TypeType.create(Types.OPTIONAL_TYPE_V)))); + private static final class Library implements CelExtensionLibrary { + private final ImmutableSet versions; - private final CelOptionalLibrary version1 = - new CelOptionalLibrary( - 1, - version0.functions, - ImmutableSet.builder() - .addAll(version0.macros) - .add( - CelMacro.newReceiverMacro( - "optFlatMap", 2, CelOptionalLibrary::expandOptFlatMap)) - .build(), - version0.variables); + Library() { + versions = + CelOptionalCompilerLibrary.library().versions().stream() + .map(CelOptionalLibrary::new) + .collect(toImmutableSet()); + } - private final CelOptionalLibrary version2 = - new CelOptionalLibrary( - 2, - ImmutableSet.builder() - .addAll(version1.functions) - .add( - CelFunctionDecl.newFunctionDeclaration( - FIRST.functionName, - CelOverloadDecl.newMemberOverload( - "optional_list_first", - "Return the first value in a list if present, otherwise" - + " optional.none()", - Types.OPTIONAL_TYPE_V, - Types.LIST_TYPE_V)), - CelFunctionDecl.newFunctionDeclaration( - LAST.functionName, - CelOverloadDecl.newMemberOverload( - "optional_list_last", - "Return the last value in a list if present, otherwise" - + " optional.none()", - Types.OPTIONAL_TYPE_V, - Types.LIST_TYPE_V))) - .build(), - version1.macros, - version1.variables); + @Override + public String name() { + return CelOptionalCompilerLibrary.library().name(); + } - @Override - public String name() { - return "optional"; - } + @Override + public ImmutableSet versions() { + return versions; + } + } - @Override - public ImmutableSet versions() { - return ImmutableSet.of(version0, version1, version2); - } - }; + private static final Library LIBRARY = new Library(); static CelExtensionLibrary library() { return LIBRARY; @@ -296,60 +131,55 @@ static CelExtensionLibrary library() { // TODO migrate from this constant to the CelExtensions.optional() public static final CelOptionalLibrary INSTANCE = CelOptionalLibrary.library().latest(); - private static final String UNUSED_ITER_VAR = "#unused"; - private static final String OPTIONAL_MAP_VAR = "@target"; + private final CelOptionalCompilerLibrary compilerLibrary; + private final CelOptionalRuntimeLibrary runtimeLibrary; - private final int version; - private final ImmutableSet functions; - private final ImmutableSet macros; - private final ImmutableSet variables; + CelOptionalLibrary() { + this(CelOptionalCompilerLibrary.optional()); + } - CelOptionalLibrary( - int version, - ImmutableSet functions, - ImmutableSet macros, - ImmutableSet variables) { - this.version = version; - this.functions = functions; - this.macros = macros; - this.variables = variables; + CelOptionalLibrary(Set functions) { + this.compilerLibrary = + new CelOptionalCompilerLibrary( + functions.stream().map(f -> f.compilerFunction).collect(toImmutableSet())); + this.runtimeLibrary = + new CelOptionalRuntimeLibrary( + functions.stream().map(f -> f.runtimeFunction).collect(toImmutableSet())); + } + + private CelOptionalLibrary(CelOptionalCompilerLibrary compilerLibrary) { + this.compilerLibrary = compilerLibrary; + this.runtimeLibrary = CelOptionalRuntimeLibrary.optional(compilerLibrary.version()); } @Override public int version() { - return version; + return compilerLibrary.version(); } @Override public ImmutableSet functions() { - return functions; + return compilerLibrary.functions(); } @Override public ImmutableSet variables() { - return variables; + return compilerLibrary.variables(); } @Override public ImmutableSet macros() { - return macros; + return compilerLibrary.macros(); } @Override public void setParserOptions(CelParserBuilder parserBuilder) { - if (!parserBuilder.getOptions().enableOptionalSyntax()) { - // Enable optional syntax by default if the optional library has been added to the - // environment - parserBuilder.setOptions( - parserBuilder.getOptions().toBuilder().enableOptionalSyntax(true).build()); - } - parserBuilder.addMacros(macros()); + compilerLibrary.setParserOptions(parserBuilder); } @Override public void setCheckerOptions(CelCheckerBuilder checkerBuilder) { - checkerBuilder.addVarDeclarations(variables()); - checkerBuilder.addFunctionDeclarations(functions()); + compilerLibrary.setCheckerOptions(checkerBuilder); } @Override @@ -357,316 +187,9 @@ public void setRuntimeOptions(CelRuntimeBuilder runtimeBuilder) { throw new UnsupportedOperationException("Unsupported"); } - @SuppressWarnings({"unchecked", "rawtypes"}) @Override public void setRuntimeOptions( CelRuntimeBuilder runtimeBuilder, RuntimeEquality runtimeEquality, CelOptions celOptions) { - runtimeBuilder.addFunctionBindings( - fromOverloads( - OPTIONAL_OF.getFunction(), - CelFunctionBinding.from("optional_of", Object.class, Optional::of))); - runtimeBuilder.addFunctionBindings( - fromOverloads( - OPTIONAL_OF_NON_ZERO_VALUE.getFunction(), - CelFunctionBinding.from( - "optional_ofNonZeroValue", - Object.class, - val -> { - if (isZeroValue(val)) { - return Optional.empty(); - } - return Optional.of(val); - }))); - runtimeBuilder.addFunctionBindings( - fromOverloads( - OPTIONAL_UNWRAP.getFunction(), - CelFunctionBinding.from( - "optional_unwrap_list", - Collection.class, - CelOptionalLibrary::elideOptionalCollection))); - runtimeBuilder.addFunctionBindings( - fromOverloads( - OPTIONAL_NONE.getFunction(), - CelFunctionBinding.from("optional_none", ImmutableList.of(), val -> Optional.empty()))); - runtimeBuilder.addFunctionBindings( - fromOverloads( - VALUE.getFunction(), - CelFunctionBinding.from( - "optional_value", Object.class, val -> ((Optional) val).get()))); - runtimeBuilder.addFunctionBindings( - fromOverloads( - HAS_VALUE.getFunction(), - CelFunctionBinding.from( - "optional_hasValue", Object.class, val -> ((Optional) val).isPresent()))); - - runtimeBuilder.addFunctionBindings( - fromOverloads( - OPTIONAL_SELECT.getFunction(), - CelFunctionBinding.from( - "select_optional_field", // This only handles map selection. Proto selection is - // special cased inside the interpreter. - Map.class, - String.class, - runtimeEquality::findInMap))); - - runtimeBuilder.addFunctionBindings( - fromOverloads( - OPTIONAL_INDEX.getFunction(), - CelFunctionBinding.from( - "list_optindex_optional_int", - List.class, - Long.class, - (List list, Long index) -> { - int castIndex = Ints.checkedCast(index); - if (castIndex < 0 || castIndex >= list.size()) { - return Optional.empty(); - } - return Optional.of(list.get(castIndex)); - }), - CelFunctionBinding.from( - "optional_list_optindex_optional_int", - Optional.class, - Long.class, - CelOptionalLibrary::indexOptionalList), - CelFunctionBinding.from( - "map_optindex_optional_value", Map.class, Object.class, runtimeEquality::findInMap), - CelFunctionBinding.from( - "optional_map_optindex_optional_value", - Optional.class, - Object.class, - (Optional optionalMap, Object key) -> - indexOptionalMap(optionalMap, key, runtimeEquality)))); - - runtimeBuilder.addFunctionBindings( - fromOverloads( - INDEX.getFunction(), - CelFunctionBinding.from( - "optional_list_index_int", - Optional.class, - Long.class, - CelOptionalLibrary::indexOptionalList), - CelFunctionBinding.from( - "optional_map_index_value", - Optional.class, - Object.class, - (Optional optionalMap, Object key) -> - indexOptionalMap(optionalMap, key, runtimeEquality)))); - - if (version >= 2) { - runtimeBuilder.addFunctionBindings( - fromOverloads( - "first", - CelFunctionBinding.from( - "optional_list_first", Collection.class, CelOptionalLibrary::listOptionalFirst))); - runtimeBuilder.addFunctionBindings( - fromOverloads( - "last", - CelFunctionBinding.from( - "optional_list_last", Collection.class, CelOptionalLibrary::listOptionalLast))); - } - } - - private static ImmutableList elideOptionalCollection(Collection> list) { - return list.stream().filter(Optional::isPresent).map(Optional::get).collect(toImmutableList()); - } - - private static boolean isZeroValue(Object val) { - if (val instanceof Boolean) { - return !((Boolean) val); - } else if (val instanceof Long) { - return (Long) val == 0L; - } else if (val instanceof Double) { - return (Double) val == 0.0d; - } else if (val instanceof String) { - return ((String) val).isEmpty(); - } else if (val instanceof UnsignedLong) { - return val.equals(UnsignedLong.ZERO); - } else if (val instanceof Collection) { - return ((Collection) val).isEmpty(); - } else if (val instanceof Map) { - return ((Map) val).isEmpty(); - } else if (val instanceof CelByteString) { - return ((CelByteString) val).isEmpty(); - } else if (val instanceof Duration) { - return val.equals(((Duration) val).getDefaultInstanceForType()); - } else if (val instanceof Timestamp) { - return val.equals(((Timestamp) val).getDefaultInstanceForType()); - } else if (val instanceof Message) { - return val.equals(((Message) val).getDefaultInstanceForType()); - } else if (val instanceof NullValue) { - // A null value always represents an absent value - return true; - } else if (val instanceof Instant) { - return val.equals(Instant.EPOCH); - } else if (val instanceof java.time.Duration) { - return val.equals(java.time.Duration.ZERO); - } else if (val instanceof CelValue) { - return ((CelValue) val).isZeroValue(); - } - - // Unknown. Assume that it is non-zero. - return false; - } - - private static Optional expandOptMap( - CelMacroExprFactory exprFactory, CelExpr target, ImmutableList arguments) { - checkNotNull(exprFactory); - checkNotNull(target); - checkArgument(arguments.size() == 2); - - CelExpr varIdent = checkNotNull(arguments.get(0)); - if (varIdent.exprKind().getKind() != CelExpr.ExprKind.Kind.IDENT) { - return Optional.of( - exprFactory.reportError( - CelIssue.formatError( - exprFactory.getSourceLocation(varIdent), - "optMap() variable name must be a simple identifier"))); - } - CelExpr mapExpr = checkNotNull(arguments.get(1)); - String varName = varIdent.ident().name(); - - if (target.exprKind().getKind() == CelExpr.ExprKind.Kind.IDENT) { - return Optional.of( - exprFactory.newGlobalCall( - Operator.CONDITIONAL.getFunction(), - exprFactory.newReceiverCall(HAS_VALUE.getFunction(), target), - exprFactory.newGlobalCall( - OPTIONAL_OF.getFunction(), - exprFactory.fold( - UNUSED_ITER_VAR, - exprFactory.newList(), - varName, - exprFactory.newReceiverCall(VALUE.getFunction(), exprFactory.copy(target)), - exprFactory.newBoolLiteral(true), - exprFactory.newIdentifier(varName), - mapExpr)), - exprFactory.newGlobalCall(OPTIONAL_NONE.getFunction()))); - } - - CelExpr localVar = exprFactory.newIdentifier(OPTIONAL_MAP_VAR); - CelExpr localVarCopy = exprFactory.copy(localVar); - CelExpr conditionalExpr = - exprFactory.newGlobalCall( - Operator.CONDITIONAL.getFunction(), - exprFactory.newReceiverCall(HAS_VALUE.getFunction(), localVar), - exprFactory.newGlobalCall( - OPTIONAL_OF.getFunction(), - exprFactory.fold( - UNUSED_ITER_VAR, - exprFactory.newList(), - varName, - exprFactory.newReceiverCall(VALUE.getFunction(), localVarCopy), - exprFactory.newBoolLiteral(true), - exprFactory.newIdentifier(varName), - mapExpr)), - exprFactory.newGlobalCall(OPTIONAL_NONE.getFunction())); - - return Optional.of( - exprFactory.fold( - UNUSED_ITER_VAR, - exprFactory.newList(), - OPTIONAL_MAP_VAR, - target, - exprFactory.newBoolLiteral(false), - exprFactory.newIdentifier(OPTIONAL_MAP_VAR), - conditionalExpr)); - } - - private static Optional expandOptFlatMap( - CelMacroExprFactory exprFactory, CelExpr target, ImmutableList arguments) { - checkNotNull(exprFactory); - checkNotNull(target); - checkArgument(arguments.size() == 2); - - CelExpr varIdent = checkNotNull(arguments.get(0)); - if (varIdent.exprKind().getKind() != CelExpr.ExprKind.Kind.IDENT) { - return Optional.of( - exprFactory.reportError( - CelIssue.formatError( - exprFactory.getSourceLocation(varIdent), - "optFlatMap() variable name must be a simple identifier"))); - } - CelExpr mapExpr = checkNotNull(arguments.get(1)); - String varName = varIdent.ident().name(); - - if (target.exprKind().getKind() == CelExpr.ExprKind.Kind.IDENT) { - return Optional.of( - exprFactory.newGlobalCall( - Operator.CONDITIONAL.getFunction(), - exprFactory.newReceiverCall(HAS_VALUE.getFunction(), target), - exprFactory.fold( - UNUSED_ITER_VAR, - exprFactory.newList(), - varName, - exprFactory.newReceiverCall(VALUE.getFunction(), exprFactory.copy(target)), - exprFactory.newBoolLiteral(true), - exprFactory.newIdentifier(varName), - mapExpr), - exprFactory.newGlobalCall(OPTIONAL_NONE.getFunction()))); - } - - CelExpr localVar = exprFactory.newIdentifier(OPTIONAL_MAP_VAR); - CelExpr localVarCopy = exprFactory.copy(localVar); - CelExpr conditionalExpr = - exprFactory.newGlobalCall( - Operator.CONDITIONAL.getFunction(), - exprFactory.newReceiverCall(HAS_VALUE.getFunction(), localVar), - exprFactory.fold( - UNUSED_ITER_VAR, - exprFactory.newList(), - varName, - exprFactory.newReceiverCall(VALUE.getFunction(), localVarCopy), - exprFactory.newBoolLiteral(true), - exprFactory.newIdentifier(varName), - mapExpr), - exprFactory.newGlobalCall(OPTIONAL_NONE.getFunction())); - - return Optional.of( - exprFactory.fold( - UNUSED_ITER_VAR, - exprFactory.newList(), - OPTIONAL_MAP_VAR, - target, - exprFactory.newBoolLiteral(false), - exprFactory.newIdentifier(OPTIONAL_MAP_VAR), - conditionalExpr)); - } - - private static Object indexOptionalMap( - Optional optionalMap, Object key, RuntimeEquality runtimeEquality) { - if (!optionalMap.isPresent()) { - return Optional.empty(); - } - - Map map = (Map) optionalMap.get(); - - return runtimeEquality.findInMap(map, key); - } - - private static Object indexOptionalList(Optional optionalList, long index) { - if (!optionalList.isPresent()) { - return Optional.empty(); - } - List list = (List) optionalList.get(); - int castIndex = Ints.checkedCast(index); - if (castIndex < 0 || castIndex >= list.size()) { - return Optional.empty(); - } - return Optional.of(list.get(castIndex)); - } - - @SuppressWarnings("rawtypes") - private static Object listOptionalFirst(Collection list) { - if (list.isEmpty()) { - return Optional.empty(); - } - if (list instanceof List) { - return Optional.ofNullable(((List) list).get(0)); - } - return Optional.ofNullable(Iterables.getFirst(list, null)); - } - - private static Object listOptionalLast(Collection list) { - return Optional.ofNullable(Iterables.getLast(list, null)); + runtimeBuilder.addFunctionBindings(runtimeLibrary.newFunctionBindings(runtimeEquality)); } } diff --git a/extensions/src/main/java/dev/cel/extensions/CelOptionalRuntimeLibrary.java b/extensions/src/main/java/dev/cel/extensions/CelOptionalRuntimeLibrary.java new file mode 100644 index 000000000..1959e99a8 --- /dev/null +++ b/extensions/src/main/java/dev/cel/extensions/CelOptionalRuntimeLibrary.java @@ -0,0 +1,338 @@ +// Copyright 2026 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package dev.cel.extensions; + +import com.google.common.collect.ImmutableList; +import com.google.common.collect.ImmutableSet; +import com.google.common.collect.Iterables; +import com.google.common.primitives.Ints; +import com.google.common.primitives.UnsignedLong; +import com.google.errorprone.annotations.Immutable; +import com.google.protobuf.MessageLite; +import dev.cel.common.CelOptions; +import dev.cel.common.Operator; +import dev.cel.common.values.CelByteString; +import dev.cel.common.values.CelValue; +import dev.cel.common.values.NullValue; +import dev.cel.runtime.CelFunctionBinding; +import dev.cel.runtime.CelInternalLiteRuntimeLibrary; +import dev.cel.runtime.CelLiteRuntimeBuilder; +import dev.cel.runtime.RuntimeEquality; +import java.time.Duration; +import java.time.Instant; +import java.util.Collection; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.Set; + +/** Runtime implementation of CEL Optional extension functions. */ +@Immutable +public final class CelOptionalRuntimeLibrary implements CelInternalLiteRuntimeLibrary { + + /** Supported functions for Optional extension library. */ + public enum Function { + VALUE("value"), + HAS_VALUE("hasValue"), + OPTIONAL_NONE("optional.none"), + OPTIONAL_OF("optional.of"), + OPTIONAL_UNWRAP("optional.unwrap"), + OPTIONAL_OF_NON_ZERO_VALUE("optional.ofNonZeroValue"), + OR("or"), + OR_VALUE("orValue"), + FIRST("first"), + LAST("last"); + + private final String functionName; + + public String getFunction() { + return functionName; + } + + Function(String functionName) { + this.functionName = functionName; + } + } + + private static final CelOptionalRuntimeLibrary VERSION_1 = + new CelOptionalRuntimeLibrary( + ImmutableSet.of( + Function.OPTIONAL_OF, + Function.OPTIONAL_OF_NON_ZERO_VALUE, + Function.OPTIONAL_NONE, + Function.VALUE, + Function.HAS_VALUE, + Function.OPTIONAL_UNWRAP, + Function.OR, + Function.OR_VALUE)); + + private static final CelOptionalRuntimeLibrary VERSION_2 = + new CelOptionalRuntimeLibrary(ImmutableSet.copyOf(Function.values())); + + /** Returns the latest version of the 'optional' runtime functions. */ + public static CelOptionalRuntimeLibrary optional() { + return VERSION_2; + } + + /** Returns the specified version of the 'optional' runtime functions. */ + public static CelOptionalRuntimeLibrary optional(int version) { + switch (version) { + case 0: + case 1: + return VERSION_1; + case 2: + case Integer.MAX_VALUE: + return VERSION_2; + default: + throw new IllegalArgumentException("Unsupported 'optional' extension version " + version); + } + } + + /** Returns the 'optional' runtime functions with only the specified functions. */ + public static CelOptionalRuntimeLibrary optional(Function... functions) { + return optional(ImmutableSet.copyOf(functions)); + } + + /** Returns the 'optional' runtime functions with only the specified functions. */ + public static CelOptionalRuntimeLibrary optional(Set functions) { + return new CelOptionalRuntimeLibrary(functions); + } + + private final ImmutableSet functions; + + CelOptionalRuntimeLibrary(Set functions) { + this.functions = ImmutableSet.copyOf(functions); + } + + ImmutableSet functions() { + return functions; + } + + @Override + public void setRuntimeOptions(CelLiteRuntimeBuilder runtimeBuilder) { + throw new UnsupportedOperationException("Unsupported"); + } + + @Override + public void setRuntimeOptions( + CelLiteRuntimeBuilder runtimeBuilder, + RuntimeEquality runtimeEquality, + CelOptions celOptions) { + runtimeBuilder.addFunctionBindings(newFunctionBindings(runtimeEquality)); + } + + public ImmutableSet newFunctionBindings(RuntimeEquality runtimeEquality) { + ImmutableSet.Builder bindingBuilder = ImmutableSet.builder(); + for (Function function : functions) { + switch (function) { + case OPTIONAL_OF: + bindingBuilder.addAll( + CelFunctionBinding.fromOverloads( + function.getFunction(), + CelFunctionBinding.from("optional_of", Object.class, Optional::of))); + break; + case OPTIONAL_OF_NON_ZERO_VALUE: + bindingBuilder.addAll( + CelFunctionBinding.fromOverloads( + function.getFunction(), + CelFunctionBinding.from( + "optional_ofNonZeroValue", + Object.class, + val -> isZeroValue(val) ? Optional.empty() : Optional.of(val)))); + break; + case OPTIONAL_UNWRAP: + bindingBuilder.addAll( + CelFunctionBinding.fromOverloads( + function.getFunction(), + CelFunctionBinding.from( + "optional_unwrap_list", + Collection.class, + CelOptionalRuntimeLibrary::elideOptionalCollection))); + break; + case OPTIONAL_NONE: + bindingBuilder.addAll( + CelFunctionBinding.fromOverloads( + function.getFunction(), + CelFunctionBinding.from( + "optional_none", ImmutableList.of(), val -> Optional.empty()))); + break; + case VALUE: + bindingBuilder.addAll( + CelFunctionBinding.fromOverloads( + function.getFunction(), + CelFunctionBinding.from( + "optional_value", Object.class, val -> ((Optional) val).get()))); + break; + case HAS_VALUE: + bindingBuilder.addAll( + CelFunctionBinding.fromOverloads( + function.getFunction(), + CelFunctionBinding.from( + "optional_hasValue", Object.class, val -> ((Optional) val).isPresent()))); + break; + case OR: + case OR_VALUE: + // Note: Implementation of "or" and "orValue" are special-cased inside the interpreter. + // Hence, their bindings are not provided here. + break; + case FIRST: + bindingBuilder.addAll( + CelFunctionBinding.fromOverloads( + function.getFunction(), + CelFunctionBinding.from( + "optional_list_first", + Collection.class, + CelOptionalRuntimeLibrary::listOptionalFirst))); + break; + case LAST: + bindingBuilder.addAll( + CelFunctionBinding.fromOverloads( + function.getFunction(), + CelFunctionBinding.from( + "optional_list_last", + Collection.class, + CelOptionalRuntimeLibrary::listOptionalLast))); + break; + } + } + + bindingBuilder.addAll( + CelFunctionBinding.fromOverloads( + Operator.OPTIONAL_SELECT.getFunction(), + CelFunctionBinding.from( + "select_optional_field", // This only handles map selection. Proto selection is + // special cased inside the interpreter. + Map.class, + String.class, + runtimeEquality::findInMap))); + + bindingBuilder.addAll( + CelFunctionBinding.fromOverloads( + Operator.OPTIONAL_INDEX.getFunction(), + CelFunctionBinding.from( + "list_optindex_optional_int", + List.class, + Long.class, + CelOptionalRuntimeLibrary::indexList), + CelFunctionBinding.from( + "optional_list_optindex_optional_int", + Optional.class, + Long.class, + CelOptionalRuntimeLibrary::indexOptionalList), + CelFunctionBinding.from( + "map_optindex_optional_value", Map.class, Object.class, runtimeEquality::findInMap), + CelFunctionBinding.from( + "optional_map_optindex_optional_value", + Optional.class, + Object.class, + (optionalMap, key) -> indexOptionalMap(optionalMap, key, runtimeEquality)))); + + bindingBuilder.addAll( + CelFunctionBinding.fromOverloads( + Operator.INDEX.getFunction(), + CelFunctionBinding.from( + "optional_list_index_int", + Optional.class, + Long.class, + CelOptionalRuntimeLibrary::indexOptionalList), + CelFunctionBinding.from( + "optional_map_index_value", + Optional.class, + Object.class, + (optionalMap, key) -> indexOptionalMap(optionalMap, key, runtimeEquality)))); + + return bindingBuilder.build(); + } + + private static ImmutableList elideOptionalCollection(Collection list) { + ImmutableList.Builder builder = ImmutableList.builder(); + for (Object element : list) { + Optional opt = (Optional) element; + if (opt.isPresent()) { + builder.add(opt.get()); + } + } + return builder.build(); + } + + private static boolean isZeroValue(Object val) { + if (val instanceof Boolean) { + return !((Boolean) val); + } else if (val instanceof Long) { + return (Long) val == 0L; + } else if (val instanceof Double) { + return (Double) val == 0.0d; + } else if (val instanceof String) { + return ((String) val).isEmpty(); + } else if (val instanceof UnsignedLong) { + return val.equals(UnsignedLong.ZERO); + } else if (val instanceof Collection) { + return ((Collection) val).isEmpty(); + } else if (val instanceof Map) { + return ((Map) val).isEmpty(); + } else if (val instanceof CelByteString) { + return ((CelByteString) val).isEmpty(); + } else if (val instanceof MessageLite) { + return val.equals(((MessageLite) val).getDefaultInstanceForType()); + } else if (val instanceof NullValue) { + // A null value always represents an absent value + return true; + } else if (val instanceof Instant) { + return val.equals(Instant.EPOCH); + } else if (val instanceof Duration) { + return val.equals(Duration.ZERO); + } else if (val instanceof CelValue) { + return ((CelValue) val).isZeroValue(); + } + + // Unknown. Assume that it is non-zero. + return false; + } + + private static Object indexOptionalMap( + Optional optionalMap, Object key, RuntimeEquality runtimeEquality) { + if (!optionalMap.isPresent()) { + return Optional.empty(); + } + + Map map = (Map) optionalMap.get(); + + return runtimeEquality.findInMap(map, key); + } + + private static Object indexList(List list, long index) { + int castIndex = Ints.checkedCast(index); + if (castIndex < 0 || castIndex >= list.size()) { + return Optional.empty(); + } + return Optional.of(list.get(castIndex)); + } + + private static Object indexOptionalList(Optional optionalList, long index) { + if (!optionalList.isPresent()) { + return Optional.empty(); + } + List list = (List) optionalList.get(); + return indexList(list, index); + } + + private static Object listOptionalFirst(Collection list) { + return Optional.ofNullable(Iterables.getFirst(list, null)); + } + + private static Object listOptionalLast(Collection list) { + return Optional.ofNullable(Iterables.getLast(list, null)); + } +} diff --git a/extensions/src/test/java/dev/cel/extensions/BUILD.bazel b/extensions/src/test/java/dev/cel/extensions/BUILD.bazel index 4168d0dfb..cd4b7a148 100644 --- a/extensions/src/test/java/dev/cel/extensions/BUILD.bazel +++ b/extensions/src/test/java/dev/cel/extensions/BUILD.bazel @@ -27,10 +27,12 @@ java_library( "//common/exceptions:index_out_of_bounds", "//common/exceptions:invalid_argument", "//common/types", + "//common/types:message_type_provider", "//common/types:type_providers", "//common/values", "//common/values:cel_byte_string", "//common/values:cel_value_provider", + "//common/values:proto_message_lite_value_provider", "//compiler", "//compiler:compiler_builder", "//extensions", @@ -47,7 +49,9 @@ java_library( "//extensions:math_compiler_library", "//extensions:math_runtime_library", "//extensions:native", + "//extensions:optional_compiler_library", "//extensions:optional_library", + "//extensions:optional_runtime_library", "//extensions:regex", "//extensions:regex_compiler_library", "//extensions:regex_runtime_library", @@ -64,16 +68,22 @@ java_library( "//runtime:lite_runtime", "//runtime:lite_runtime_factory", "//runtime:partial_vars", + "//runtime:program", "//runtime:runtime_equality", "//runtime:runtime_helpers", + "//runtime:standard_functions", "//runtime:unknown_attributes", + "//runtime/standard:dyn", + "//runtime/standard:index", "//testing:cel_runtime_flavor", + "//testing/protos:test_all_types_cel_java_proto3", "//validator", "//validator:validator_builder", "//validator/validators:homogeneous_literal", "@cel_spec//proto/cel/expr/conformance/proto2:test_all_types_java_proto", "@cel_spec//proto/cel/expr/conformance/proto3:test_all_types_java_proto", "@cel_spec//proto/cel/expr/conformance/test:simple_java_proto", + "@maven//:com_google_errorprone_error_prone_annotations", "@maven//:com_google_guava_guava", "@maven//:com_google_protobuf_protobuf_java", # unuseddeps: keep "@maven//:com_google_testparameterinjector_test_parameter_injector", diff --git a/extensions/src/test/java/dev/cel/extensions/CelOptionalLibraryTest.java b/extensions/src/test/java/dev/cel/extensions/CelOptionalLibraryTest.java index 8be97b40d..7c228de41 100644 --- a/extensions/src/test/java/dev/cel/extensions/CelOptionalLibraryTest.java +++ b/extensions/src/test/java/dev/cel/extensions/CelOptionalLibraryTest.java @@ -21,7 +21,9 @@ import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableMap; +import com.google.common.collect.ImmutableSet; import com.google.common.primitives.UnsignedLong; +import com.google.errorprone.annotations.CanIgnoreReturnValue; import com.google.protobuf.DoubleValue; import com.google.testing.junit.testparameterinjector.TestParameter; import com.google.testing.junit.testparameterinjector.TestParameterInjector; @@ -37,29 +39,47 @@ import dev.cel.common.CelValidationException; import dev.cel.common.CelVarDecl; import dev.cel.common.ast.CelExpr; +import dev.cel.common.ast.CelExpr.CelComprehension; import dev.cel.common.ast.CelExpr.CelList; import dev.cel.common.types.CelKind; import dev.cel.common.types.CelType; import dev.cel.common.types.ListType; import dev.cel.common.types.MapType; import dev.cel.common.types.OptionalType; +import dev.cel.common.types.ProtoMessageTypeProvider; import dev.cel.common.types.SimpleType; import dev.cel.common.types.StructTypeReference; import dev.cel.common.types.TypeType; import dev.cel.common.values.CelByteString; import dev.cel.common.values.NullValue; +import dev.cel.common.values.ProtoMessageLiteValueProvider; import dev.cel.compiler.CelCompiler; +import dev.cel.compiler.CelCompilerBuilder; +import dev.cel.compiler.CelCompilerFactory; import dev.cel.expr.conformance.proto3.NestedTestAllTypes; +import dev.cel.expr.conformance.proto3.NestedTestAllTypesCelDescriptor; import dev.cel.expr.conformance.proto3.TestAllTypes; import dev.cel.expr.conformance.proto3.TestAllTypes.NestedMessage; +import dev.cel.expr.conformance.proto3.TestAllTypesCelDescriptor; import dev.cel.parser.CelMacro; import dev.cel.parser.CelStandardMacro; import dev.cel.runtime.CelAttributePattern; import dev.cel.runtime.CelEvaluationException; import dev.cel.runtime.CelFunctionBinding; +import dev.cel.runtime.CelLiteRuntime; +import dev.cel.runtime.CelLiteRuntimeBuilder; +import dev.cel.runtime.CelLiteRuntimeFactory; import dev.cel.runtime.CelRuntime; +import dev.cel.runtime.CelRuntimeBuilder; +import dev.cel.runtime.CelRuntimeFactory; +import dev.cel.runtime.CelStandardFunctions; import dev.cel.runtime.CelUnknownSet; import dev.cel.runtime.PartialVars; +import dev.cel.runtime.Program; +import dev.cel.runtime.RuntimeEquality; +import dev.cel.runtime.RuntimeHelpers; +import dev.cel.runtime.standard.DynFunction; +import dev.cel.runtime.standard.IndexOperator; import java.time.Duration; import java.time.Instant; import java.util.List; @@ -75,7 +95,17 @@ public class CelOptionalLibraryTest { private enum TestMode { PLANNER_PARSE_ONLY, PLANNER_CHECKED, - LEGACY_CHECKED + LEGACY_CHECKED, + LITE_PARSE_ONLY, + LITE_CHECKED; + + private boolean isParseOnly() { + return this.equals(PLANNER_PARSE_ONLY) || this.equals(LITE_PARSE_ONLY); + } + + private boolean isLite() { + return this.equals(LITE_PARSE_ONLY) || this.equals(LITE_CHECKED); + } } @TestParameter TestMode testMode; @@ -109,31 +139,53 @@ private enum ConstantTestCases { } } - private CelBuilder newCelBuilder() { + private TestCelBuilder newCelBuilder() { return newCelBuilder(Integer.MAX_VALUE); } - private CelBuilder newCelBuilder(int version) { - CelBuilder celBuilder; - switch (testMode) { - case PLANNER_PARSE_ONLY: - case PLANNER_CHECKED: - celBuilder = CelFactory.plannerCelBuilder(); - break; - case LEGACY_CHECKED: - celBuilder = CelFactory.standardCelBuilder(); - break; - default: - throw new IllegalArgumentException("Unknown test mode: " + testMode); + // Intentionally testing the deprecated legacy runtime in the test matrix. + @SuppressWarnings("deprecation") + private TestCelBuilder newCelBuilder(int version) { + CelOptions options = CelOptions.current().enableHeterogeneousNumericComparisons(true).build(); + CelContainer container = CelContainer.ofName("cel.expr.conformance.proto3"); + if (testMode.isLite()) { + CelCompilerBuilder compilerBuilder = + CelCompilerFactory.standardCelCompilerBuilder() + .setOptions(options) + .setStandardMacros(CelStandardMacro.STANDARD_MACROS) + .setContainer(container) + .addMessageTypes(TestAllTypes.getDescriptor()) + .addLibraries(CelOptionalCompilerLibrary.optional(version)); + CelLiteRuntimeBuilder liteRuntimeBuilder = + CelLiteRuntimeFactory.newLiteRuntimeBuilder() + .setOptions(options) + .setStandardFunctions(CelStandardFunctions.ALL_STANDARD_FUNCTIONS) + .setContainer(container) + .setValueProvider( + ProtoMessageLiteValueProvider.newInstance( + TestAllTypesCelDescriptor.getDescriptor(), + NestedTestAllTypesCelDescriptor.getDescriptor())) + .setTypeProvider( + ProtoMessageTypeProvider.newBuilder() + .addFileDescriptors(TestAllTypes.getDescriptor().getFile()) + .build()) + .addLibraries(CelOptionalRuntimeLibrary.optional(version)); + return new TestCelBuilder(compilerBuilder, liteRuntimeBuilder); } - return celBuilder - .setOptions(CelOptions.current().enableHeterogeneousNumericComparisons(true).build()) - .setStandardMacros(CelStandardMacro.STANDARD_MACROS) - .setContainer(CelContainer.ofName("cel.expr.conformance.proto3")) - .addMessageTypes(TestAllTypes.getDescriptor()) - .addRuntimeLibraries(CelExtensions.optional(version)) - .addCompilerLibraries(CelExtensions.optional(version)); + CelBuilder celBuilder = + testMode.equals(TestMode.LEGACY_CHECKED) + ? CelFactory.standardCelBuilder() + : CelFactory.plannerCelBuilder(); + + return new TestCelBuilder( + celBuilder + .setOptions(options) + .setStandardMacros(CelStandardMacro.STANDARD_MACROS) + .setContainer(container) + .addMessageTypes(TestAllTypes.getDescriptor()) + .addRuntimeLibraries(CelExtensions.optional(version)) + .addCompilerLibraries(CelExtensions.optional(version))); } @Test @@ -206,7 +258,7 @@ public void library() { @Test public void optionalOf_constant_success(@TestParameter ConstantTestCases testCase) throws Exception { - Cel cel = newCelBuilder().setResultType(OptionalType.create(testCase.type)).build(); + TestCel cel = newCelBuilder().setResultType(OptionalType.create(testCase.type)).build(); String expression = String.format("optional.of(%s)", testCase.sourceWithNonZeroValue); CelAbstractSyntaxTree ast = compile(cel, expression); @@ -219,7 +271,7 @@ public void optionalOf_constant_success(@TestParameter ConstantTestCases testCas @Test public void optionalType_runtimeEquality(@TestParameter ConstantTestCases testCase) throws Exception { - Cel cel = + TestCel cel = newCelBuilder() .addVar("a", OptionalType.create(testCase.type)) .addVar("b", OptionalType.create(testCase.type)) @@ -242,7 +294,7 @@ public void optionalType_runtimeEquality(@TestParameter ConstantTestCases testCa @Test public void optionalType_adaptsIntegerToLong_success() throws Exception { - Cel cel = newCelBuilder().addVar("a", OptionalType.create(SimpleType.INT)).build(); + TestCel cel = newCelBuilder().addVar("a", OptionalType.create(SimpleType.INT)).build(); CelAbstractSyntaxTree ast = compile(cel, "a"); Optional result = @@ -253,7 +305,7 @@ public void optionalType_adaptsIntegerToLong_success() throws Exception { @Test public void optionalType_adaptsFloatToLong_success() throws Exception { - Cel cel = newCelBuilder().addVar("a", OptionalType.create(SimpleType.DOUBLE)).build(); + TestCel cel = newCelBuilder().addVar("a", OptionalType.create(SimpleType.DOUBLE)).build(); CelAbstractSyntaxTree ast = compile(cel, "a"); Optional result = @@ -264,7 +316,7 @@ public void optionalType_adaptsFloatToLong_success() throws Exception { @Test public void optionalOf_nullValue_success() throws Exception { - Cel cel = newCelBuilder().setResultType(SimpleType.DYN).build(); + TestCel cel = newCelBuilder().setResultType(SimpleType.DYN).build(); String expression = "optional.of(TestAllTypes{}.single_value)"; CelAbstractSyntaxTree ast = compile(cel, expression); @@ -277,7 +329,7 @@ public void optionalOf_nullValue_success() throws Exception { @Test public void optionalOfNonZeroValue_withZeroValue_returnsEmptyOptionalValue( @TestParameter ConstantTestCases testCase) throws Exception { - Cel cel = newCelBuilder().setResultType(OptionalType.create(testCase.type)).build(); + TestCel cel = newCelBuilder().setResultType(OptionalType.create(testCase.type)).build(); String expression = String.format("optional.ofNonZeroValue(%s)", testCase.sourceWithZeroValue); CelAbstractSyntaxTree ast = compile(cel, expression); @@ -290,7 +342,7 @@ public void optionalOfNonZeroValue_withZeroValue_returnsEmptyOptionalValue( @Test public void optionalOfNonZeroValue_withNonZeroValue_returnsOptionalValue( @TestParameter ConstantTestCases testCase) throws Exception { - Cel cel = newCelBuilder().setResultType(OptionalType.create(testCase.type)).build(); + TestCel cel = newCelBuilder().setResultType(OptionalType.create(testCase.type)).build(); String expression = String.format("optional.ofNonZeroValue(%s)", testCase.sourceWithNonZeroValue); CelAbstractSyntaxTree ast = compile(cel, expression); @@ -303,7 +355,7 @@ public void optionalOfNonZeroValue_withNonZeroValue_returnsOptionalValue( @Test public void optionalOfNonZeroValue_withNullValue_returnsEmptyOptionalValue() throws Exception { - Cel cel = newCelBuilder().setResultType(SimpleType.DYN).build(); + TestCel cel = newCelBuilder().setResultType(SimpleType.DYN).build(); CelAbstractSyntaxTree ast = compile(cel, "optional.ofNonZeroValue(TestAllTypes{}.single_value)"); @@ -315,7 +367,7 @@ public void optionalOfNonZeroValue_withNullValue_returnsEmptyOptionalValue() thr @Test public void optionalOfNonZeroValue_withEmptyMessage_returnsEmptyOptionalValue() throws Exception { - Cel cel = newCelBuilder().setResultType(SimpleType.DYN).build(); + TestCel cel = newCelBuilder().setResultType(SimpleType.DYN).build(); CelAbstractSyntaxTree ast = compile(cel, "optional.ofNonZeroValue(TestAllTypes{})"); Object result = cel.createProgram(ast).eval(); @@ -326,7 +378,7 @@ public void optionalOfNonZeroValue_withEmptyMessage_returnsEmptyOptionalValue() @Test public void optionalNone_success() throws Exception { - Cel cel = newCelBuilder().setResultType(SimpleType.DYN).build(); + TestCel cel = newCelBuilder().setResultType(SimpleType.DYN).build(); CelAbstractSyntaxTree ast = compile(cel, "optional.none()"); Object result = cel.createProgram(ast).eval(); @@ -337,7 +389,7 @@ public void optionalNone_success() throws Exception { @Test public void optionalValue_success(@TestParameter ConstantTestCases testCase) throws Exception { - Cel cel = newCelBuilder().setResultType(testCase.type).build(); + TestCel cel = newCelBuilder().setResultType(testCase.type).build(); String expression = String.format("optional.of(%s).value()", testCase.sourceWithNonZeroValue); CelAbstractSyntaxTree ast = compile(cel, expression); @@ -348,7 +400,7 @@ public void optionalValue_success(@TestParameter ConstantTestCases testCase) thr @Test public void optionalValue_whenOptionalValueEmpty_throws() throws Exception { - Cel cel = newCelBuilder().build(); + TestCel cel = newCelBuilder().build(); CelAbstractSyntaxTree ast = compile(cel, "optional.none().value()"); assertThrows(CelEvaluationException.class, () -> cel.createProgram(ast).eval()); @@ -357,7 +409,7 @@ public void optionalValue_whenOptionalValueEmpty_throws() throws Exception { @Test public void optionalHasValue_whenOptionalValuePresent_returnsTrue( @TestParameter ConstantTestCases testCase) throws Exception { - Cel cel = newCelBuilder().setResultType(SimpleType.BOOL).build(); + TestCel cel = newCelBuilder().setResultType(SimpleType.BOOL).build(); String expression = String.format("optional.of(%s).hasValue()", testCase.sourceWithNonZeroValue); CelAbstractSyntaxTree ast = compile(cel, expression); @@ -370,7 +422,7 @@ public void optionalHasValue_whenOptionalValuePresent_returnsTrue( @Test public void optionalHasValue_whenOptionalValueEmpty_returnsFalse( @TestParameter ConstantTestCases testCase) throws Exception { - Cel cel = newCelBuilder().setResultType(SimpleType.BOOL).build(); + TestCel cel = newCelBuilder().setResultType(SimpleType.BOOL).build(); String expression = String.format("optional.ofNonZeroValue(%s).hasValue()", testCase.sourceWithZeroValue); CelAbstractSyntaxTree ast = compile(cel, expression); @@ -382,13 +434,13 @@ public void optionalHasValue_whenOptionalValueEmpty_returnsFalse( @Test public void optionalOr_success() throws Exception { - Cel cel = + TestCel cel = newCelBuilder() .addVar("x", OptionalType.create(SimpleType.INT)) .addVar("y", OptionalType.create(SimpleType.INT)) .setResultType(OptionalType.create(SimpleType.INT)) .build(); - CelRuntime.Program program = cel.createProgram(compile(cel, "x.or(y)")); + Program program = cel.createProgram(compile(cel, "x.or(y)")); Object resultLhs = program.eval(ImmutableMap.of("x", Optional.of(5), "y", Optional.empty())); Object resultRhs = program.eval(ImmutableMap.of("x", Optional.empty(), "y", Optional.of(10))); @@ -399,7 +451,7 @@ public void optionalOr_success() throws Exception { @Test public void optionalOr_shortCircuits() throws Exception { - Cel cel = + TestCel cel = newCelBuilder() .addVar("x", OptionalType.create(SimpleType.INT)) .addFunctionDeclarations( @@ -419,7 +471,7 @@ public void optionalOr_shortCircuits() throws Exception { }))) .setResultType(OptionalType.create(SimpleType.INT)) .build(); - CelRuntime.Program program = cel.createProgram(compile(cel, "x.or(errorFunc())")); + Program program = cel.createProgram(compile(cel, "x.or(errorFunc())")); Object resultLhs = program.eval(ImmutableMap.of("x", Optional.of(5))); @@ -428,7 +480,7 @@ public void optionalOr_shortCircuits() throws Exception { @Test public void optionalOr_producesNonOptionalValue_throws() throws Exception { - Cel cel = newCelBuilder().addVar("x", OptionalType.create(SimpleType.INT)).build(); + TestCel cel = newCelBuilder().addVar("x", OptionalType.create(SimpleType.INT)).build(); CelAbstractSyntaxTree ast = compile(cel, "x.or(optional.of(10))"); @@ -444,7 +496,7 @@ public void optionalOr_producesNonOptionalValue_throws() throws Exception { @Test public void optionalOrValue_lhsHasValue_success(@TestParameter ConstantTestCases testCase) throws Exception { - Cel cel = newCelBuilder().setResultType(testCase.type).build(); + TestCel cel = newCelBuilder().setResultType(testCase.type).build(); String expression = String.format( "optional.of(%s).orValue(%s)", @@ -458,7 +510,7 @@ public void optionalOrValue_lhsHasValue_success(@TestParameter ConstantTestCases @Test public void optionalOrValue_shortCircuits() throws Exception { - Cel cel = + TestCel cel = newCelBuilder() .addVar("x", OptionalType.create(SimpleType.INT)) .addFunctionDeclarations( @@ -477,7 +529,7 @@ public void optionalOrValue_shortCircuits() throws Exception { }))) .setResultType(SimpleType.INT) .build(); - CelRuntime.Program program = cel.createProgram(compile(cel, "x.orValue(errorFunc())")); + Program program = cel.createProgram(compile(cel, "x.orValue(errorFunc())")); Object resultLhs = program.eval(ImmutableMap.of("x", Optional.of(5))); @@ -487,7 +539,7 @@ public void optionalOrValue_shortCircuits() throws Exception { @Test public void optionalOrValue_rhsHasValue_success(@TestParameter ConstantTestCases testCase) throws Exception { - Cel cel = newCelBuilder().setResultType(testCase.type).build(); + TestCel cel = newCelBuilder().setResultType(testCase.type).build(); String expression = String.format("optional.none().orValue(%s)", testCase.sourceWithNonZeroValue); CelAbstractSyntaxTree ast = compile(cel, expression); @@ -503,10 +555,10 @@ public void optionalOrValue_rhsHasValue_success(@TestParameter ConstantTestCases @TestParameters("{source: 5.orValue(optional.of(10))}") @TestParameters("{source: 5.orValue(optional.none())}") public void optionalOrValue_unmatchingTypes_throwsCompilationException(String source) { - if (testMode.equals(TestMode.PLANNER_PARSE_ONLY)) { + if (testMode.isParseOnly()) { return; } - Cel cel = newCelBuilder().build(); + TestCel cel = newCelBuilder().build(); CelValidationException e = assertThrows(CelValidationException.class, () -> compile(cel, source)); @@ -515,7 +567,7 @@ public void optionalOrValue_unmatchingTypes_throwsCompilationException(String so @Test public void optionalOrValue_producesNonOptionalValue_throws() throws Exception { - Cel cel = newCelBuilder().addVar("x", OptionalType.create(SimpleType.INT)).build(); + TestCel cel = newCelBuilder().addVar("x", OptionalType.create(SimpleType.INT)).build(); CelAbstractSyntaxTree ast = compile(cel, "x.orValue(10)"); @@ -532,7 +584,7 @@ public void optionalOrValue_producesNonOptionalValue_throws() throws Exception { @TestParameters("{source: optional.ofNonZeroValue(42).or(optional.of(10)).value() == 42}") @TestParameters("{source: optional.none().or(optional.none()).orValue(42) == 42}") public void optionalChainedFunctions_constants_success(String source) throws Exception { - Cel cel = newCelBuilder().setResultType(SimpleType.BOOL).build(); + TestCel cel = newCelBuilder().setResultType(SimpleType.BOOL).build(); CelAbstractSyntaxTree ast = compile(cel, source); boolean result = (boolean) cel.createProgram(ast).eval(); @@ -542,7 +594,7 @@ public void optionalChainedFunctions_constants_success(String source) throws Exc @Test public void optionalChainedFunctions_nestedMaps_success() throws Exception { - Cel cel = + TestCel cel = newCelBuilder() .addVar( "m", @@ -566,7 +618,7 @@ public void optionalChainedFunctions_nestedMaps_success() throws Exception { @Test public void optionalChainedFunctions_nestedMapsInvalidAccess_throws() throws Exception { - Cel cel = + TestCel cel = newCelBuilder() .addVar( "m", @@ -591,7 +643,7 @@ public void optionalChainedFunctions_nestedMapsInvalidAccess_throws() throws Exc @Test public void optionalFieldSelection_onMap_returnsOptionalEmpty() throws Exception { - Cel cel = newCelBuilder().setResultType(OptionalType.create(SimpleType.INT)).build(); + TestCel cel = newCelBuilder().setResultType(OptionalType.create(SimpleType.INT)).build(); CelAbstractSyntaxTree ast = compile(cel, "{'a': 2}.?x"); Object result = cel.createProgram(ast).eval(); @@ -601,7 +653,7 @@ public void optionalFieldSelection_onMap_returnsOptionalEmpty() throws Exception @Test public void optionalFieldSelection_onMap_returnsOptionalValue() throws Exception { - Cel cel = newCelBuilder().setResultType(OptionalType.create(SimpleType.INT)).build(); + TestCel cel = newCelBuilder().setResultType(OptionalType.create(SimpleType.INT)).build(); CelAbstractSyntaxTree ast = compile(cel, "{'a': 2}.?a"); Optional result = (Optional) cel.createProgram(ast).eval(); @@ -611,7 +663,7 @@ public void optionalFieldSelection_onMap_returnsOptionalValue() throws Exception @Test public void optionalFieldSelection_onMap_chained_returnsSinglyWrappedOptional() throws Exception { - Cel cel = newCelBuilder().setResultType(OptionalType.create(SimpleType.STRING)).build(); + TestCel cel = newCelBuilder().setResultType(OptionalType.create(SimpleType.STRING)).build(); CelAbstractSyntaxTree ast = compile(cel, "{'foo': {'bar': 'baz'}}.?foo.?bar"); Optional result = (Optional) cel.createProgram(ast).eval(); @@ -625,7 +677,7 @@ public void optionalFieldSelection_onMap_chained_returnsSinglyWrappedOptional() @TestParameters("{source: \"optional.of({'a': optional.none()}).a\"}") public void optionalFieldSelection_onMapWithEmptyOptionalValue_returnsNestedOptionalEmpty( String source) throws Exception { - Cel cel = newCelBuilder().build(); + TestCel cel = newCelBuilder().build(); CelAbstractSyntaxTree ast = compile(cel, source); Optional> result = (Optional>) cel.createProgram(ast).eval(); @@ -639,7 +691,7 @@ public void optionalFieldSelection_onMapWithEmptyOptionalValue_returnsNestedOpti @TestParameters("{source: \"optional.of({'a': optional.of(1)}).a\"}") public void optionalFieldSelection_onMapWithPresentOptionalValue_returnsNestedOptionalValue( String source) throws Exception { - Cel cel = newCelBuilder().build(); + TestCel cel = newCelBuilder().build(); CelAbstractSyntaxTree ast = compile(cel, source); Optional> result = (Optional>) cel.createProgram(ast).eval(); @@ -649,7 +701,7 @@ public void optionalFieldSelection_onMapWithPresentOptionalValue_returnsNestedOp @Test public void optionalFieldSelection_onProtoMessage_returnsOptionalEmpty() throws Exception { - Cel cel = + TestCel cel = newCelBuilder() .setResultType(OptionalType.create(SimpleType.INT)) .addVar("msg", StructTypeReference.create(TestAllTypes.getDescriptor().getFullName())) @@ -666,7 +718,7 @@ public void optionalFieldSelection_onProtoMessage_returnsOptionalEmpty() throws @Test public void optionalFieldSelection_onProtoMessage_chained_returnsSinglyWrappedOptional() throws Exception { - Cel cel = + TestCel cel = newCelBuilder() .setResultType(OptionalType.create(SimpleType.INT)) .addVar( @@ -689,7 +741,7 @@ public void optionalFieldSelection_onProtoMessage_chained_returnsSinglyWrappedOp @Test public void optionalFieldSelection_onProtoMessage_returnsOptionalValue() throws Exception { - Cel cel = + TestCel cel = newCelBuilder() .setResultType(OptionalType.create(SimpleType.INT)) .addVar("msg", StructTypeReference.create(TestAllTypes.getDescriptor().getFullName())) @@ -706,7 +758,10 @@ public void optionalFieldSelection_onProtoMessage_returnsOptionalValue() throws @Test public void optionalFieldSelection_onProtoMessage_listValue() throws Exception { - Cel cel = newCelBuilder().build(); + // TODO: Message creation with prepopulated fields is not yet supported in + // CelLiteRuntime. + assumeFalse(testMode.isLite()); + TestCel cel = newCelBuilder().build(); CelAbstractSyntaxTree ast = compile( cel, "optional.of(TestAllTypes{repeated_string: ['foo']}).?repeated_string.value()"); @@ -718,7 +773,10 @@ public void optionalFieldSelection_onProtoMessage_listValue() throws Exception { @Test public void optionalFieldSelection_onProtoMessage_indexValue() throws Exception { - Cel cel = newCelBuilder().build(); + // TODO: Message creation with prepopulated fields is not yet supported in + // CelLiteRuntime. + assumeFalse(testMode.isLite()); + TestCel cel = newCelBuilder().build(); CelAbstractSyntaxTree ast = compile( cel, "optional.of(TestAllTypes{repeated_string: ['foo']}).?repeated_string[0].value()"); @@ -730,7 +788,7 @@ public void optionalFieldSelection_onProtoMessage_indexValue() throws Exception @Test public void optionalFieldSelection_onProtoMessage_chainedSuccess() throws Exception { - Cel cel = + TestCel cel = newCelBuilder() .setResultType(OptionalType.create(SimpleType.INT)) .addVar( @@ -761,10 +819,10 @@ public void optionalFieldSelection_onProtoMessage_chainedSuccess() throws Except @Test public void optionalFieldSelection_indexerOnProtoMessage_typeCheck_throwsException() { - if (testMode.equals(TestMode.PLANNER_PARSE_ONLY)) { + if (testMode.isParseOnly()) { return; } - Cel cel = + TestCel cel = newCelBuilder() .setResultType(OptionalType.create(SimpleType.INT)) .addVar("msg", StructTypeReference.create(TestAllTypes.getDescriptor().getFullName())) @@ -778,7 +836,7 @@ public void optionalFieldSelection_indexerOnProtoMessage_typeCheck_throwsExcepti @Test public void optionalFieldSelection_onProtoMessage_presenceTest() throws Exception { - Cel cel = + TestCel cel = newCelBuilder() .addVar("msg", StructTypeReference.create(TestAllTypes.getDescriptor().getFullName())) .setResultType(SimpleType.BOOL) @@ -804,7 +862,7 @@ public void optionalFieldSelection_onProtoMessage_presenceTest() throws Exceptio @TestParameters("{source: \"{'a': 2}.?x.hasValue()\", expectedResult: false}") public void optionalFieldSelection_onMap_hasValueReturnsBoolean( String source, boolean expectedResult) throws Exception { - Cel cel = newCelBuilder().setResultType(SimpleType.BOOL).build(); + TestCel cel = newCelBuilder().setResultType(SimpleType.BOOL).build(); CelAbstractSyntaxTree ast = compile(cel, source); boolean result = (boolean) cel.createProgram(ast).eval(); @@ -814,7 +872,7 @@ public void optionalFieldSelection_onMap_hasValueReturnsBoolean( @Test public void optionalFieldSelection_onMap_hasMacroReturnsTrue() throws Exception { - Cel cel = + TestCel cel = newCelBuilder() .addVar( "m", @@ -834,7 +892,7 @@ public void optionalFieldSelection_onMap_hasMacroReturnsTrue() throws Exception @Test public void optionalFieldSelection_onMap_hasMacroReturnsFalse() throws Exception { - Cel cel = + TestCel cel = newCelBuilder() .addVar( "m", @@ -851,7 +909,7 @@ public void optionalFieldSelection_onMap_hasMacroReturnsFalse() throws Exception @Test public void optionalFieldSelection_onOptionalMap_presenceTest() throws Exception { - Cel cel = + TestCel cel = newCelBuilder() .addVar( "optm", @@ -876,7 +934,7 @@ public void optionalFieldSelection_onOptionalMap_presenceTest() throws Exception @Test public void optionalIndex_onOptionalMap_returnsOptionalValue() throws Exception { - Cel cel = + TestCel cel = newCelBuilder() .addVar( "optm", @@ -899,7 +957,7 @@ public void optionalIndex_onOptionalMap_returnsOptionalValue() throws Exception @Test public void optionalIndex_onOptionalMap_returnsOptionalEmpty() throws Exception { - Cel cel = + TestCel cel = newCelBuilder() .addVar( "optm", @@ -909,17 +967,34 @@ public void optionalIndex_onOptionalMap_returnsOptionalEmpty() throws Exception .setResultType(OptionalType.create(SimpleType.STRING)) .build(); CelAbstractSyntaxTree ast = compile(cel, "optm.c[?'dashed-index']"); + Program program = cel.createProgram(ast); - Object result = - cel.createProgram(ast) - .eval(ImmutableMap.of("optm", Optional.of(ImmutableMap.of("c", ImmutableMap.of())))); + assertThat(program.eval(ImmutableMap.of("optm", Optional.empty()))).isEqualTo(Optional.empty()); + assertThat( + program.eval( + ImmutableMap.of("optm", Optional.of(ImmutableMap.of("c", ImmutableMap.of()))))) + .isEqualTo(Optional.empty()); + } - assertThat(result).isEqualTo(Optional.empty()); + @Test + public void traditionalIndex_onOptionalMap_returnsOptionalEmpty() throws Exception { + TestCel cel = + newCelBuilder() + .addVar( + "optm", OptionalType.create(MapType.create(SimpleType.STRING, SimpleType.STRING))) + .setResultType(OptionalType.create(SimpleType.STRING)) + .build(); + CelAbstractSyntaxTree ast = compile(cel, "optm['dashed-index']"); + Program program = cel.createProgram(ast); + + assertThat(program.eval(ImmutableMap.of("optm", Optional.empty()))).isEqualTo(Optional.empty()); + assertThat(program.eval(ImmutableMap.of("optm", Optional.of(ImmutableMap.of())))) + .isEqualTo(Optional.empty()); } @Test public void optionalIndex_onMap_returnsOptionalEmpty() throws Exception { - Cel cel = + TestCel cel = newCelBuilder() .addVar( "m", @@ -937,7 +1012,7 @@ public void optionalIndex_onMap_returnsOptionalEmpty() throws Exception { @Test public void optionalIndex_onMap_returnsOptionalValue() throws Exception { - Cel cel = + TestCel cel = newCelBuilder() .addVar( "m", @@ -962,7 +1037,7 @@ public void optionalIndex_onMap_returnsOptionalValue() throws Exception { @TestParameters("{source: '{?x: x}'}") public void optionalIndex_onMapWithUnknownInput_returnsUnknownResult(String source) throws Exception { - Cel cel = newCelBuilder().addVar("x", OptionalType.create(SimpleType.INT)).build(); + TestCel cel = newCelBuilder().addVar("x", OptionalType.create(SimpleType.INT)).build(); CelAbstractSyntaxTree ast = compile(cel, source); Object result = @@ -975,7 +1050,7 @@ public void optionalIndex_onMapWithUnknownInput_returnsUnknownResult(String sour @Test public void optionalIndex_onOptionalMapUsingFieldSelection_returnsOptionalValue() throws Exception { - Cel cel = + TestCel cel = newCelBuilder() .addVar( "m", @@ -992,20 +1067,20 @@ public void optionalIndex_onOptionalMapUsingFieldSelection_returnsOptionalValue( @Test public void optionalIndex_onList_returnsOptionalEmpty() throws Exception { - Cel cel = + TestCel cel = newCelBuilder() .addVar("l", ListType.create(SimpleType.STRING)) .setResultType(OptionalType.create(SimpleType.STRING)) .build(); CelAbstractSyntaxTree ast = compile(cel, "l[?0]"); - CelRuntime.Program program = cel.createProgram(ast); + Program program = cel.createProgram(ast); assertThat(program.eval(ImmutableMap.of("l", ImmutableList.of()))).isEqualTo(Optional.empty()); } @Test public void optionalIndex_onList_returnsOptionalValue() throws Exception { - Cel cel = + TestCel cel = newCelBuilder() .addVar("l", ListType.create(SimpleType.STRING)) .setResultType(OptionalType.create(SimpleType.STRING)) @@ -1019,13 +1094,13 @@ public void optionalIndex_onList_returnsOptionalValue() throws Exception { @Test public void optionalIndex_onOptionalList_returnsOptionalEmpty() throws Exception { - Cel cel = + TestCel cel = newCelBuilder() .addVar("optl", OptionalType.create(ListType.create(SimpleType.STRING))) .setResultType(OptionalType.create(SimpleType.STRING)) .build(); CelAbstractSyntaxTree ast = compile(cel, "optl[?0]"); - CelRuntime.Program program = cel.createProgram(ast); + Program program = cel.createProgram(ast); assertThat(program.eval(ImmutableMap.of("optl", Optional.empty()))).isEqualTo(Optional.empty()); assertThat(program.eval(ImmutableMap.of("optl", Optional.of(ImmutableList.of())))) @@ -1034,7 +1109,7 @@ public void optionalIndex_onOptionalList_returnsOptionalEmpty() throws Exception @Test public void optionalIndex_onOptionalList_returnsOptionalValue() throws Exception { - Cel cel = + TestCel cel = newCelBuilder() .addVar("optl", OptionalType.create(ListType.create(SimpleType.STRING))) .setResultType(OptionalType.create(SimpleType.STRING)) @@ -1050,7 +1125,7 @@ public void optionalIndex_onOptionalList_returnsOptionalValue() throws Exception @Test public void optionalIndex_onListWithUnknownInput_returnsUnknownResult() throws Exception { - Cel cel = + TestCel cel = newCelBuilder() .addVar("x", OptionalType.create(SimpleType.INT)) .setResultType(ListType.create(SimpleType.INT)) @@ -1066,7 +1141,7 @@ public void optionalIndex_onListWithUnknownInput_returnsUnknownResult() throws E @Test public void traditionalIndex_onOptionalList_returnsOptionalEmpty() throws Exception { - Cel cel = + TestCel cel = newCelBuilder() .addVar("optl", OptionalType.create(ListType.create(SimpleType.STRING))) .setResultType(OptionalType.create(SimpleType.STRING)) @@ -1085,7 +1160,7 @@ public void optionalFieldSelect_fieldMarkedUnknown_returnsUnknownSet() throws Ex return; } - Cel cel = + TestCel cel = newCelBuilder() .addVar("msg", StructTypeReference.create(TestAllTypes.getDescriptor().getFullName())) .build(); @@ -1110,7 +1185,7 @@ public void optionalFieldSelect_fieldMarkedUnknown_returnsUnknownSet() throws Ex @TestParameters("{expression: 'optional.none().orValue(optx)'}") public void optionalChainedFunctions_lhsIsUnknown_returnsUnknown(String expression) throws Exception { - Cel cel = + TestCel cel = newCelBuilder() .addVar("optx", OptionalType.create(SimpleType.INT)) .addVar("x", SimpleType.INT) @@ -1132,7 +1207,7 @@ public void optionalChainedFunctions_lhsIsUnknown_returnsUnknown(String expressi @TestParameters("{expression: 'optional.none().or(optional.of(1/0))'}") @TestParameters("{expression: 'optional.none().orValue(1/0)'}") public void optionalChainedFunctions_lhsIsError_returnsError(String expression) throws Exception { - Cel cel = + TestCel cel = newCelBuilder() .addVar("optx", OptionalType.create(SimpleType.INT)) .addVar("x", SimpleType.INT) @@ -1144,7 +1219,7 @@ public void optionalChainedFunctions_lhsIsError_returnsError(String expression) @Test public void traditionalIndex_onOptionalList_returnsOptionalValue() throws Exception { - Cel cel = + TestCel cel = newCelBuilder() .addVar("optl", OptionalType.create(ListType.create(SimpleType.STRING))) .setResultType(OptionalType.create(SimpleType.STRING)) @@ -1164,7 +1239,7 @@ public void traditionalIndex_onOptionalList_returnsOptionalValue() throws Except @TestParameters("{source: \"m.c[?'missing-index'].orValue('default value') == 'default value'\"}") public void optionalFieldSelection_onMap_chainedWithSelectorAndIndexer(String source) throws Exception { - Cel cel = + TestCel cel = newCelBuilder() .addVar( "m", @@ -1191,7 +1266,7 @@ public void optionalFieldSelection_onMap_chainedWithSelectorAndIndexer(String so @TestParameters("{source: \"optm.c['missing'].orValue('default value') == 'default value'\"}") public void traditionalIndexSelection_onOptionalMap_chainedOperatorSuccess(String source) throws Exception { - Cel cel = + TestCel cel = newCelBuilder() .addVar( "optm", @@ -1215,7 +1290,7 @@ public void traditionalIndexSelection_onOptionalMap_chainedOperatorSuccess(Strin @Test public void traditionalIndexSelection_onOptionalMap_orChainedList() throws Exception { - Cel cel = + TestCel cel = newCelBuilder() .addVar( "optm", @@ -1243,7 +1318,7 @@ public void traditionalIndexSelection_onOptionalMap_orChainedList() throws Excep @Test public void optionalMapCreation_valueIsEmpty_returnsEmptyMap() throws Exception { - Cel cel = newCelBuilder().build(); + TestCel cel = newCelBuilder().build(); CelAbstractSyntaxTree ast = compile(cel, "{?'key': optional.none()}"); Map result = (Map) cel.createProgram(ast).eval(); @@ -1253,7 +1328,7 @@ public void optionalMapCreation_valueIsEmpty_returnsEmptyMap() throws Exception @Test public void optionalMapCreation_valueIsPresent_returnsMap() throws Exception { - Cel cel = newCelBuilder().build(); + TestCel cel = newCelBuilder().build(); CelAbstractSyntaxTree ast = compile(cel, "{?'key': optional.of(5)}"); Map result = (Map) cel.createProgram(ast).eval(); @@ -1263,7 +1338,7 @@ public void optionalMapCreation_valueIsPresent_returnsMap() throws Exception { @Test public void optionalMapCreation_withNestedMap_returnsNestedMap() throws Exception { - Cel cel = + TestCel cel = newCelBuilder() .setResultType( MapType.create( @@ -1293,7 +1368,7 @@ public void optionalMapCreation_withNestedMap_returnsNestedMap() throws Exceptio @Test public void optionalMapCreation_withNestedMapContainingEmptyValue_emptyValueStripped() throws Exception { - Cel cel = + TestCel cel = newCelBuilder() .setResultType( MapType.create( @@ -1314,10 +1389,10 @@ public void optionalMapCreation_withNestedMapContainingEmptyValue_emptyValueStri @Test public void optionalMapCreation_valueIsNonOptional_typeCheck_throws() { - if (testMode.equals(TestMode.PLANNER_PARSE_ONLY)) { + if (testMode.isParseOnly()) { return; } - Cel cel = newCelBuilder().build(); + TestCel cel = newCelBuilder().build(); CelValidationException e = assertThrows(CelValidationException.class, () -> compile(cel, "{?'hi': 'world'}")); @@ -1329,7 +1404,7 @@ public void optionalMapCreation_valueIsNonOptional_typeCheck_throws() { @Test public void optionalMessageCreation_fieldValueIsEmpty_returnsEmptyMessage() throws Exception { - Cel cel = + TestCel cel = newCelBuilder() .setResultType(StructTypeReference.create(TestAllTypes.getDescriptor().getFullName())) .build(); @@ -1343,7 +1418,10 @@ public void optionalMessageCreation_fieldValueIsEmpty_returnsEmptyMessage() thro @Test public void optionalMessageCreation_fieldValueIsPresent_returnsMessage() throws Exception { - Cel cel = newCelBuilder().build(); + // TODO: Message creation with prepopulated fields is not yet supported in + // CelLiteRuntime. + assumeFalse(testMode.isLite()); + TestCel cel = newCelBuilder().build(); CelAbstractSyntaxTree ast = compile(cel, "TestAllTypes{?single_double_wrapper: optional.ofNonZeroValue(5.0)}"); @@ -1363,7 +1441,7 @@ public void optionalMessageCreation_fieldValueIsPresent_returnsMessage() throws + " optional.ofNonZeroValue(m[?'nested'].orValue({}))}\"}") public void optionalMessageCreation_fieldValueContainsEmptyMap_returnsEmptyMessage(String source) throws Exception { - Cel cel = + TestCel cel = newCelBuilder() .addVar( "m", @@ -1380,7 +1458,10 @@ public void optionalMessageCreation_fieldValueContainsEmptyMap_returnsEmptyMessa @Test public void optionalMessageCreation_fieldValueContainsMap_returnsEmptyMessage() throws Exception { - Cel cel = + // TODO: Message creation with prepopulated fields is not yet supported in + // CelLiteRuntime. + assumeFalse(testMode.isLite()); + TestCel cel = newCelBuilder() .addVar( "m", @@ -1402,7 +1483,7 @@ public void optionalMessageCreation_fieldValueContainsMap_returnsEmptyMessage() @Test public void optionalListCreation_allElementsAreEmpty_returnsEmptyList() throws Exception { - Cel cel = + TestCel cel = newCelBuilder() .setResultType(ListType.create(SimpleType.DYN)) .addVar("m", MapType.create(SimpleType.STRING, SimpleType.DYN)) @@ -1424,7 +1505,7 @@ public void optionalListCreation_allElementsAreEmpty_returnsEmptyList() throws E @Test public void optionalListCreation_containsEmptyElements_emptyElementsAreStripped() throws Exception { - Cel cel = + TestCel cel = newCelBuilder() .setResultType(ListType.create(SimpleType.DYN)) .addVar("m", MapType.create(SimpleType.STRING, SimpleType.DYN)) @@ -1445,7 +1526,7 @@ public void optionalListCreation_containsEmptyElements_emptyElementsAreStripped( @Test public void optionalListCreation_containsMixedTypeElements_success() throws Exception { - Cel cel = + TestCel cel = newCelBuilder() .setResultType(ListType.create(SimpleType.DYN)) .addVar( @@ -1479,10 +1560,10 @@ public void optionalListCreation_containsMixedTypeElements_success() throws Exce @Test public void optionalListCreation_containsMixedTypeElements_typeCheck_throwsWhenHomogeneousLiteralsEnabled() { - if (testMode.equals(TestMode.PLANNER_PARSE_ONLY)) { + if (testMode.isParseOnly()) { return; } - Cel cel = + TestCel cel = newCelBuilder() .setOptions(CelOptions.current().enableHomogeneousLiterals(true).build()) .addVar( @@ -1501,7 +1582,10 @@ public void optionalListCreation_containsMixedTypeElements_success() throws Exce @Test public void optionalListCreation_withinProtoMessage_success() throws Exception { - Cel cel = + // TODO: Message creation with prepopulated fields is not yet supported in + // CelLiteRuntime. + assumeFalse(testMode.isLite()); + TestCel cel = newCelBuilder() .addVar("m", MapType.create(SimpleType.STRING, SimpleType.DYN)) .addVar("x", OptionalType.create(SimpleType.DYN)) @@ -1532,7 +1616,7 @@ public void optionalListCreation_withinProtoMessage_success() throws Exception { @Test public void optionalMapMacro_receiverIsEmpty_returnsOptionalEmpty() throws Exception { - Cel cel = + TestCel cel = newCelBuilder() .addVar("x", OptionalType.create(SimpleType.INT)) .setResultType(OptionalType.create(SimpleType.INT)) @@ -1547,7 +1631,7 @@ public void optionalMapMacro_receiverIsEmpty_returnsOptionalEmpty() throws Excep @Test public void optionalMapMacro_receiverHasValue_returnsOptionalValue() throws Exception { - Cel cel = + TestCel cel = newCelBuilder() .addVar("x", OptionalType.create(SimpleType.INT)) .setResultType(OptionalType.create(SimpleType.INT)) @@ -1562,7 +1646,7 @@ public void optionalMapMacro_receiverHasValue_returnsOptionalValue() throws Exce @Test public void optionalMapMacro_withNonIdent_throws() { - Cel cel = + TestCel cel = newCelBuilder() .addVar("x", OptionalType.create(SimpleType.INT)) .setResultType(OptionalType.create(SimpleType.INT)) @@ -1576,7 +1660,7 @@ public void optionalMapMacro_withNonIdent_throws() { @Test public void optionalFlatMapMacro_receiverIsEmpty_returnsOptionalEmpty() throws Exception { - Cel cel = + TestCel cel = newCelBuilder() .addVar("x", OptionalType.create(SimpleType.INT)) .setResultType(OptionalType.create(SimpleType.INT)) @@ -1591,7 +1675,7 @@ public void optionalFlatMapMacro_receiverIsEmpty_returnsOptionalEmpty() throws E @Test public void optionalFlatMapMacro_receiverHasValue_returnsOptionalValue() throws Exception { - Cel cel = + TestCel cel = newCelBuilder() .addVar("x", OptionalType.create(SimpleType.INT)) .setResultType(OptionalType.create(SimpleType.INT)) @@ -1606,7 +1690,7 @@ public void optionalFlatMapMacro_receiverHasValue_returnsOptionalValue() throws @Test public void optionalMapMacro_simpleTarget_notWrappedInComprehension() throws Exception { - Cel cel = + TestCel cel = newCelBuilder() .addVar("x", OptionalType.create(SimpleType.INT)) .setResultType(OptionalType.create(SimpleType.INT)) @@ -1614,11 +1698,15 @@ public void optionalMapMacro_simpleTarget_notWrappedInComprehension() throws Exc CelAbstractSyntaxTree ast = compile(cel, "x.optMap(y, y + 1)"); assertThat(ast.getExpr().exprKind().getKind()).isEqualTo(CelExpr.ExprKind.Kind.CALL); + CelComprehension innerComprehension = + ast.getExpr().call().args().get(1).call().args().get(0).comprehension(); + assertThat(innerComprehension.accuVar()).isEqualTo("y"); + assertThat(innerComprehension.loopCondition().constant().booleanValue()).isTrue(); } @Test public void optionalMapMacro_complexTarget_astWrappedInComprehension() throws Exception { - Cel cel = + TestCel cel = newCelBuilder() .setResultType(OptionalType.create(SimpleType.INT)) .addVar("msg", StructTypeReference.create(TestAllTypes.getDescriptor().getFullName())) @@ -1626,7 +1714,13 @@ public void optionalMapMacro_complexTarget_astWrappedInComprehension() throws Ex CelAbstractSyntaxTree ast = compile(cel, "msg.?single_int32.optMap(y, y + 1)"); assertThat(ast.getExpr().exprKind().getKind()).isEqualTo(CelExpr.ExprKind.Kind.COMPREHENSION); - assertThat(ast.getExpr().comprehension().accuVar()).isEqualTo("@target"); + CelComprehension outerComprehension = ast.getExpr().comprehension(); + assertThat(outerComprehension.accuVar()).isEqualTo("@target"); + assertThat(outerComprehension.loopCondition().constant().booleanValue()).isFalse(); + CelComprehension innerComprehension = + outerComprehension.result().call().args().get(1).call().args().get(0).comprehension(); + assertThat(innerComprehension.accuVar()).isEqualTo("y"); + assertThat(innerComprehension.loopCondition().constant().booleanValue()).isTrue(); Optional result = (Optional) @@ -1637,7 +1731,7 @@ public void optionalMapMacro_complexTarget_astWrappedInComprehension() throws Ex @Test public void optionalFlatMapMacro_simpleTarget_notWrappedInComprehension() throws Exception { - Cel cel = + TestCel cel = newCelBuilder() .addVar("x", OptionalType.create(SimpleType.INT)) .setResultType(OptionalType.create(SimpleType.INT)) @@ -1645,11 +1739,14 @@ public void optionalFlatMapMacro_simpleTarget_notWrappedInComprehension() throws CelAbstractSyntaxTree ast = compile(cel, "x.optFlatMap(y, optional.of(y + 1))"); assertThat(ast.getExpr().exprKind().getKind()).isEqualTo(CelExpr.ExprKind.Kind.CALL); + CelComprehension innerComprehension = ast.getExpr().call().args().get(1).comprehension(); + assertThat(innerComprehension.accuVar()).isEqualTo("y"); + assertThat(innerComprehension.loopCondition().constant().booleanValue()).isTrue(); } @Test public void optionalFlatMapMacro_complexTarget_astWrappedInComprehension() throws Exception { - Cel cel = + TestCel cel = newCelBuilder() .setResultType(OptionalType.create(SimpleType.INT)) .addVar("msg", StructTypeReference.create(TestAllTypes.getDescriptor().getFullName())) @@ -1657,7 +1754,13 @@ public void optionalFlatMapMacro_complexTarget_astWrappedInComprehension() throw CelAbstractSyntaxTree ast = compile(cel, "msg.?single_int32.optFlatMap(y, optional.of(y + 1))"); assertThat(ast.getExpr().exprKind().getKind()).isEqualTo(CelExpr.ExprKind.Kind.COMPREHENSION); - assertThat(ast.getExpr().comprehension().accuVar()).isEqualTo("@target"); + CelComprehension outerComprehension = ast.getExpr().comprehension(); + assertThat(outerComprehension.accuVar()).isEqualTo("@target"); + assertThat(outerComprehension.loopCondition().constant().booleanValue()).isFalse(); + CelComprehension innerComprehension = + outerComprehension.result().call().args().get(1).comprehension(); + assertThat(innerComprehension.accuVar()).isEqualTo("y"); + assertThat(innerComprehension.loopCondition().constant().booleanValue()).isTrue(); Optional result = (Optional) @@ -1669,7 +1772,7 @@ public void optionalFlatMapMacro_complexTarget_astWrappedInComprehension() throw @Test public void optionalFlatMapMacro_withOptionalOfNonZeroValue_optionalEmptyWhenValueIsZero() throws Exception { - Cel cel = + TestCel cel = newCelBuilder() .addVar("x", OptionalType.create(SimpleType.INT)) .setResultType(OptionalType.create(SimpleType.INT)) @@ -1685,7 +1788,7 @@ public void optionalFlatMapMacro_withOptionalOfNonZeroValue_optionalEmptyWhenVal @Test public void optionalFlatMapMacro_withOptionalOfNonZeroValue_optionalValueWhenValueExists() throws Exception { - Cel cel = + TestCel cel = newCelBuilder() .addVar("x", OptionalType.create(SimpleType.INT)) .setResultType(OptionalType.create(SimpleType.INT)) @@ -1700,11 +1803,11 @@ public void optionalFlatMapMacro_withOptionalOfNonZeroValue_optionalValueWhenVal @Test public void optionalFlatMapMacro_mappingExprIsNonOptional_typeCheck_throws() { - if (testMode.equals(TestMode.PLANNER_PARSE_ONLY)) { + if (testMode.isParseOnly()) { return; } - Cel cel = + TestCel cel = newCelBuilder() .addVar("x", OptionalType.create(SimpleType.INT)) .setResultType(OptionalType.create(SimpleType.INT)) @@ -1717,7 +1820,7 @@ public void optionalFlatMapMacro_mappingExprIsNonOptional_typeCheck_throws() { @Test public void optionalFlatMapMacro_withNonIdent_throws() { - Cel cel = + TestCel cel = newCelBuilder() .addVar("x", OptionalType.create(SimpleType.INT)) .setResultType(OptionalType.create(SimpleType.INT)) @@ -1734,7 +1837,7 @@ public void optionalFlatMapMacro_withNonIdent_throws() { @Test public void optionalType_typeResolution() throws Exception { - Cel cel = newCelBuilder().build(); + TestCel cel = newCelBuilder().build(); CelAbstractSyntaxTree ast = compile(cel, "optional_type"); TypeType optionalRuntimeType = (TypeType) cel.createProgram(ast).eval(); @@ -1745,7 +1848,7 @@ public void optionalType_typeResolution() throws Exception { @Test public void optionalType_typeComparison() throws Exception { - Cel cel = newCelBuilder().build(); + TestCel cel = newCelBuilder().build(); CelAbstractSyntaxTree ast = compile(cel, "type(optional.none()) == optional_type"); @@ -1756,7 +1859,7 @@ public void optionalType_typeComparison() throws Exception { @TestParameters("{expression: '[].first().hasValue() == false'}") @TestParameters("{expression: '[\"a\",\"b\",\"c\"].first().value() == \"a\"'}") public void listFirst_success(String expression) throws Exception { - Cel cel = newCelBuilder().build(); + TestCel cel = newCelBuilder().build(); boolean result = (boolean) cel.createProgram(compile(cel, expression)).eval(); assertThat(result).isTrue(); } @@ -1765,7 +1868,7 @@ public void listFirst_success(String expression) throws Exception { @TestParameters("{expression: '[].last().hasValue() == false'}") @TestParameters("{expression: '[1, 2, 3].last().value() == 3'}") public void listLast_success(String expression) throws Exception { - Cel cel = newCelBuilder().build(); + TestCel cel = newCelBuilder().build(); boolean result = (boolean) cel.createProgram(compile(cel, expression)).eval(); assertThat(result).isTrue(); } @@ -1775,12 +1878,12 @@ public void listLast_success(String expression) throws Exception { @TestParameters("{expression: '[2].last()', expectedError: 'undeclared reference to ''last'''}") public void listFirstAndLast_typeCheck_throws_earlyVersion( String expression, String expectedError) throws Exception { - if (testMode.equals(TestMode.PLANNER_PARSE_ONLY)) { + if (testMode.isParseOnly()) { return; } // Configure Cel with an earlier version of the 'optional' library, which did not support // 'first' and 'last' - Cel cel = newCelBuilder(1).build(); + TestCel cel = newCelBuilder(1).build(); assertThat( assertThrows( CelValidationException.class, @@ -1794,7 +1897,7 @@ public void listFirstAndLast_typeCheck_throws_earlyVersion( @Test public void optionalMapCreation_mapKeySetOnNonOptional_throws() { String expression = "{?1: dyn(\"one\")}"; - Cel cel = newCelBuilder().build(); + TestCel cel = newCelBuilder().build(); CelEvaluationException e = assertThrows( @@ -1807,7 +1910,7 @@ public void optionalMapCreation_mapKeySetOnNonOptional_throws() { @Test public void optionalListCreation_listKeySetOnNonOptional_throws() { String expression = "[?dyn(1)]"; - Cel cel = newCelBuilder().build(); + TestCel cel = newCelBuilder().build(); CelEvaluationException e = assertThrows( @@ -1820,7 +1923,7 @@ public void optionalListCreation_listKeySetOnNonOptional_throws() { @Test public void optionalMessageCreation_fieldKeySetOnNonOptional_throws() { String expression = "TestAllTypes{?single_double_wrapper: dyn('foo')}"; - Cel cel = newCelBuilder().build(); + TestCel cel = newCelBuilder().build(); CelEvaluationException e = assertThrows( @@ -1840,9 +1943,9 @@ public void optionalMessageCreation_fieldKeySetOnNonOptional_throws() { @TestParameters("{expression: '[type(optional.none()), int, type([])]'}") public void listType_heterogeneousTypePermutations_resolvesToListDyn(String expression) throws Exception { - assumeFalse(testMode.equals(TestMode.PLANNER_PARSE_ONLY)); + assumeFalse(testMode.isParseOnly()); - Cel cel = newCelBuilder().build(); + TestCel cel = newCelBuilder().build(); CelAbstractSyntaxTree ast = compile(cel, expression); CelList list = ast.getExpr().listOrDefault(); @@ -1857,13 +1960,431 @@ public void listType_heterogeneousTypePermutations_resolvesToListDyn(String expr assertThat((List) cel.createProgram(ast).eval()).hasSize(3); } + private CelAbstractSyntaxTree compile(TestCel cel, String expression) + throws CelValidationException { + return compile(cel.compiler, expression); + } + private CelAbstractSyntaxTree compile(CelCompiler compiler, String expression) throws CelValidationException { CelAbstractSyntaxTree ast = compiler.parse(expression).getAst(); - if (testMode.equals(TestMode.PLANNER_PARSE_ONLY)) { + if (testMode.isParseOnly()) { return ast; } return compiler.check(ast).getAst(); } + + // Intentionally testing the deprecated legacy runtime in the test matrix. + @SuppressWarnings("deprecation") + private Object eval( + CelCompiler celCompiler, CelOptionalRuntimeLibrary runtimeLibrary, String expression) + throws Exception { + CelAbstractSyntaxTree ast = compile(celCompiler, expression); + switch (testMode) { + case LITE_PARSE_ONLY: + case LITE_CHECKED: + CelLiteRuntime celLiteRuntime = + CelLiteRuntimeFactory.newLiteRuntimeBuilder().addLibraries(runtimeLibrary).build(); + return celLiteRuntime.createProgram(ast).eval(); + case PLANNER_PARSE_ONLY: + case PLANNER_CHECKED: + RuntimeEquality plannerEquality = + RuntimeEquality.create(RuntimeHelpers.create(), CelOptions.DEFAULT); + CelRuntime plannerRuntime = + CelRuntimeFactory.plannerRuntimeBuilder() + .addFunctionBindings(runtimeLibrary.newFunctionBindings(plannerEquality)) + .build(); + return plannerRuntime.createProgram(ast).eval(); + case LEGACY_CHECKED: + RuntimeEquality legacyEquality = + RuntimeEquality.create(RuntimeHelpers.create(), CelOptions.DEFAULT); + CelRuntime legacyRuntime = + CelRuntimeFactory.standardCelRuntimeBuilder() + .addFunctionBindings(runtimeLibrary.newFunctionBindings(legacyEquality)) + .build(); + return legacyRuntime.createProgram(ast).eval(); + } + throw new AssertionError("Unexpected testMode: " + testMode); + } + + @Test + public void separateLibraryAndRuntime_allFunctions_success() throws Exception { + CelCompiler celCompiler = + CelCompilerFactory.standardCelCompilerBuilder() + .addLibraries(CelOptionalCompilerLibrary.optional()) + .build(); + + Object result = + eval( + celCompiler, + CelOptionalRuntimeLibrary.optional(), + "optional.of(1).or(optional.none()).value()"); + + assertThat(result).isEqualTo(1L); + } + + @Test + public void separateLibraryAndRuntime_versioned_success() throws Exception { + CelCompiler celCompiler = + CelCompilerFactory.standardCelCompilerBuilder() + .addLibraries(CelOptionalCompilerLibrary.optional(0)) + .build(); + + Object result = + eval(celCompiler, CelOptionalRuntimeLibrary.optional(0), "optional.of(1).value()"); + + assertThat(result).isEqualTo(1L); + } + + @Test + public void separateLibraryAndRuntime_subsetOfFunctions_success() throws Exception { + CelCompiler celCompiler = + CelCompilerFactory.standardCelCompilerBuilder() + .addLibraries( + CelOptionalCompilerLibrary.optional( + CelOptionalCompilerLibrary.Function.OPTIONAL_OF, + CelOptionalCompilerLibrary.Function.VALUE)) + .build(); + CelOptionalRuntimeLibrary runtimeLibrary = + CelOptionalRuntimeLibrary.optional( + CelOptionalRuntimeLibrary.Function.OPTIONAL_OF, + CelOptionalRuntimeLibrary.Function.VALUE); + + Object result = eval(celCompiler, runtimeLibrary, "optional.of(1).value()"); + + assertThat(result).isEqualTo(1L); + if (testMode.isParseOnly()) { + assertThrows( + CelEvaluationException.class, () -> eval(celCompiler, runtimeLibrary, "[1].first()")); + } else { + assertThrows(CelValidationException.class, () -> compile(celCompiler, "[1].first()")); + } + } + + @Test + public void separateLibraryAndRuntime_setOfFunctions_success() throws Exception { + CelCompiler celCompiler = + CelCompilerFactory.standardCelCompilerBuilder() + .addLibraries( + CelOptionalCompilerLibrary.optional( + ImmutableSet.of( + CelOptionalCompilerLibrary.Function.OPTIONAL_OF, + CelOptionalCompilerLibrary.Function.VALUE))) + .build(); + CelOptionalRuntimeLibrary runtimeLibrary = + CelOptionalRuntimeLibrary.optional( + ImmutableSet.of( + CelOptionalRuntimeLibrary.Function.OPTIONAL_OF, + CelOptionalRuntimeLibrary.Function.VALUE)); + + Object result = eval(celCompiler, runtimeLibrary, "optional.of(1).value()"); + + assertThat(result).isEqualTo(1L); + } + + @Test + public void separateLibraryAndRuntime_unsupportedVersion_throws() { + assertThrows(IllegalArgumentException.class, () -> CelOptionalCompilerLibrary.optional(99)); + assertThrows(IllegalArgumentException.class, () -> CelOptionalRuntimeLibrary.optional(99)); + } + + // Intentionally testing the deprecated legacy runtime in the test matrix. + @SuppressWarnings("deprecation") + private CelBuilder newBaseCelBuilder() { + return testMode.equals(TestMode.LEGACY_CHECKED) + ? CelFactory.standardCelBuilder() + : CelFactory.plannerCelBuilder(); + } + + @Test + public void optional_subsetOfFunctions_success() throws Exception { + assumeFalse(testMode.isLite()); + Cel cel = + newBaseCelBuilder() + .addCompilerLibraries( + CelExtensions.optional( + CelOptionalLibrary.Function.OPTIONAL_OF, CelOptionalLibrary.Function.VALUE)) + .addRuntimeLibraries( + CelExtensions.optional( + CelOptionalLibrary.Function.OPTIONAL_OF, CelOptionalLibrary.Function.VALUE)) + .build(); + + CelAbstractSyntaxTree ast = compile(cel, "optional.of(1).value()"); + Object result = cel.createProgram(ast).eval(); + + assertThat(result).isEqualTo(1L); + if (testMode.isParseOnly()) { + CelAbstractSyntaxTree firstAst = compile(cel, "[1].first()"); + CelRuntime.Program firstProgram = cel.createProgram(firstAst); + assertThrows(CelEvaluationException.class, firstProgram::eval); + } else { + assertThrows(CelValidationException.class, () -> compile(cel, "[1].first()")); + } + } + + @Test + public void optional_setOfFunctions_success() throws Exception { + assumeFalse(testMode.isLite()); + Cel cel = + newBaseCelBuilder() + .addCompilerLibraries( + CelExtensions.optional( + ImmutableSet.of( + CelOptionalLibrary.Function.OPTIONAL_OF, + CelOptionalLibrary.Function.VALUE))) + .addRuntimeLibraries( + CelExtensions.optional( + ImmutableSet.of( + CelOptionalLibrary.Function.OPTIONAL_OF, + CelOptionalLibrary.Function.VALUE))) + .build(); + + CelAbstractSyntaxTree ast = compile(cel, "optional.of(1).value()"); + Object result = cel.createProgram(ast).eval(); + + assertThat(result).isEqualTo(1L); + } + + @Test + public void liteRuntime_toRuntimeBuilder_updatesRuntimeEquality() throws Exception { + CelCompiler celCompiler = + CelCompilerFactory.standardCelCompilerBuilder() + .addLibraries(CelOptionalCompilerLibrary.optional()) + .build(); + CelAbstractSyntaxTree ast = compile(celCompiler, "{1: 'a'}[?dyn(1.0)]"); + CelLiteRuntime defaultRuntime = + CelLiteRuntimeFactory.newLiteRuntimeBuilder() + .setStandardFunctions(DynFunction.create()) + .addLibraries(CelOptionalRuntimeLibrary.optional()) + .build(); + CelLiteRuntime strictMapKeyRuntime = + defaultRuntime + .toRuntimeBuilder() + .setOptions(CelOptions.current().disableCelStandardEquality(true).build()) + .build(); + + assertThat(defaultRuntime.createProgram(ast).eval()).isEqualTo(Optional.of("a")); + assertThat(strictMapKeyRuntime.createProgram(ast).eval()).isEqualTo(Optional.empty()); + } + + @Test + public void liteRuntime_mergesIndexOverloadsWithStandardFunction() throws Exception { + CelCompiler celCompiler = + CelCompilerFactory.standardCelCompilerBuilder() + .addLibraries(CelOptionalCompilerLibrary.optional()) + .build(); + CelLiteRuntime runtime = + CelLiteRuntimeFactory.newLiteRuntimeBuilder() + .setStandardFunctions(IndexOperator.create()) + .addLibraries(CelOptionalRuntimeLibrary.optional()) + .build(); + CelAbstractSyntaxTree stdListIndexAst = compile(celCompiler, "[10, 20][0]"); + CelAbstractSyntaxTree optListIndexAst = compile(celCompiler, "optional.of([10, 20])[0]"); + CelAbstractSyntaxTree stdMapIndexAst = compile(celCompiler, "{'a': 10}['a']"); + CelAbstractSyntaxTree optMapIndexAst = compile(celCompiler, "optional.of({'a': 10})['a']"); + + assertThat(runtime.createProgram(stdListIndexAst).eval()).isEqualTo(10L); + assertThat(runtime.createProgram(optListIndexAst).eval()).isEqualTo(Optional.of(10L)); + assertThat(runtime.createProgram(stdMapIndexAst).eval()).isEqualTo(10L); + assertThat(runtime.createProgram(optMapIndexAst).eval()).isEqualTo(Optional.of(10L)); + } + + @Test + public void optional_noArgConstructor_success() { + CelOptionalLibrary extensions = new CelOptionalLibrary(); + + assertThat(extensions.version()).isEqualTo(CelOptionalCompilerLibrary.optional().version()); + assertThat(extensions.functions()).isNotEmpty(); + assertThat(extensions.macros()).isNotEmpty(); + assertThat(extensions.variables()).isNotEmpty(); + } + + @Test + public void runtimeLibrary_constructorsAndFactories() { + CelOptionalRuntimeLibrary lib1 = + new CelOptionalRuntimeLibrary( + ImmutableSet.of(CelOptionalRuntimeLibrary.Function.OPTIONAL_OF)); + assertThat(lib1.functions()).containsExactly(CelOptionalRuntimeLibrary.Function.OPTIONAL_OF); + + CelOptionalRuntimeLibrary lib2 = CelOptionalRuntimeLibrary.optional(); + assertThat(lib2.functions()) + .containsExactlyElementsIn(CelOptionalRuntimeLibrary.Function.values()); + assertThat(CelOptionalRuntimeLibrary.optional(2)).isSameInstanceAs(lib2); + assertThat(CelOptionalRuntimeLibrary.optional(Integer.MAX_VALUE)).isSameInstanceAs(lib2); + + CelOptionalRuntimeLibrary lib3 = CelOptionalRuntimeLibrary.optional(0); + assertThat(lib3.functions()) + .containsExactly( + CelOptionalRuntimeLibrary.Function.OPTIONAL_OF, + CelOptionalRuntimeLibrary.Function.OPTIONAL_OF_NON_ZERO_VALUE, + CelOptionalRuntimeLibrary.Function.OPTIONAL_NONE, + CelOptionalRuntimeLibrary.Function.VALUE, + CelOptionalRuntimeLibrary.Function.HAS_VALUE, + CelOptionalRuntimeLibrary.Function.OPTIONAL_UNWRAP, + CelOptionalRuntimeLibrary.Function.OR, + CelOptionalRuntimeLibrary.Function.OR_VALUE); + assertThat(CelOptionalRuntimeLibrary.optional(1)).isSameInstanceAs(lib3); + + CelOptionalRuntimeLibrary lib4 = + CelOptionalRuntimeLibrary.optional(CelOptionalRuntimeLibrary.Function.OPTIONAL_OF); + assertThat(lib4.functions()).containsExactly(CelOptionalRuntimeLibrary.Function.OPTIONAL_OF); + + CelOptionalRuntimeLibrary lib5 = + CelOptionalRuntimeLibrary.optional( + ImmutableSet.of(CelOptionalRuntimeLibrary.Function.OPTIONAL_OF)); + assertThat(lib5.functions()).containsExactly(CelOptionalRuntimeLibrary.Function.OPTIONAL_OF); + } + + @Test + public void runtimeOptions_unsupported_throws() { + CelOptionalLibrary extensions = new CelOptionalLibrary(); + CelRuntimeBuilder runtimeBuilder = CelRuntimeFactory.plannerRuntimeBuilder(); + assertThrows( + UnsupportedOperationException.class, () -> extensions.setRuntimeOptions(runtimeBuilder)); + + CelOptionalRuntimeLibrary runtimeLibrary = CelOptionalRuntimeLibrary.optional(); + CelLiteRuntimeBuilder liteRuntimeBuilder = CelLiteRuntimeFactory.newLiteRuntimeBuilder(); + assertThrows( + UnsupportedOperationException.class, + () -> runtimeLibrary.setRuntimeOptions(liteRuntimeBuilder)); + } + + @Test + public void optionalMacros_invalidExpanderArguments_throws() throws Exception { + ImmutableSet macros = CelOptionalCompilerLibrary.optional().macros(); + assertThat(macros).hasSize(2); + + CelMacro verifyPreconditionsMacro = + CelMacro.newGlobalMacro( + "verifyPreconditions", + 0, + (exprFactory, unusedTarget, unusedArgs) -> { + CelExpr validTarget = exprFactory.newIdentifier("x"); + for (CelMacro macro : macros) { + assertThrows( + NullPointerException.class, + () -> + macro + .getExpander() + .expandMacro(null, validTarget, ImmutableList.of(validTarget))); + assertThrows( + NullPointerException.class, + () -> macro.getExpander().expandMacro(exprFactory, null, ImmutableList.of())); + assertThrows( + IllegalArgumentException.class, + () -> + macro + .getExpander() + .expandMacro(exprFactory, validTarget, ImmutableList.of())); + } + return Optional.of(exprFactory.newBoolLiteral(true)); + }); + + CelCompiler compiler = + CelCompilerFactory.standardCelCompilerBuilder().addMacros(verifyPreconditionsMacro).build(); + assertThat(compiler.parse("verifyPreconditions()").getAst().getExpr().constant().booleanValue()) + .isTrue(); + } + + /** + * A wrapper around {@link CelBuilder} (which uses {@link CelRuntimeBuilder}) and {@link + * CelLiteRuntimeBuilder} to allow for testing of both runtimes. + */ + private static final class TestCelBuilder { + private final CelBuilder celBuilder; + private final CelCompilerBuilder compilerBuilder; + private final CelLiteRuntimeBuilder liteRuntimeBuilder; + + private TestCelBuilder(CelBuilder celBuilder) { + this.celBuilder = celBuilder; + this.compilerBuilder = null; + this.liteRuntimeBuilder = null; + } + + private TestCelBuilder( + CelCompilerBuilder compilerBuilder, CelLiteRuntimeBuilder liteRuntimeBuilder) { + this.celBuilder = null; + this.compilerBuilder = compilerBuilder; + this.liteRuntimeBuilder = liteRuntimeBuilder; + } + + @CanIgnoreReturnValue + private TestCelBuilder setOptions(CelOptions options) { + if (celBuilder != null) { + celBuilder.setOptions(options); + } else { + compilerBuilder.setOptions(options); + liteRuntimeBuilder.setOptions(options); + } + return this; + } + + @CanIgnoreReturnValue + private TestCelBuilder setResultType(CelType resultType) { + if (celBuilder != null) { + celBuilder.setResultType(resultType); + } else { + compilerBuilder.setResultType(resultType); + } + return this; + } + + @CanIgnoreReturnValue + private TestCelBuilder addVar(String name, CelType type) { + if (celBuilder != null) { + celBuilder.addVar(name, type); + } else { + compilerBuilder.addVar(name, type); + } + return this; + } + + @CanIgnoreReturnValue + private TestCelBuilder addFunctionDeclarations(CelFunctionDecl... decls) { + if (celBuilder != null) { + celBuilder.addFunctionDeclarations(decls); + } else { + compilerBuilder.addFunctionDeclarations(decls); + } + return this; + } + + @CanIgnoreReturnValue + private TestCelBuilder addFunctionBindings(Iterable bindings) { + if (celBuilder != null) { + celBuilder.addFunctionBindings(bindings); + } else { + liteRuntimeBuilder.addFunctionBindings(bindings); + } + return this; + } + + private TestCel build() { + if (celBuilder != null) { + Cel cel = celBuilder.build(); + return new TestCel(cel, cel, null); + } + return new TestCel(compilerBuilder.build(), null, liteRuntimeBuilder.build()); + } + } + + private static final class TestCel { + private final CelCompiler compiler; + private final CelRuntime celRuntime; + private final CelLiteRuntime celLiteRuntime; + + private TestCel(CelCompiler compiler, CelRuntime celRuntime, CelLiteRuntime celLiteRuntime) { + this.compiler = compiler; + this.celRuntime = celRuntime; + this.celLiteRuntime = celLiteRuntime; + } + + private Program createProgram(CelAbstractSyntaxTree ast) throws CelEvaluationException { + if (celRuntime != null) { + return celRuntime.createProgram(ast); + } + return celLiteRuntime.createProgram(ast); + } + } } diff --git a/runtime/src/main/java/dev/cel/runtime/CelLiteRuntimeImpl.java b/runtime/src/main/java/dev/cel/runtime/CelLiteRuntimeImpl.java index d5a376788..00eb6dcfd 100644 --- a/runtime/src/main/java/dev/cel/runtime/CelLiteRuntimeImpl.java +++ b/runtime/src/main/java/dev/cel/runtime/CelLiteRuntimeImpl.java @@ -19,7 +19,6 @@ import com.google.common.annotations.VisibleForTesting; import com.google.common.base.Preconditions; import com.google.common.collect.ImmutableList; -import com.google.common.collect.ImmutableMap; import com.google.common.collect.ImmutableSet; import javax.annotation.concurrent.ThreadSafe; import dev.cel.common.CelAbstractSyntaxTree; @@ -45,8 +44,6 @@ final class CelLiteRuntimeImpl implements CelLiteRuntime { private final CelValueProvider celValueProvider; private final CelTypeProvider celTypeProvider; private final CelContainer celContainer; - - // CEL-Internal-4 private final ImmutableSet runtimeLibraries; @Override @@ -174,9 +171,22 @@ private static void assertAllowedCelOptions(CelOptions celOptions) { } } + private static void addBindingToDispatcher( + DefaultDispatcher.Builder dispatcherBuilder, CelFunctionBinding func) { + String overloadId = func.getOverloadId(); + String functionName = overloadId; + if (func instanceof InternalCelFunctionBinding) { + functionName = ((InternalCelFunctionBinding) func).getFunctionName(); + } + dispatcherBuilder.addOverload( + functionName, overloadId, func.getArgTypes(), func.isStrict(), func.getDefinition()); + } + @Override public CelLiteRuntime build() { assertAllowedCelOptions(celOptions); + ImmutableList userFunctionBindings = + ImmutableList.copyOf(customFunctionBindings.values()); RuntimeHelpers runtimeHelpers = RuntimeHelpers.create(); RuntimeEquality runtimeEquality = RuntimeEquality.create(runtimeHelpers, celOptions); ImmutableSet runtimeLibs = runtimeLibrariesBuilder.build(); @@ -195,9 +205,7 @@ public CelLiteRuntime build() { new CelTypeProvider.CombinedCelTypeProvider(celTypeProvider, this.celTypeProvider); } - ImmutableMap.Builder functionBindingsBuilder = - ImmutableMap.builder(); - + DefaultDispatcher.Builder dispatcherBuilder = DefaultDispatcher.newBuilder(); ImmutableSet standardFunctions = standardFunctionBuilder.build(); for (CelStandardFunction standardFunction : standardFunctions) { for (CelFunctionBinding func : @@ -206,28 +214,13 @@ public CelLiteRuntime build() { runtimeEquality, celTypeProvider, celValueProvider.celValueConverter())) { - functionBindingsBuilder.put(func.getOverloadId(), func); + addBindingToDispatcher(dispatcherBuilder, func); } } - functionBindingsBuilder.putAll(customFunctionBindings); - - DefaultDispatcher.Builder dispatcherBuilder = DefaultDispatcher.newBuilder(); - functionBindingsBuilder - .buildOrThrow() - .forEach( - (String overloadId, CelFunctionBinding func) -> { - String functionName = func.getOverloadId(); - if (func instanceof InternalCelFunctionBinding) { - functionName = ((InternalCelFunctionBinding) func).getFunctionName(); - } - dispatcherBuilder.addOverload( - functionName, - overloadId, - func.getArgTypes(), - func.isStrict(), - func.getDefinition()); - }); + for (CelFunctionBinding func : customFunctionBindings.values()) { + addBindingToDispatcher(dispatcherBuilder, func); + } ProgramPlanner planner = ProgramPlanner.newPlanner( @@ -246,7 +239,7 @@ public CelLiteRuntime build() { return new CelLiteRuntimeImpl( planner, celOptions, - customFunctionBindings.values(), + userFunctionBindings, standardFunctions, runtimeLibs, celValueProvider, @@ -283,7 +276,7 @@ static CelLiteRuntimeBuilder newBuilder() { private CelLiteRuntimeImpl( ProgramPlanner planner, CelOptions celOptions, - Iterable customFunctionBindings, + ImmutableList customFunctionBindings, ImmutableSet celStandardFunctions, ImmutableSet runtimeLibraries, CelValueProvider celValueProvider, @@ -291,7 +284,7 @@ private CelLiteRuntimeImpl( CelContainer celContainer) { this.planner = planner; this.celOptions = celOptions; - this.customFunctionBindings = ImmutableList.copyOf(customFunctionBindings); + this.customFunctionBindings = customFunctionBindings; this.celStandardFunctions = celStandardFunctions; this.runtimeLibraries = runtimeLibraries; this.celValueProvider = celValueProvider; diff --git a/runtime/src/main/java/dev/cel/runtime/FunctionBindingImpl.java b/runtime/src/main/java/dev/cel/runtime/FunctionBindingImpl.java index 7b8efe8fd..fb641fd8d 100644 --- a/runtime/src/main/java/dev/cel/runtime/FunctionBindingImpl.java +++ b/runtime/src/main/java/dev/cel/runtime/FunctionBindingImpl.java @@ -176,6 +176,8 @@ public Object apply(Object[] args) throws CelEvaluationException { public Object apply(Object arg) throws CelEvaluationException { for (CelFunctionBinding overload : overloadBindings) { if (CelFunctionOverload.canHandle(arg, overload.getArgTypes(), overload.isStrict())) { + // Unary function binding setups always instantiate an OptimizedFunctionOverload, avoiding + // an instanceof check on the hotpath. OptimizedFunctionOverload def = (OptimizedFunctionOverload) overload.getDefinition(); return def.apply(arg); } @@ -192,6 +194,8 @@ public Object apply(Object arg1, Object arg2) throws CelEvaluationException { for (CelFunctionBinding overload : overloadBindings) { if (CelFunctionOverload.canHandle( arg1, arg2, overload.getArgTypes(), overload.isStrict())) { + // Binary function binding setups always instantiate an OptimizedFunctionOverload, + // avoiding an instanceof check on the hotpath. OptimizedFunctionOverload def = (OptimizedFunctionOverload) overload.getDefinition(); return def.apply(arg1, arg2); } diff --git a/runtime/src/test/java/dev/cel/runtime/BUILD.bazel b/runtime/src/test/java/dev/cel/runtime/BUILD.bazel index 61b0cd7a2..1e6b8db7c 100644 --- a/runtime/src/test/java/dev/cel/runtime/BUILD.bazel +++ b/runtime/src/test/java/dev/cel/runtime/BUILD.bazel @@ -203,6 +203,7 @@ cel_android_local_test( "//extensions:encoders_runtime_library_android", "//extensions:lists_runtime_library_android", "//extensions:math_runtime_library_android", + "//extensions:optional_runtime_library_android", "//extensions:regex_runtime_library_android", "//extensions:sets_runtime_library_android", "//extensions:strings_runtime_library_android", diff --git a/runtime/src/test/java/dev/cel/runtime/CelLiteRuntimeAndroidTest.java b/runtime/src/test/java/dev/cel/runtime/CelLiteRuntimeAndroidTest.java index 7b7119147..f1a9f4908 100644 --- a/runtime/src/test/java/dev/cel/runtime/CelLiteRuntimeAndroidTest.java +++ b/runtime/src/test/java/dev/cel/runtime/CelLiteRuntimeAndroidTest.java @@ -49,6 +49,7 @@ import dev.cel.extensions.CelEncoderRuntimeLibrary; import dev.cel.extensions.CelListsRuntimeLibrary; import dev.cel.extensions.CelMathRuntimeLibrary; +import dev.cel.extensions.CelOptionalRuntimeLibrary; import dev.cel.extensions.CelRegexRuntimeLibrary; import dev.cel.extensions.CelSetsRuntimeLibrary; import dev.cel.extensions.CelStringRuntimeLibrary; @@ -154,10 +155,8 @@ public void toRuntimeBuilder_propertiesCopied() { assertThat(newRuntimeBuilder.standardFunctionBuilder.build()) .containsExactly(intFunction, equalsOperator) .inOrder(); - assertThat(newRuntimeBuilder.customFunctionBindings).hasSize(3); + assertThat(newRuntimeBuilder.customFunctionBindings).hasSize(1); assertThat(newRuntimeBuilder.customFunctionBindings).containsKey("string_isEmpty"); - assertThat(newRuntimeBuilder.customFunctionBindings).containsKey("list_sets_intersects_list"); - assertThat(newRuntimeBuilder.customFunctionBindings).containsKey("sets.intersects"); } @Test @@ -291,6 +290,51 @@ public void eval_customFunctions() throws Exception { assertThat(result).isTrue(); } + @Test + public void eval_customFunctions_dispatchFailureUsesFunctionName() throws Exception { + CelLiteRuntime runtime = + CelLiteRuntimeFactory.newLiteRuntimeBuilder() + .addFunctionBindings( + CelFunctionBinding.fromOverloads( + "isEmpty", + CelFunctionBinding.from( + "string_isEmpty", + String.class, + unused -> { + throw new IllegalStateException("unexpected failure"); + }))) + .setStandardFunctions(CelStandardFunctions.ALL_STANDARD_FUNCTIONS) + .build(); + // Expr: ''.isEmpty() && [].isEmpty() + CelAbstractSyntaxTree ast = readCheckedExpr("compiled_custom_functions"); + Program program = runtime.createProgram(ast); + + CelEvaluationException e = assertThrows(CelEvaluationException.class, program::eval); + + assertThat(e).hasMessageThat().contains("Function 'isEmpty' failed with arg(s)"); + } + + @Test + public void eval_customFunctions_overloadMismatchUsesFunctionName() throws Exception { + CelLiteRuntime runtime = + CelLiteRuntimeFactory.newLiteRuntimeBuilder() + .addFunctionBindings( + CelFunctionBinding.fromOverloads( + "isEmpty", CelFunctionBinding.from("string_isEmpty", Long.class, x -> x == 0L))) + .setStandardFunctions(CelStandardFunctions.ALL_STANDARD_FUNCTIONS) + .build(); + // Expr: ''.isEmpty() && [].isEmpty() + CelAbstractSyntaxTree ast = readCheckedExpr("compiled_custom_functions"); + Program program = runtime.createProgram(ast); + + CelEvaluationException e = assertThrows(CelEvaluationException.class, program::eval); + + assertThat(e) + .hasMessageThat() + .contains( + "No matching overload for function 'isEmpty'. Overload candidates: string_isEmpty"); + } + @Test @SuppressWarnings("rawtypes") public void eval_customFunctions_asLateBoundFunctions() throws Exception { @@ -818,6 +862,19 @@ public void eval_regexExtension() throws Exception { assertThat(runtime.createProgram(ast).eval()).isEqualTo("hello cel"); } + @Test + public void eval_optionalExtension() throws Exception { + CelLiteRuntime runtime = + CelLiteRuntimeFactory.newLiteRuntimeBuilder() + .setStandardFunctions(CelStandardFunctions.ALL_STANDARD_FUNCTIONS) + .addLibraries(CelOptionalRuntimeLibrary.optional()) + .build(); + // Expr: optional.of(1).or(optional.none()).value() + CelAbstractSyntaxTree ast = readCheckedExpr("compiled_optional_value"); + + assertThat(runtime.createProgram(ast).eval()).isEqualTo(1L); + } + private enum CelOptionsTestCase { UNSIGNED_LONG_DISABLED(newBaseTestOptions().enableUnsignedLongs(false).build()), UNWRAP_WKT_DISABLED(newBaseTestOptions().unwrapWellKnownTypesOnFunctionDispatch(false).build()), diff --git a/testing/src/main/java/dev/cel/testing/compiled/BUILD.bazel b/testing/src/main/java/dev/cel/testing/compiled/BUILD.bazel index 104633e5d..521debcd3 100644 --- a/testing/src/main/java/dev/cel/testing/compiled/BUILD.bazel +++ b/testing/src/main/java/dev/cel/testing/compiled/BUILD.bazel @@ -54,6 +54,7 @@ java_library( ":compiled_lists_slice", ":compiled_math_greatest", ":compiled_one_plus_two", + ":compiled_optional_value", ":compiled_primitive_variables", ":compiled_proto2_deep_traversal", ":compiled_proto2_select_map_fields", @@ -129,6 +130,12 @@ compile_cel( expression = "math.greatest(1, 2)", ) +compile_cel( + name = "compiled_optional_value", + environment = "//testing/environment:all_extensions", + expression = "optional.of(1).or(optional.none()).value()", +) + compile_cel( name = "compiled_sets_contains", environment = "//testing/environment:all_extensions",