From 42b53c9c74ca78b525655187f0dea14767557bb3 Mon Sep 17 00:00:00 2001 From: Radovan Semancik Date: Wed, 1 Jul 2026 15:15:46 +0200 Subject: [PATCH 01/13] Make it buildable without relaying on its own maven artifacts. WARNING: lots of dirty hacks! --- MODULE.bazel | 31 ++++++------- .../src/main/java/dev/cel/checker/BUILD.bazel | 1 + .../src/main/java/dev/cel/common/BUILD.bazel | 4 ++ .../main/java/dev/cel/common/ast/BUILD.bazel | 1 + .../java/dev/cel/common/types/BUILD.bazel | 2 + .../main/java/dev/cel/compiler/BUILD.bazel | 1 + .../test/java/dev/cel/conformance/BUILD.bazel | 11 ++--- .../src/test/java/dev/cel/maven/BUILD.bazel | 44 ++++++++++++++++--- .../src/main/java/dev/cel/parser/BUILD.bazel | 4 ++ publish/cel_version.bzl | 2 +- .../src/main/java/dev/cel/runtime/BUILD.bazel | 6 +++ 11 files changed, 81 insertions(+), 26 deletions(-) diff --git a/MODULE.bazel b/MODULE.bazel index fcaf041ba..1175c2619 100644 --- a/MODULE.bazel +++ b/MODULE.bazel @@ -46,7 +46,7 @@ TRUTH_VERSION = "1.4.4" PROTOBUF_JAVA_VERSION = "4.33.5" -CEL_VERSION = "0.13.1" +CEL_VERSION = "0.13.1e-SNAPSHOT" # Compile only artifacts [ @@ -119,20 +119,21 @@ maven.install( # Conformance test only -maven.install( - name = "maven_conformance", - artifacts = [ - "dev.cel:cel:" + CEL_VERSION, - "dev.cel:compiler:" + CEL_VERSION, - "dev.cel:runtime:" + CEL_VERSION, - ], - repositories = [ - "https://maven.google.com", - "https://repo1.maven.org/maven2", - "https://central.sonatype.com/repository/maven-snapshots/", - ], -) -use_repo(maven, "maven", "maven_android", "maven_conformance") +#maven.install( +# name = "maven_conformance", +# artifacts = [ +# "dev.cel:cel:" + CEL_VERSION, +# "dev.cel:compiler:" + CEL_VERSION, +# "dev.cel:runtime:" + CEL_VERSION, +# ], +# repositories = [ +# "https://maven.google.com", +# "https://repo1.maven.org/maven2", +# "https://central.sonatype.com/repository/maven-snapshots/", +# ], +#) +#use_repo(maven, "maven", "maven_android", "maven_conformance") +use_repo(maven, "maven", "maven_android") non_module_dependencies = use_extension("//:repositories.bzl", "non_module_dependencies") use_repo(non_module_dependencies, "antlr4_jar") diff --git a/checker/src/main/java/dev/cel/checker/BUILD.bazel b/checker/src/main/java/dev/cel/checker/BUILD.bazel index 99d4da586..6afe2a4e6 100644 --- a/checker/src/main/java/dev/cel/checker/BUILD.bazel +++ b/checker/src/main/java/dev/cel/checker/BUILD.bazel @@ -98,6 +98,7 @@ java_library( java_library( name = "checker_builder", srcs = CHECKER_BUILDER_SOURCES, + visibility = ["//visibility:public"], tags = [ ], deps = [ diff --git a/common/src/main/java/dev/cel/common/BUILD.bazel b/common/src/main/java/dev/cel/common/BUILD.bazel index 38548744c..7a6e96c6e 100644 --- a/common/src/main/java/dev/cel/common/BUILD.bazel +++ b/common/src/main/java/dev/cel/common/BUILD.bazel @@ -106,7 +106,9 @@ java_library( # used_by_android tags = [ ], + visibility = ["//visibility:public"], deps = [ +# "@maven_conformance//:dev_cel_common", "//:auto_value", "//common/annotations", "@maven//:com_google_errorprone_error_prone_annotations", @@ -118,6 +120,7 @@ java_library( srcs = PROTO_AST_SOURCE, tags = [ ], + visibility = ["//visibility:public"], deps = [ ":cel_ast", ":cel_source", @@ -358,6 +361,7 @@ java_library( srcs = ["CelContainer.java"], tags = [ ], + visibility = ["//visibility:public"], deps = [ "//:auto_value", "@maven//:com_google_errorprone_error_prone_annotations", diff --git a/common/src/main/java/dev/cel/common/ast/BUILD.bazel b/common/src/main/java/dev/cel/common/ast/BUILD.bazel index 3fc709a07..0e4d927b4 100644 --- a/common/src/main/java/dev/cel/common/ast/BUILD.bazel +++ b/common/src/main/java/dev/cel/common/ast/BUILD.bazel @@ -44,6 +44,7 @@ MUTABLE_EXPR_SOURCES = [ java_library( name = "ast", srcs = AST_SOURCES, + visibility = ["//visibility:public"], tags = [ ], deps = [ diff --git a/common/src/main/java/dev/cel/common/types/BUILD.bazel b/common/src/main/java/dev/cel/common/types/BUILD.bazel index de65d0b1f..fb5b7e2d4 100644 --- a/common/src/main/java/dev/cel/common/types/BUILD.bazel +++ b/common/src/main/java/dev/cel/common/types/BUILD.bazel @@ -46,6 +46,7 @@ java_library( srcs = CEL_TYPE_SOURCES, tags = [ ], + visibility = ["//visibility:public"], deps = [ ":type_providers", "//:auto_value", @@ -168,6 +169,7 @@ java_library( srcs = [ "ProtoMessageTypeProvider.java", ], + visibility = ["//visibility:public"], tags = [ ], deps = [ diff --git a/compiler/src/main/java/dev/cel/compiler/BUILD.bazel b/compiler/src/main/java/dev/cel/compiler/BUILD.bazel index 6a9a80709..9843896e0 100644 --- a/compiler/src/main/java/dev/cel/compiler/BUILD.bazel +++ b/compiler/src/main/java/dev/cel/compiler/BUILD.bazel @@ -59,6 +59,7 @@ java_library( srcs = COMPILER_BUILDER_SOURCES, tags = [ ], + visibility = ["//visibility:public"], deps = [ "//:auto_value", "//checker:checker_builder", diff --git a/conformance/src/test/java/dev/cel/conformance/BUILD.bazel b/conformance/src/test/java/dev/cel/conformance/BUILD.bazel index e9ed58642..147fc031f 100644 --- a/conformance/src/test/java/dev/cel/conformance/BUILD.bazel +++ b/conformance/src/test/java/dev/cel/conformance/BUILD.bazel @@ -43,11 +43,11 @@ java_library( ) MAVEN_JAR_DEPS = [ - "@maven_conformance//:dev_cel_compiler", - "@maven_conformance//:dev_cel_common", - "@maven_conformance//:dev_cel_runtime", - "@maven_conformance//:dev_cel_protobuf", - "@maven_conformance//:dev_cel_cel", +# "@maven_conformance//:dev_cel_compiler", +# "@maven_conformance//:dev_cel_common", +# "@maven_conformance//:dev_cel_runtime", +# "@maven_conformance//:dev_cel_protobuf", +# "@maven_conformance//:dev_cel_cel", ] java_library( @@ -58,6 +58,7 @@ java_library( deps = MAVEN_JAR_DEPS + [ "//:java_truth", "//compiler:compiler_builder", + "//extensions:extension_library", "//parser:parser_factory", "//runtime:runtime_planner_impl", "//testing:expr_value_utils", diff --git a/conformance/src/test/java/dev/cel/maven/BUILD.bazel b/conformance/src/test/java/dev/cel/maven/BUILD.bazel index 895339521..50cc55127 100644 --- a/conformance/src/test/java/dev/cel/maven/BUILD.bazel +++ b/conformance/src/test/java/dev/cel/maven/BUILD.bazel @@ -6,15 +6,15 @@ package(default_applicable_licenses = [ # keep sorted MAVEN_COMPILER_JAR_DEPS = [ - "@maven_conformance//:dev_cel_common", - "@maven_conformance//:dev_cel_compiler", +# "@maven_conformance//:dev_cel_common", +# "@maven_conformance//:dev_cel_compiler", ] # keep sorted MAVEN_RUNTIME_JAR_DEPS = [ - "@maven_conformance//:dev_cel_common", - "@maven_conformance//:dev_cel_protobuf", - "@maven_conformance//:dev_cel_runtime", +# "@maven_conformance//:dev_cel_common", +# "@maven_conformance//:dev_cel_protobuf", +# "@maven_conformance//:dev_cel_runtime", ] java_test( @@ -24,6 +24,22 @@ java_test( deps = MAVEN_COMPILER_JAR_DEPS + [ "//:java_truth", + "//common:cel_ast", + "//common:compiler_common", + "//compiler", + "//checker", + "//parser/src/main/java/dev/cel/parser:parser_factory", + "//parser/src/main/java/dev/cel/parser:unparser", + "//runtime:evaluation_exception", + "//checker/src/main/java/dev/cel/checker:checker_builder", + "//common/src/main/java/dev/cel/common/ast", + "//common/src/main/java/dev/cel/common/types:message_type_provider", + "//common/src/main/java/dev/cel/common/types", + "//common/src/main/java/dev/cel/common:container", + "//common/src/main/java/dev/cel/common:options", + "//compiler/src/main/java/dev/cel/compiler:compiler_builder", + "//parser/src/main/java/dev/cel/parser:macro", + "//parser/src/main/java/dev/cel/parser:parser_builder", "@cel_spec//proto/cel/expr/conformance/proto3:test_all_types_java_proto", "@maven//:com_google_testparameterinjector_test_parameter_injector", "@maven//:junit_junit", @@ -37,6 +53,24 @@ java_test( deps = MAVEN_RUNTIME_JAR_DEPS + [ "//:java_truth", + "//common:cel_ast", + "//common:compiler_common", + "//common:options", + "//runtime:evaluation_exception", + "//runtime:function_binding", + "//runtime:runtime_factory", + "//common/src/main/java/dev/cel/common:proto_ast", + "//runtime/src/main/java/dev/cel/runtime", + "//runtime/src/main/java/dev/cel/runtime:standard_functions", + "//checker/src/main/java/dev/cel/checker:checker_builder", + "//common/src/main/java/dev/cel/common/ast", + "//common/src/main/java/dev/cel/common/types:message_type_provider", + "//common/src/main/java/dev/cel/common/types", + "//common/src/main/java/dev/cel/common:container", + "//common/src/main/java/dev/cel/common:options", + "//compiler/src/main/java/dev/cel/compiler:compiler_builder", + "//parser/src/main/java/dev/cel/parser:macro", + "//parser/src/main/java/dev/cel/parser:parser_builder", "@cel_spec//proto/cel/expr:checked_java_proto", "@cel_spec//proto/cel/expr/conformance/proto3:test_all_types_java_proto", "@maven//:com_google_guava_guava", diff --git a/parser/src/main/java/dev/cel/parser/BUILD.bazel b/parser/src/main/java/dev/cel/parser/BUILD.bazel index e32c50ee8..96e4af5f7 100644 --- a/parser/src/main/java/dev/cel/parser/BUILD.bazel +++ b/parser/src/main/java/dev/cel/parser/BUILD.bazel @@ -42,6 +42,7 @@ java_library( srcs = ["CelParserFactory.java"], tags = [ ], + visibility = ["//visibility:public"], deps = [ ":parser", "//common:options", @@ -80,6 +81,7 @@ java_library( srcs = PARSER_BUILDER_SOURCES, tags = [ ], + visibility = ["//visibility:public"], deps = [ ":macro", "//common:cel_source", @@ -94,6 +96,7 @@ java_library( srcs = MACRO_SOURCES, tags = [ ], + visibility = ["//visibility:public"], deps = [ "//:auto_value", "//common:compiler_common", @@ -111,6 +114,7 @@ java_library( srcs = UNPARSER_SOURCES, tags = [ ], + visibility = ["//visibility:public"], deps = [ ":unparser_visitor", "//common:cel_ast", diff --git a/publish/cel_version.bzl b/publish/cel_version.bzl index 4ceb4bfa4..c1a948f77 100644 --- a/publish/cel_version.bzl +++ b/publish/cel_version.bzl @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. """Maven artifact version for CEL.""" -CEL_VERSION = "0.13.1" +CEL_VERSION = "0.13.1e-SNAPSHOT" diff --git a/runtime/src/main/java/dev/cel/runtime/BUILD.bazel b/runtime/src/main/java/dev/cel/runtime/BUILD.bazel index 17160e346..2a9bc53cf 100644 --- a/runtime/src/main/java/dev/cel/runtime/BUILD.bazel +++ b/runtime/src/main/java/dev/cel/runtime/BUILD.bazel @@ -623,6 +623,7 @@ java_library( srcs = ["CelStandardFunctions.java"], tags = [ ], + visibility = ["//visibility:public"], deps = [ ":function_binding", ":runtime_equality", @@ -799,6 +800,8 @@ java_library( tags = [ ], deps = [ +# "@maven_conformance//:dev_cel_common", + "//common/values", ":evaluation_exception", ":unknown_attributes", "@maven//:com_google_errorprone_error_prone_annotations", @@ -815,6 +818,7 @@ cel_android_library( deps = [ ":evaluation_exception", ":unknown_attributes_android", + "//common/values", "@maven//:com_google_errorprone_error_prone_annotations", "@maven_android//:com_google_guava_guava", ], @@ -930,6 +934,7 @@ java_library( srcs = RUNTIME_SOURCES, tags = [ ], + visibility = ["//visibility:public"], deps = [ ":activation", ":evaluation_exception", @@ -1305,6 +1310,7 @@ java_library( tags = [ ], deps = [ +# "@maven_conformance//:dev_cel_runtime", ":evaluation_exception", ":function_binding", ":function_overload", From 736e095dbef1686bde7a334c4272c9cfb0c73aca Mon Sep 17 00:00:00 2001 From: Radovan Semancik Date: Wed, 1 Jul 2026 15:17:00 +0200 Subject: [PATCH 02/13] First attempt at supporting nullability in CEL. Work in progress. --- .../main/java/dev/cel/common/CelOptions.java | 11 +++- .../dev/cel/runtime/CelFunctionBinding.java | 18 +++++- .../dev/cel/runtime/CelFunctionOverload.java | 56 ++++++++++++++++--- .../cel/runtime/CelLateFunctionBindings.java | 1 + .../dev/cel/runtime/CelResolvedOverload.java | 24 +++++--- .../java/dev/cel/runtime/CelRuntimeImpl.java | 2 + .../dev/cel/runtime/CelRuntimeLegacyImpl.java | 2 + .../dev/cel/runtime/DefaultDispatcher.java | 13 ++++- .../dev/cel/runtime/FunctionBindingImpl.java | 34 ++++++++--- .../cel/runtime/InternalFunctionBinder.java | 8 +-- .../java/dev/cel/runtime/LiteRuntimeImpl.java | 1 + .../standard/NotStrictlyFalseFunction.java | 2 +- .../cel/runtime/CelResolvedOverloadTest.java | 5 ++ .../cel/runtime/DefaultDispatcherTest.java | 2 + .../cel/runtime/DefaultInterpreterTest.java | 2 + .../runtime/planner/ProgramPlannerTest.java | 1 + 16 files changed, 147 insertions(+), 35 deletions(-) diff --git a/common/src/main/java/dev/cel/common/CelOptions.java b/common/src/main/java/dev/cel/common/CelOptions.java index d9c2dd818..eeac117de 100644 --- a/common/src/main/java/dev/cel/common/CelOptions.java +++ b/common/src/main/java/dev/cel/common/CelOptions.java @@ -1,4 +1,5 @@ // Copyright 2022 Google LLC +// Portions Copyright 2026 Evolveum // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. @@ -120,6 +121,8 @@ public enum ProtoUnsetFieldOptions { public abstract int maxRegexProgramSize(); + public abstract boolean enableEasyNull(); + public abstract Builder toBuilder(); /** @@ -163,7 +166,8 @@ public static Builder newBuilder() { .unwrapWellKnownTypesOnFunctionDispatch(true) .fromProtoUnsetFieldOption(ProtoUnsetFieldOptions.BIND_DEFAULT) .enableComprehension(true) - .maxRegexProgramSize(-1); + .maxRegexProgramSize(-1) + .enableEasyNull(false); } /** @@ -508,6 +512,11 @@ public abstract static class Builder { */ public abstract Builder maxRegexProgramSize(int value); + /** + * TODO + */ + public abstract Builder enableEasyNull(boolean value); + /** * Use the `json_name` field option on a protobuf message as the name of the field. * diff --git a/runtime/src/main/java/dev/cel/runtime/CelFunctionBinding.java b/runtime/src/main/java/dev/cel/runtime/CelFunctionBinding.java index 98991d383..89b723656 100644 --- a/runtime/src/main/java/dev/cel/runtime/CelFunctionBinding.java +++ b/runtime/src/main/java/dev/cel/runtime/CelFunctionBinding.java @@ -1,4 +1,5 @@ // Copyright 2025 Google LLC +// Portions Copyright 2026 Evolveum // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. @@ -50,6 +51,8 @@ public interface CelFunctionBinding { boolean isStrict(); + boolean isNullable(); + /** Create a unary function binding from the {@code overloadId}, {@code arg}, and {@code impl}. */ @SuppressWarnings("unchecked") // Safe from CelFunctionOverload.canHandle check before invocation static CelFunctionBinding from( @@ -97,11 +100,22 @@ public Object apply(Object arg1, Object arg2) throws CelEvaluationException { static CelFunctionBinding from( String overloadId, Iterable> argTypes, CelFunctionOverload impl) { return new FunctionBindingImpl( - overloadId, ImmutableList.copyOf(argTypes), impl, /* isStrict= */ true); + overloadId, ImmutableList.copyOf(argTypes), impl, /* isStrict= */ true, determineNullability(argTypes)); + } + + // TODO: find better place? + consider CelOptions + static boolean determineNullability(Iterable> argTypes) { + for( Class argType : argTypes) { + // If overload declaration is using primitive type then it cannot accept null + if (argType.isPrimitive()) { + return false; + } + } + return true; } - /** See {@link #fromOverloads(String, Collection)}. */ + /** See {@link #fromOverloads(String, Collection)}. */ static ImmutableSet fromOverloads( String functionName, CelFunctionBinding... overloadBindings) { return fromOverloads(functionName, ImmutableList.copyOf(overloadBindings)); diff --git a/runtime/src/main/java/dev/cel/runtime/CelFunctionOverload.java b/runtime/src/main/java/dev/cel/runtime/CelFunctionOverload.java index c5f75096d..9f9c37eee 100644 --- a/runtime/src/main/java/dev/cel/runtime/CelFunctionOverload.java +++ b/runtime/src/main/java/dev/cel/runtime/CelFunctionOverload.java @@ -1,4 +1,5 @@ // Copyright 2024 Google LLC +// Portions Copyright 2026 Evolveum // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. @@ -17,6 +18,8 @@ import com.google.common.collect.ImmutableList; import com.google.errorprone.annotations.Immutable; import java.util.Map; +import java.util.Optional; +import dev.cel.common.values.NullValue; /** Interface describing the general signature of all CEL custom function implementations. */ @Immutable @@ -51,14 +54,14 @@ interface Binary { * Returns true if the overload's expected argument types match the types of the given arguments. */ static boolean canHandle( - Object[] arguments, ImmutableList> parameterTypes, boolean isStrict) { + Object[] arguments, ImmutableList> parameterTypes, boolean isStrict, boolean isNullable) { if (parameterTypes.size() != arguments.length) { return false; } for (int i = 0; i < parameterTypes.size(); i++) { Class paramType = parameterTypes.get(i); Object arg = arguments[i]; - boolean result = canHandleArg(arg, paramType, isStrict); + boolean result = canHandleArg(arg, paramType, isStrict, isNullable); if (!result) { return false; } @@ -66,23 +69,27 @@ static boolean canHandle( return true; } - static boolean canHandle(Object arg, ImmutableList> parameterTypes, boolean isStrict) { + static boolean canHandle(Object arg, ImmutableList> parameterTypes, boolean isStrict, boolean isNullable) { if (parameterTypes.size() != 1) { return false; } - return canHandleArg(arg, parameterTypes.get(0), isStrict); + return canHandleArg(arg, parameterTypes.get(0), isStrict, isNullable); } static boolean canHandle( - Object arg1, Object arg2, ImmutableList> parameterTypes, boolean isStrict) { + Object arg1, Object arg2, ImmutableList> parameterTypes, boolean isStrict, boolean isNullable) { if (parameterTypes.size() != 2) { return false; } - return canHandleArg(arg1, parameterTypes.get(0), isStrict) - && canHandleArg(arg2, parameterTypes.get(1), isStrict); + return canHandleArg(arg1, parameterTypes.get(0), isStrict, isNullable) + && canHandleArg(arg2, parameterTypes.get(1), isStrict, isNullable); } - static boolean canHandleArg(Object arg, Class paramType, boolean isStrict) { + static boolean canHandleArg(Object arg, Class paramType, boolean isStrict, boolean isNullable) { + if (isNullable && isNullEquivalent(arg)) { + return true; + } + // null can be assigned to messages, maps, and to objects. // TODO: Remove null special casing if (arg == null) { @@ -100,4 +107,37 @@ static boolean canHandleArg(Object arg, Class paramType, boolean isStrict) { return paramType.isAssignableFrom(arg.getClass()); } + + static Object[] reflectNullability( + Object[] arguments, boolean isNullable) { + if (!isNullable) { + return arguments; + } + Object[] processedArguments = new Object[arguments.length]; + for (int i = 0; i < arguments.length; i++) { + processedArguments[i] = reflectNullability(arguments[i], isNullable); + } + return processedArguments; + } + + static Object reflectNullability( + Object argument, boolean isNullable) { + if (isNullable && isNullEquivalent(argument)) { + return null; + } + return argument; + } + + static boolean isNullEquivalent(Object arg) { + if (arg == null) { + return true; + } + if (arg == NullValue.NULL_VALUE) { + return true; + } + if (arg instanceof Optional) { + return !((Optional)arg).isPresent(); + } + return false; + } } diff --git a/runtime/src/main/java/dev/cel/runtime/CelLateFunctionBindings.java b/runtime/src/main/java/dev/cel/runtime/CelLateFunctionBindings.java index 2da08120c..5ec96ffb7 100644 --- a/runtime/src/main/java/dev/cel/runtime/CelLateFunctionBindings.java +++ b/runtime/src/main/java/dev/cel/runtime/CelLateFunctionBindings.java @@ -72,6 +72,7 @@ private static CelResolvedOverload createResolvedOverload(CelFunctionBinding bin binding.getOverloadId(), binding.getDefinition(), binding.isStrict(), + binding.isNullable(), binding.getArgTypes()); } } diff --git a/runtime/src/main/java/dev/cel/runtime/CelResolvedOverload.java b/runtime/src/main/java/dev/cel/runtime/CelResolvedOverload.java index fbe9a3289..63d68f7a7 100644 --- a/runtime/src/main/java/dev/cel/runtime/CelResolvedOverload.java +++ b/runtime/src/main/java/dev/cel/runtime/CelResolvedOverload.java @@ -1,4 +1,5 @@ // Copyright 2024 Google LLC +// Portions Copyright 2026 Evolveum // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. @@ -53,6 +54,8 @@ public abstract class CelResolvedOverload { */ public abstract boolean isStrict(); + public abstract boolean isNullable(); + /** The function definition. */ public abstract CelFunctionOverload getDefinition(); @@ -61,24 +64,26 @@ public abstract class CelResolvedOverload { public Object invoke(Object[] args) throws CelEvaluationException { // Note: canHandle check is handled separately in DynamicDispatchOverload if (isDynamicDispatch() - || CelFunctionOverload.canHandle(args, getParameterTypes(), isStrict())) { - return getDefinition().apply(args); + || CelFunctionOverload.canHandle(args, getParameterTypes(), isStrict(), isNullable())) { + return getDefinition().apply(CelFunctionOverload.reflectNullability(args, isNullable())); } throw new CelOverloadNotFoundException(getFunctionName(), ImmutableList.of(getOverloadId())); } public Object invoke(Object arg) throws CelEvaluationException { if (isDynamicDispatch() - || CelFunctionOverload.canHandle(arg, getParameterTypes(), isStrict())) { - return getOptimizedDefinition().apply(arg); + || CelFunctionOverload.canHandle(arg, getParameterTypes(), isStrict(), isNullable())) { + return getOptimizedDefinition().apply(CelFunctionOverload.reflectNullability(arg, isNullable())); } throw new CelOverloadNotFoundException(getFunctionName(), ImmutableList.of(getOverloadId())); } public Object invoke(Object arg1, Object arg2) throws CelEvaluationException { if (isDynamicDispatch() - || CelFunctionOverload.canHandle(arg1, arg2, getParameterTypes(), isStrict())) { - return getOptimizedDefinition().apply(arg1, arg2); + || CelFunctionOverload.canHandle(arg1, arg2, getParameterTypes(), isStrict(), isNullable())) { + return getOptimizedDefinition().apply( + CelFunctionOverload.reflectNullability(arg1, isNullable()), + CelFunctionOverload.reflectNullability(arg2, isNullable())); } throw new CelOverloadNotFoundException(getFunctionName(), ImmutableList.of(getOverloadId())); } @@ -92,8 +97,9 @@ public static CelResolvedOverload of( String overloadId, CelFunctionOverload definition, boolean isStrict, + boolean isNullable, Class... parameterTypes) { - return of(functionName, overloadId, definition, isStrict, ImmutableList.copyOf(parameterTypes)); + return of(functionName, overloadId, definition, isStrict, isNullable, ImmutableList.copyOf(parameterTypes)); } /** @@ -105,6 +111,7 @@ public static CelResolvedOverload of( String overloadId, CelFunctionOverload definition, boolean isStrict, + boolean isNullable, List> parameterTypes) { OptimizedFunctionOverload optimizedDef = (definition instanceof OptimizedFunctionOverload) @@ -115,6 +122,7 @@ public static CelResolvedOverload of( overloadId, ImmutableList.copyOf(parameterTypes), isStrict, + isNullable, definition, optimizedDef); } @@ -123,7 +131,7 @@ public static CelResolvedOverload of( * Returns true if the overload's expected argument types match the types of the given arguments. */ boolean canHandle(Object[] arguments) { - return CelFunctionOverload.canHandle(arguments, getParameterTypes(), isStrict()); + return CelFunctionOverload.canHandle(arguments, getParameterTypes(), isStrict(), isNullable()); } private boolean isDynamicDispatch() { diff --git a/runtime/src/main/java/dev/cel/runtime/CelRuntimeImpl.java b/runtime/src/main/java/dev/cel/runtime/CelRuntimeImpl.java index b02f64b61..4a2318892 100644 --- a/runtime/src/main/java/dev/cel/runtime/CelRuntimeImpl.java +++ b/runtime/src/main/java/dev/cel/runtime/CelRuntimeImpl.java @@ -443,6 +443,7 @@ private static DefaultDispatcher newDispatcher( binding.getOverloadId(), binding.getArgTypes(), binding.isStrict(), + binding.isNullable(), binding.getDefinition()); } @@ -456,6 +457,7 @@ private static DefaultDispatcher newDispatcher( binding.getOverloadId(), binding.getArgTypes(), binding.isStrict(), + binding.isNullable(), binding.getDefinition()); } diff --git a/runtime/src/main/java/dev/cel/runtime/CelRuntimeLegacyImpl.java b/runtime/src/main/java/dev/cel/runtime/CelRuntimeLegacyImpl.java index b9ce022cf..582d2a51f 100644 --- a/runtime/src/main/java/dev/cel/runtime/CelRuntimeLegacyImpl.java +++ b/runtime/src/main/java/dev/cel/runtime/CelRuntimeLegacyImpl.java @@ -315,6 +315,7 @@ public CelRuntimeLegacyImpl build() { standardFunctionBinding.getOverloadId(), standardFunctionBinding.getArgTypes(), standardFunctionBinding.isStrict(), + standardFunctionBinding.isNullable(), standardFunctionBinding.getDefinition()); } @@ -328,6 +329,7 @@ public CelRuntimeLegacyImpl build() { customBinding.getOverloadId(), customBinding.getArgTypes(), customBinding.isStrict(), + customBinding.isNullable(), customBinding.getDefinition()); } diff --git a/runtime/src/main/java/dev/cel/runtime/DefaultDispatcher.java b/runtime/src/main/java/dev/cel/runtime/DefaultDispatcher.java index 0a467db81..bd320bba7 100644 --- a/runtime/src/main/java/dev/cel/runtime/DefaultDispatcher.java +++ b/runtime/src/main/java/dev/cel/runtime/DefaultDispatcher.java @@ -140,15 +140,18 @@ abstract static class OverloadEntry { abstract boolean isStrict(); + abstract boolean isNullable(); + abstract CelFunctionOverload overload(); private static OverloadEntry of( String functionName, ImmutableList> argTypes, boolean isStrict, + boolean isNullable, CelFunctionOverload overload) { return new AutoValue_DefaultDispatcher_Builder_OverloadEntry( - functionName, argTypes, isStrict, overload); + functionName, argTypes, isStrict, isNullable, overload); } } @@ -160,6 +163,7 @@ public Builder addOverload( String overloadId, ImmutableList> argTypes, boolean isStrict, + boolean isNullable, CelFunctionOverload overload) { checkNotNull(functionName); checkArgument(!functionName.isEmpty(), "Function name cannot be empty."); @@ -168,7 +172,7 @@ public Builder addOverload( checkNotNull(argTypes); checkNotNull(overload); - OverloadEntry newEntry = OverloadEntry.of(functionName, argTypes, isStrict, overload); + OverloadEntry newEntry = OverloadEntry.of(functionName, argTypes, isStrict, isNullable, overload); overloads.merge( overloadId, @@ -196,8 +200,10 @@ private OverloadEntry mergeDynamicDispatchesOrThrow( boolean isStrict = mergedOverload.getOverloadBindings().stream().allMatch(CelFunctionBinding::isStrict); + boolean isNullable = + mergedOverload.getOverloadBindings().stream().allMatch(CelFunctionBinding::isNullable); - return OverloadEntry.of(overloadId, incoming.argTypes(), isStrict, mergedOverload); + return OverloadEntry.of(overloadId, incoming.argTypes(), isStrict, isNullable, mergedOverload); } throw new IllegalArgumentException("Duplicate overload ID binding: " + overloadId); @@ -217,6 +223,7 @@ public DefaultDispatcher build() { overloadId, overloadImpl, overloadEntry.isStrict(), + overloadEntry.isNullable(), overloadEntry.argTypes())); } diff --git a/runtime/src/main/java/dev/cel/runtime/FunctionBindingImpl.java b/runtime/src/main/java/dev/cel/runtime/FunctionBindingImpl.java index 7b8efe8fd..d7f7e7519 100644 --- a/runtime/src/main/java/dev/cel/runtime/FunctionBindingImpl.java +++ b/runtime/src/main/java/dev/cel/runtime/FunctionBindingImpl.java @@ -1,4 +1,5 @@ // Copyright 2025 Google LLC +// Portions Copyright 2026 Evolveum // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. @@ -35,6 +36,8 @@ final class FunctionBindingImpl implements InternalCelFunctionBinding { private final boolean isStrict; + private final boolean isNullable; + @Override public String getFunctionName() { return functionName; @@ -60,25 +63,33 @@ public boolean isStrict() { return isStrict; } + @Override + public boolean isNullable() { + return isNullable; + } + FunctionBindingImpl( String functionName, String overloadId, ImmutableList> argTypes, CelFunctionOverload definition, - boolean isStrict) { + boolean isStrict, + boolean isNullable) { this.functionName = functionName; this.overloadId = overloadId; this.argTypes = argTypes; this.definition = definition; this.isStrict = isStrict; + this.isNullable = isNullable; } FunctionBindingImpl( String overloadId, ImmutableList> argTypes, CelFunctionOverload definition, - boolean isStrict) { - this(overloadId, overloadId, argTypes, definition, isStrict); + boolean isStrict, + boolean isNullable) { + this(overloadId, overloadId, argTypes, definition, isStrict, isNullable); } static ImmutableSet groupOverloadsToFunction( @@ -87,7 +98,7 @@ static ImmutableSet groupOverloadsToFunction( for (CelFunctionBinding b : overloadBindings) { builder.add( new FunctionBindingImpl( - functionName, b.getOverloadId(), b.getArgTypes(), b.getDefinition(), b.isStrict())); + functionName, b.getOverloadId(), b.getArgTypes(), b.getDefinition(), b.isStrict(), b.isNullable())); } // If there is already a binding with the same name as the function, we treat it as a @@ -105,7 +116,8 @@ static ImmutableSet groupOverloadsToFunction( functionName, singleBinding.getArgTypes(), singleBinding.getDefinition(), - singleBinding.isStrict())); + singleBinding.isStrict(), + singleBinding.isNullable())); } else if (overloadBindings.size() > 1) { builder.add(new DynamicDispatchBinding(functionName, overloadBindings)); } @@ -118,6 +130,7 @@ static ImmutableSet groupOverloadsToFunction( static final class DynamicDispatchBinding implements InternalCelFunctionBinding { private final boolean isStrict; + private final boolean isNullable; private final DynamicDispatchOverload dynamicDispatchOverload; @Override @@ -145,9 +158,14 @@ public boolean isStrict() { return isStrict; } + public boolean isNullable() { + return isNullable; + } + private DynamicDispatchBinding( String functionName, ImmutableSet overloadBindings) { this.isStrict = overloadBindings.stream().allMatch(CelFunctionBinding::isStrict); + this.isNullable = overloadBindings.stream().allMatch(CelFunctionBinding::isNullable); this.dynamicDispatchOverload = new DynamicDispatchOverload(functionName, overloadBindings); } } @@ -160,7 +178,7 @@ static final class DynamicDispatchOverload implements OptimizedFunctionOverload @Override public Object apply(Object[] args) throws CelEvaluationException { for (CelFunctionBinding overload : overloadBindings) { - if (CelFunctionOverload.canHandle(args, overload.getArgTypes(), overload.isStrict())) { + if (CelFunctionOverload.canHandle(args, overload.getArgTypes(), overload.isStrict(), overload.isNullable())) { return overload.getDefinition().apply(args); } } @@ -175,7 +193,7 @@ public Object apply(Object[] args) throws CelEvaluationException { @Override public Object apply(Object arg) throws CelEvaluationException { for (CelFunctionBinding overload : overloadBindings) { - if (CelFunctionOverload.canHandle(arg, overload.getArgTypes(), overload.isStrict())) { + if (CelFunctionOverload.canHandle(arg, overload.getArgTypes(), overload.isStrict(), overload.isNullable())) { OptimizedFunctionOverload def = (OptimizedFunctionOverload) overload.getDefinition(); return def.apply(arg); } @@ -191,7 +209,7 @@ public Object apply(Object arg) throws CelEvaluationException { public Object apply(Object arg1, Object arg2) throws CelEvaluationException { for (CelFunctionBinding overload : overloadBindings) { if (CelFunctionOverload.canHandle( - arg1, arg2, overload.getArgTypes(), overload.isStrict())) { + arg1, arg2, overload.getArgTypes(), overload.isStrict(), overload.isNullable())) { OptimizedFunctionOverload def = (OptimizedFunctionOverload) overload.getDefinition(); return def.apply(arg1, arg2); } diff --git a/runtime/src/main/java/dev/cel/runtime/InternalFunctionBinder.java b/runtime/src/main/java/dev/cel/runtime/InternalFunctionBinder.java index 5a063ee6c..1a47856b5 100644 --- a/runtime/src/main/java/dev/cel/runtime/InternalFunctionBinder.java +++ b/runtime/src/main/java/dev/cel/runtime/InternalFunctionBinder.java @@ -32,8 +32,8 @@ public final class InternalFunctionBinder { */ @SuppressWarnings("unchecked") public static CelFunctionBinding from( - String overloadId, Class arg, CelFunctionOverload.Unary impl, boolean isStrict) { - return from(overloadId, ImmutableList.of(arg), (args) -> impl.apply((T) args[0]), isStrict); + String overloadId, Class arg, CelFunctionOverload.Unary impl, boolean isStrict, boolean isNullable) { + return from(overloadId, ImmutableList.of(arg), (args) -> impl.apply((T) args[0]), isStrict, isNullable); } /** @@ -41,8 +41,8 @@ public static CelFunctionBinding from( * {@code isStrict}. */ public static CelFunctionBinding from( - String overloadId, Iterable> argTypes, CelFunctionOverload impl, boolean isStrict) { - return new FunctionBindingImpl(overloadId, ImmutableList.copyOf(argTypes), impl, isStrict); + String overloadId, Iterable> argTypes, CelFunctionOverload impl, boolean isStrict, boolean isNullable) { + return new FunctionBindingImpl(overloadId, ImmutableList.copyOf(argTypes), impl, isStrict, isNullable); } private InternalFunctionBinder() {} diff --git a/runtime/src/main/java/dev/cel/runtime/LiteRuntimeImpl.java b/runtime/src/main/java/dev/cel/runtime/LiteRuntimeImpl.java index 8ce2d7733..a39c8149f 100644 --- a/runtime/src/main/java/dev/cel/runtime/LiteRuntimeImpl.java +++ b/runtime/src/main/java/dev/cel/runtime/LiteRuntimeImpl.java @@ -172,6 +172,7 @@ public CelLiteRuntime build() { overloadId, func.getArgTypes(), func.isStrict(), + func.isNullable(), func.getDefinition()); }); diff --git a/runtime/src/main/java/dev/cel/runtime/standard/NotStrictlyFalseFunction.java b/runtime/src/main/java/dev/cel/runtime/standard/NotStrictlyFalseFunction.java index 8e0ceead4..fc5ea1c2f 100644 --- a/runtime/src/main/java/dev/cel/runtime/standard/NotStrictlyFalseFunction.java +++ b/runtime/src/main/java/dev/cel/runtime/standard/NotStrictlyFalseFunction.java @@ -48,7 +48,7 @@ public enum NotStrictlyFalseOverload implements CelStandardOverload { return true; }, - /* isStrict= */ false)), + /* isStrict= */ false, /* isNullable= */ false)), ; private final CelStandardOverload bindingCreator; diff --git a/runtime/src/test/java/dev/cel/runtime/CelResolvedOverloadTest.java b/runtime/src/test/java/dev/cel/runtime/CelResolvedOverloadTest.java index 471282117..d99a2fc5a 100644 --- a/runtime/src/test/java/dev/cel/runtime/CelResolvedOverloadTest.java +++ b/runtime/src/test/java/dev/cel/runtime/CelResolvedOverloadTest.java @@ -35,6 +35,7 @@ CelResolvedOverload getIncrementIntOverload() { return arg + 1; }, /* isStrict= */ true, + /* isNullable= */ false, Long.class); } @@ -51,6 +52,7 @@ public void canHandle_nullMessageType_returnsFalse() { /* overloadId= */ "identity_overload", (CelFunctionOverload) (args) -> args[0], /* isStrict= */ true, + /* isNullable= */ false, TestAllTypes.class); assertThat(overload.canHandle(new Object[] {null})).isFalse(); } @@ -63,6 +65,7 @@ public void canHandle_nullPrimitive_returnsFalse() { /* overloadId= */ "identity_overload", (CelFunctionOverload) (args) -> args[0], /* isStrict= */ true, + /* isNullable= */ false, Long.class); assertThat(overload.canHandle(new Object[] {null})).isFalse(); } @@ -88,6 +91,7 @@ public void canHandle_nonStrictOverload_returnsTrue() { return false; }, /* isStrict= */ false, + /* isNullable= */ false, Long.class, Long.class); assertThat( @@ -107,6 +111,7 @@ public void canHandle_nonStrictOverload_returnsFalse() { return false; }, /* isStrict= */ false, + /* isNullable= */ false, Long.class, Long.class); assertThat(nonStrictOverload.canHandle(new Object[] {new RuntimeException(), "Foo"})).isFalse(); diff --git a/runtime/src/test/java/dev/cel/runtime/DefaultDispatcherTest.java b/runtime/src/test/java/dev/cel/runtime/DefaultDispatcherTest.java index d862ddb33..8f56ad605 100644 --- a/runtime/src/test/java/dev/cel/runtime/DefaultDispatcherTest.java +++ b/runtime/src/test/java/dev/cel/runtime/DefaultDispatcherTest.java @@ -41,6 +41,7 @@ public void setup() { /* overloadId= */ "overload_1", args -> (Long) args[0] + 1, /* isStrict= */ true, + /* isNullable= */ false, Long.class)); overloads.put( "overload_2", @@ -49,6 +50,7 @@ public void setup() { /* overloadId= */ "overload_2", args -> (Long) args[0] + 2, /* isStrict= */ true, + /* isNullable= */ false, Long.class)); } diff --git a/runtime/src/test/java/dev/cel/runtime/DefaultInterpreterTest.java b/runtime/src/test/java/dev/cel/runtime/DefaultInterpreterTest.java index a23e3b2fb..b935330a4 100644 --- a/runtime/src/test/java/dev/cel/runtime/DefaultInterpreterTest.java +++ b/runtime/src/test/java/dev/cel/runtime/DefaultInterpreterTest.java @@ -82,6 +82,7 @@ public Object adapt(String messageName, Object message) { /* overloadId= */ "error_overload", ImmutableList.>of(long.class), /* isStrict= */ true, + /* isNullable= */ false, (args) -> new IllegalArgumentException("Always throws")); CelFunctionBinding notStrictlyFalseBinding = NotStrictlyFalseOverload.NOT_STRICTLY_FALSE.newFunctionBinding( @@ -96,6 +97,7 @@ public Object adapt(String messageName, Object message) { notStrictlyFalseBinding.getOverloadId(), notStrictlyFalseBinding.getArgTypes(), notStrictlyFalseBinding.isStrict(), + notStrictlyFalseBinding.isNullable(), notStrictlyFalseBinding.getDefinition()); DefaultInterpreter defaultInterpreter = new DefaultInterpreter( diff --git a/runtime/src/test/java/dev/cel/runtime/planner/ProgramPlannerTest.java b/runtime/src/test/java/dev/cel/runtime/planner/ProgramPlannerTest.java index c749028ff..fbe2c9c44 100644 --- a/runtime/src/test/java/dev/cel/runtime/planner/ProgramPlannerTest.java +++ b/runtime/src/test/java/dev/cel/runtime/planner/ProgramPlannerTest.java @@ -251,6 +251,7 @@ private static void addBindingsToDispatcher( overload.getOverloadId(), overload.getArgTypes(), overload.isStrict(), + overload.isNullable(), overload.getDefinition())); } From 62f10244a8d4c041eff498c19970c26147961457 Mon Sep 17 00:00:00 2001 From: Radovan Semancik Date: Mon, 6 Jul 2026 09:46:36 +0200 Subject: [PATCH 03/13] Nullability: experiment with boolean isNullable --- .../src/main/java/dev/cel/runtime/BUILD.bazel | 4 +- .../dev/cel/runtime/CelFunctionOverload.java | 2 + .../cel/runtime/CelResolvedNullOverload.java | 89 +++++++++++++++++++ .../dev/cel/runtime/DefaultDispatcher.java | 37 ++++++-- 4 files changed, 121 insertions(+), 11 deletions(-) create mode 100644 runtime/src/main/java/dev/cel/runtime/CelResolvedNullOverload.java diff --git a/runtime/src/main/java/dev/cel/runtime/BUILD.bazel b/runtime/src/main/java/dev/cel/runtime/BUILD.bazel index 2a9bc53cf..5775560fa 100644 --- a/runtime/src/main/java/dev/cel/runtime/BUILD.bazel +++ b/runtime/src/main/java/dev/cel/runtime/BUILD.bazel @@ -1306,7 +1306,7 @@ cel_android_library( java_library( name = "resolved_overload", - srcs = ["CelResolvedOverload.java"], + srcs = ["CelResolvedOverload.java", "CelResolvedNullOverload.java"], tags = [ ], deps = [ @@ -1324,7 +1324,7 @@ java_library( cel_android_library( name = "resolved_overload_android", - srcs = ["CelResolvedOverload.java"], + srcs = ["CelResolvedOverload.java", "CelResolvedNullOverload.java"], tags = [ ], deps = [ diff --git a/runtime/src/main/java/dev/cel/runtime/CelFunctionOverload.java b/runtime/src/main/java/dev/cel/runtime/CelFunctionOverload.java index 9f9c37eee..19bcd777a 100644 --- a/runtime/src/main/java/dev/cel/runtime/CelFunctionOverload.java +++ b/runtime/src/main/java/dev/cel/runtime/CelFunctionOverload.java @@ -140,4 +140,6 @@ static boolean isNullEquivalent(Object arg) { } return false; } + + static final Object NULL_VALUE = NullValue.NULL_VALUE; } diff --git a/runtime/src/main/java/dev/cel/runtime/CelResolvedNullOverload.java b/runtime/src/main/java/dev/cel/runtime/CelResolvedNullOverload.java new file mode 100644 index 000000000..77d2ff017 --- /dev/null +++ b/runtime/src/main/java/dev/cel/runtime/CelResolvedNullOverload.java @@ -0,0 +1,89 @@ +// Copyright 2026 Evolveum +// +// 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.runtime; + +import com.google.common.collect.ImmutableList; +import com.google.errorprone.annotations.Immutable; +import dev.cel.common.annotations.Internal; +import java.util.List; + +/** + * TODO + */ +@Immutable +@Internal +public class CelResolvedNullOverload extends CelResolvedOverload { + + private final ImmutableList> parameterTypes; + + private static final OptimizedFunctionOverload OPTIMIZED_FUNCTION_OVERLOAD = new OptimizedFunctionOverload() { + public Object apply(Object[] args) throws CelEvaluationException { + return CelFunctionOverload.NULL_VALUE; + } + }; + + CelResolvedNullOverload(ImmutableList> parameterTypes) { + this.parameterTypes = parameterTypes; + } + + /** The base function name. */ + public String getFunctionName() { + return "nullOverload"; + } + + /** The overload id of the function. */ + public String getOverloadId() { + return "null-overload"; + } + + /** The types of the function parameters. */ + public ImmutableList> getParameterTypes() { + return parameterTypes; + } + + public boolean isStrict() { + return false; + } + + public boolean isNullable() { + return true; + } + + /** The function definition. */ + public CelFunctionOverload getDefinition() { + return OPTIMIZED_FUNCTION_OVERLOAD; + } + + OptimizedFunctionOverload getOptimizedDefinition() { + return OPTIMIZED_FUNCTION_OVERLOAD; + } + + public Object invoke(Object[] args) throws CelEvaluationException { + return CelFunctionOverload.NULL_VALUE; + } + + public Object invoke(Object arg) throws CelEvaluationException { + return CelFunctionOverload.NULL_VALUE; + } + + public Object invoke(Object arg1, Object arg2) throws CelEvaluationException { + return CelFunctionOverload.NULL_VALUE; + } + + public static CelResolvedNullOverload of(List> parameterTypes) { + return new CelResolvedNullOverload(ImmutableList.copyOf(parameterTypes)); + } + +} diff --git a/runtime/src/main/java/dev/cel/runtime/DefaultDispatcher.java b/runtime/src/main/java/dev/cel/runtime/DefaultDispatcher.java index bd320bba7..d40e987fd 100644 --- a/runtime/src/main/java/dev/cel/runtime/DefaultDispatcher.java +++ b/runtime/src/main/java/dev/cel/runtime/DefaultDispatcher.java @@ -1,4 +1,5 @@ // Copyright 2022 Google LLC +// Portions Copyright 2026 Evolveum // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. @@ -18,7 +19,6 @@ import static com.google.common.base.Preconditions.checkNotNull; import com.google.auto.value.AutoValue; -import com.google.common.base.Joiner; import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableMap; import com.google.common.collect.ImmutableSet; @@ -33,6 +33,7 @@ import java.util.List; import java.util.Map; import java.util.Optional; +import java.util.stream.Collectors; /** * Default implementation of dispatcher. @@ -71,7 +72,7 @@ static Optional findOverloadMatchingArgs( throws CelEvaluationException { int matchingOverloadCount = 0; CelResolvedOverload match = null; - List candidates = null; + List candidates = null; for (String overloadId : overloadIds) { CelResolvedOverload overload = overloads.get(overloadId); // If the overload is null, it means that the function was not registered; however, it is @@ -80,24 +81,42 @@ static Optional findOverloadMatchingArgs( if (++matchingOverloadCount > 1) { if (candidates == null) { candidates = new ArrayList<>(); - candidates.add(match.getOverloadId()); + candidates.add(match); } - candidates.add(overloadId); + candidates.add(overload); } match = overload; } } if (matchingOverloadCount > 1) { - throw CelEvaluationExceptionBuilder.newBuilder( - "Ambiguous overloads for function '%s'. Matching candidates: %s", - functionName, Joiner.on(", ").join(candidates)) - .setErrorCode(CelErrorCode.AMBIGUOUS_OVERLOAD) - .build(); + if (isCaseOfNullOverload(args, candidates)) { + return Optional.of( + CelResolvedNullOverload.of(match.getParameterTypes()) + ); + } else { + throw CelEvaluationExceptionBuilder.newBuilder( + "Ambiguous overloads for function '%s'. Matching candidates: %s", + functionName, + candidates + .stream() + .map(CelResolvedOverload::getOverloadId) + .collect(Collectors.joining(", ")) + ) + .setErrorCode(CelErrorCode.AMBIGUOUS_OVERLOAD) + .build(); + } } return Optional.ofNullable(match); } + private static boolean isCaseOfNullOverload(Object[] args, List candidates) { + if (args.length < 1) { + return false; + } + return CelFunctionOverload.isNullEquivalent(args[0]); + } + /** * Finds the single registered overload iff it's marked as a non-strict function. * From 3ea495650e5aaffa860428c1053a2c1b6c8cf345 Mon Sep 17 00:00:00 2001 From: Radovan Semancik Date: Tue, 7 Jul 2026 13:59:11 +0200 Subject: [PATCH 04/13] Nullability properties for function overloads, easy nulls for field selection --- .../java/dev/cel/common/values/BUILD.bazel | 1 + .../src/main/java/dev/cel/runtime/BUILD.bazel | 5 ++ .../dev/cel/runtime/CelFunctionBinding.java | 60 +++++++++++++---- .../dev/cel/runtime/CelFunctionOverload.java | 28 ++++---- .../cel/runtime/CelLateFunctionBindings.java | 2 +- .../cel/runtime/CelResolvedNullOverload.java | 25 ++++---- .../dev/cel/runtime/CelResolvedOverload.java | 26 ++++---- .../java/dev/cel/runtime/CelRuntimeImpl.java | 4 +- .../dev/cel/runtime/CelRuntimeLegacyImpl.java | 4 +- .../dev/cel/runtime/DefaultDispatcher.java | 64 ++++++++++++++----- .../runtime/DescriptorMessageProvider.java | 6 ++ .../dev/cel/runtime/FunctionBindingImpl.java | 35 +++++----- .../cel/runtime/InternalFunctionBinder.java | 8 +-- .../java/dev/cel/runtime/LiteRuntimeImpl.java | 2 +- .../cel/runtime/NullabilityProperties.java | 57 +++++++++++++++++ .../java/dev/cel/runtime/standard/BUILD.bazel | 8 +++ .../runtime/standard/ContainsFunction.java | 5 +- .../runtime/standard/EndsWithFunction.java | 4 +- .../standard/NotStrictlyFalseFunction.java | 3 +- .../runtime/standard/StartsWithFunction.java | 4 +- .../cel/runtime/CelResolvedOverloadTest.java | 10 +-- .../cel/runtime/DefaultDispatcherTest.java | 4 +- .../cel/runtime/DefaultInterpreterTest.java | 4 +- .../runtime/planner/ProgramPlannerTest.java | 2 +- 24 files changed, 260 insertions(+), 111 deletions(-) create mode 100644 runtime/src/main/java/dev/cel/runtime/NullabilityProperties.java diff --git a/common/src/main/java/dev/cel/common/values/BUILD.bazel b/common/src/main/java/dev/cel/common/values/BUILD.bazel index 5ccc498fd..bedcfc80b 100644 --- a/common/src/main/java/dev/cel/common/values/BUILD.bazel +++ b/common/src/main/java/dev/cel/common/values/BUILD.bazel @@ -161,6 +161,7 @@ java_library( srcs = CEL_VALUES_SOURCES, tags = [ ], + visibility = ["//visibility:public"], deps = [ ":cel_byte_string", ":cel_value", diff --git a/runtime/src/main/java/dev/cel/runtime/BUILD.bazel b/runtime/src/main/java/dev/cel/runtime/BUILD.bazel index 5775560fa..9a9f1b5b7 100644 --- a/runtime/src/main/java/dev/cel/runtime/BUILD.bazel +++ b/runtime/src/main/java/dev/cel/runtime/BUILD.bazel @@ -108,6 +108,7 @@ java_library( "//common/internal:proto_message_factory", "//common/types:cel_types", "//common/values:cel_byte_string", + "//common/src/main/java/dev/cel/common/values", "@maven//:com_google_errorprone_error_prone_annotations", "@maven//:com_google_guava_guava", "@maven//:com_google_protobuf_protobuf_java", @@ -796,9 +797,11 @@ java_library( srcs = [ "CelFunctionOverload.java", "OptimizedFunctionOverload.java", + "NullabilityProperties.java" ], tags = [ ], + visibility = ["//visibility:public"], deps = [ # "@maven_conformance//:dev_cel_common", "//common/values", @@ -814,7 +817,9 @@ cel_android_library( srcs = [ "CelFunctionOverload.java", "OptimizedFunctionOverload.java", + "NullabilityProperties.java" ], + visibility = ["//visibility:public"], deps = [ ":evaluation_exception", ":unknown_attributes_android", diff --git a/runtime/src/main/java/dev/cel/runtime/CelFunctionBinding.java b/runtime/src/main/java/dev/cel/runtime/CelFunctionBinding.java index 89b723656..c54f9ef11 100644 --- a/runtime/src/main/java/dev/cel/runtime/CelFunctionBinding.java +++ b/runtime/src/main/java/dev/cel/runtime/CelFunctionBinding.java @@ -51,12 +51,19 @@ public interface CelFunctionBinding { boolean isStrict(); - boolean isNullable(); + NullabilityProperties getNullabilityProperties(); /** Create a unary function binding from the {@code overloadId}, {@code arg}, and {@code impl}. */ @SuppressWarnings("unchecked") // Safe from CelFunctionOverload.canHandle check before invocation static CelFunctionBinding from( - String overloadId, Class arg, CelFunctionOverload.Unary impl) { + String overloadId, Class arg, CelFunctionOverload.Unary impl) { + return from(overloadId, arg, impl, determineNullability(ImmutableList.of(arg))); + } + + /** Create a unary function binding from the {@code overloadId}, {@code arg}, and {@code impl}. */ + @SuppressWarnings("unchecked") // Safe from CelFunctionOverload.canHandle check before invocation + static CelFunctionBinding from( + String overloadId, Class arg, CelFunctionOverload.Unary impl, NullabilityProperties nullabilityProperties) { return from( overloadId, ImmutableList.of(arg), @@ -70,16 +77,26 @@ public Object apply(Object[] args) throws CelEvaluationException { public Object apply(Object arg1) throws CelEvaluationException { return impl.apply((T) arg1); } - }); + }, + nullabilityProperties); } - /** - * Create a binary function binding from the {@code overloadId}, {@code arg1}, {@code arg2}, and - * {@code impl}. - */ + /** + * Create a binary function binding from the {@code overloadId}, {@code arg1}, {@code arg2}, and + * {@code impl}. + */ + @SuppressWarnings("unchecked") // Safe from CelFunctionOverload.canHandle check before invocation + static CelFunctionBinding from( + String overloadId, Class arg1, Class arg2, CelFunctionOverload.Binary impl) { + return from(overloadId, arg1, arg2, impl, determineNullability(ImmutableList.of(arg1, arg2))); + } + @SuppressWarnings("unchecked") // Safe from CelFunctionOverload.canHandle check before invocation static CelFunctionBinding from( - String overloadId, Class arg1, Class arg2, CelFunctionOverload.Binary impl) { + String overloadId, + Class arg1, Class arg2, + CelFunctionOverload.Binary impl, + NullabilityProperties nullabilityProperties) { return from( overloadId, ImmutableList.of(arg1, arg2), @@ -93,25 +110,40 @@ public Object apply(Object[] args) throws CelEvaluationException { public Object apply(Object arg1, Object arg2) throws CelEvaluationException { return impl.apply((T1) arg1, (T2) arg2); } - }); + }, + nullabilityProperties); } /** Create a function binding from the {@code overloadId}, {@code argTypes}, and {@code impl}. */ static CelFunctionBinding from( String overloadId, Iterable> argTypes, CelFunctionOverload impl) { + return from(overloadId, argTypes, impl, determineNullability(argTypes)); + } + + static CelFunctionBinding from( + String overloadId, Iterable> argTypes, + CelFunctionOverload impl, + NullabilityProperties nullabilityProperties) { return new FunctionBindingImpl( - overloadId, ImmutableList.copyOf(argTypes), impl, /* isStrict= */ true, determineNullability(argTypes)); + overloadId, ImmutableList.copyOf(argTypes), impl, /* isStrict= */ true, nullabilityProperties); } - // TODO: find better place? + consider CelOptions - static boolean determineNullability(Iterable> argTypes) { + // TODO: find better place? + consider CelOptions + static NullabilityProperties determineNullability(Iterable> argTypes) { for( Class argType : argTypes) { // If overload declaration is using primitive type then it cannot accept null if (argType.isPrimitive()) { - return false; + return NullabilityProperties.NOT_NULLABLE; } } - return true; + return NullabilityProperties.NULLABLE_NULL; + } + + static NullabilityProperties mergeNullabilityProperties(Collection nullabilityProperties) { + if (nullabilityProperties.stream().distinct().count() > 1) { + throw new IllegalArgumentException("Cannot merge nullability properties"); + } + return nullabilityProperties.iterator().next(); } diff --git a/runtime/src/main/java/dev/cel/runtime/CelFunctionOverload.java b/runtime/src/main/java/dev/cel/runtime/CelFunctionOverload.java index 19bcd777a..8fa93188e 100644 --- a/runtime/src/main/java/dev/cel/runtime/CelFunctionOverload.java +++ b/runtime/src/main/java/dev/cel/runtime/CelFunctionOverload.java @@ -54,14 +54,14 @@ interface Binary { * Returns true if the overload's expected argument types match the types of the given arguments. */ static boolean canHandle( - Object[] arguments, ImmutableList> parameterTypes, boolean isStrict, boolean isNullable) { + Object[] arguments, ImmutableList> parameterTypes, boolean isStrict, NullabilityProperties nullabilityProperties) { if (parameterTypes.size() != arguments.length) { return false; } for (int i = 0; i < parameterTypes.size(); i++) { Class paramType = parameterTypes.get(i); Object arg = arguments[i]; - boolean result = canHandleArg(arg, paramType, isStrict, isNullable); + boolean result = canHandleArg(arg, paramType, isStrict, nullabilityProperties); if (!result) { return false; } @@ -69,24 +69,24 @@ static boolean canHandle( return true; } - static boolean canHandle(Object arg, ImmutableList> parameterTypes, boolean isStrict, boolean isNullable) { + static boolean canHandle(Object arg, ImmutableList> parameterTypes, boolean isStrict, NullabilityProperties nullabilityProperties) { if (parameterTypes.size() != 1) { return false; } - return canHandleArg(arg, parameterTypes.get(0), isStrict, isNullable); + return canHandleArg(arg, parameterTypes.get(0), isStrict, nullabilityProperties); } static boolean canHandle( - Object arg1, Object arg2, ImmutableList> parameterTypes, boolean isStrict, boolean isNullable) { + Object arg1, Object arg2, ImmutableList> parameterTypes, boolean isStrict, NullabilityProperties nullabilityProperties) { if (parameterTypes.size() != 2) { return false; } - return canHandleArg(arg1, parameterTypes.get(0), isStrict, isNullable) - && canHandleArg(arg2, parameterTypes.get(1), isStrict, isNullable); + return canHandleArg(arg1, parameterTypes.get(0), isStrict, nullabilityProperties) + && canHandleArg(arg2, parameterTypes.get(1), isStrict, nullabilityProperties); } - static boolean canHandleArg(Object arg, Class paramType, boolean isStrict, boolean isNullable) { - if (isNullable && isNullEquivalent(arg)) { + static boolean canHandleArg(Object arg, Class paramType, boolean isStrict, NullabilityProperties nullabilityProperties) { + if (nullabilityProperties.isNullable() && isNullEquivalent(arg)) { return true; } @@ -109,20 +109,20 @@ static boolean canHandleArg(Object arg, Class paramType, boolean isStrict, bo } static Object[] reflectNullability( - Object[] arguments, boolean isNullable) { - if (!isNullable) { + Object[] arguments, NullabilityProperties nullabilityProperties) { + if (!nullabilityProperties.isNullable()) { return arguments; } Object[] processedArguments = new Object[arguments.length]; for (int i = 0; i < arguments.length; i++) { - processedArguments[i] = reflectNullability(arguments[i], isNullable); + processedArguments[i] = reflectNullability(arguments[i], nullabilityProperties); } return processedArguments; } static Object reflectNullability( - Object argument, boolean isNullable) { - if (isNullable && isNullEquivalent(argument)) { + Object argument, NullabilityProperties nullabilityProperties) { + if (nullabilityProperties.isNullable() && isNullEquivalent(argument)) { return null; } return argument; diff --git a/runtime/src/main/java/dev/cel/runtime/CelLateFunctionBindings.java b/runtime/src/main/java/dev/cel/runtime/CelLateFunctionBindings.java index 5ec96ffb7..d5e3c49c9 100644 --- a/runtime/src/main/java/dev/cel/runtime/CelLateFunctionBindings.java +++ b/runtime/src/main/java/dev/cel/runtime/CelLateFunctionBindings.java @@ -72,7 +72,7 @@ private static CelResolvedOverload createResolvedOverload(CelFunctionBinding bin binding.getOverloadId(), binding.getDefinition(), binding.isStrict(), - binding.isNullable(), + binding.getNullabilityProperties(), binding.getArgTypes()); } } diff --git a/runtime/src/main/java/dev/cel/runtime/CelResolvedNullOverload.java b/runtime/src/main/java/dev/cel/runtime/CelResolvedNullOverload.java index 77d2ff017..52278f0e4 100644 --- a/runtime/src/main/java/dev/cel/runtime/CelResolvedNullOverload.java +++ b/runtime/src/main/java/dev/cel/runtime/CelResolvedNullOverload.java @@ -27,17 +27,14 @@ public class CelResolvedNullOverload extends CelResolvedOverload { private final ImmutableList> parameterTypes; + private final NullabilityProperties nullabilityProperties; + private final OptimizedFunctionOverload optimizedFunctionOverload; - private static final OptimizedFunctionOverload OPTIMIZED_FUNCTION_OVERLOAD = new OptimizedFunctionOverload() { - public Object apply(Object[] args) throws CelEvaluationException { - return CelFunctionOverload.NULL_VALUE; - } - }; - - CelResolvedNullOverload(ImmutableList> parameterTypes) { + CelResolvedNullOverload(ImmutableList> parameterTypes, NullabilityProperties nullabilityProperties) { this.parameterTypes = parameterTypes; + this.nullabilityProperties = nullabilityProperties; + this.optimizedFunctionOverload = args -> nullabilityProperties.getDefaultFunction().apply(args); } - /** The base function name. */ public String getFunctionName() { return "nullOverload"; @@ -57,17 +54,17 @@ public boolean isStrict() { return false; } - public boolean isNullable() { - return true; + public NullabilityProperties getNullabilityProperties() { + return nullabilityProperties; } /** The function definition. */ public CelFunctionOverload getDefinition() { - return OPTIMIZED_FUNCTION_OVERLOAD; + return optimizedFunctionOverload; } OptimizedFunctionOverload getOptimizedDefinition() { - return OPTIMIZED_FUNCTION_OVERLOAD; + return optimizedFunctionOverload; } public Object invoke(Object[] args) throws CelEvaluationException { @@ -82,8 +79,8 @@ public Object invoke(Object arg1, Object arg2) throws CelEvaluationException { return CelFunctionOverload.NULL_VALUE; } - public static CelResolvedNullOverload of(List> parameterTypes) { - return new CelResolvedNullOverload(ImmutableList.copyOf(parameterTypes)); + public static CelResolvedNullOverload of(List> parameterTypes, NullabilityProperties nullabilityProperties) { + return new CelResolvedNullOverload(ImmutableList.copyOf(parameterTypes), nullabilityProperties); } } diff --git a/runtime/src/main/java/dev/cel/runtime/CelResolvedOverload.java b/runtime/src/main/java/dev/cel/runtime/CelResolvedOverload.java index 63d68f7a7..04f20eeb2 100644 --- a/runtime/src/main/java/dev/cel/runtime/CelResolvedOverload.java +++ b/runtime/src/main/java/dev/cel/runtime/CelResolvedOverload.java @@ -54,7 +54,7 @@ public abstract class CelResolvedOverload { */ public abstract boolean isStrict(); - public abstract boolean isNullable(); + public abstract NullabilityProperties getNullabilityProperties(); /** The function definition. */ public abstract CelFunctionOverload getDefinition(); @@ -64,26 +64,26 @@ public abstract class CelResolvedOverload { public Object invoke(Object[] args) throws CelEvaluationException { // Note: canHandle check is handled separately in DynamicDispatchOverload if (isDynamicDispatch() - || CelFunctionOverload.canHandle(args, getParameterTypes(), isStrict(), isNullable())) { - return getDefinition().apply(CelFunctionOverload.reflectNullability(args, isNullable())); + || CelFunctionOverload.canHandle(args, getParameterTypes(), isStrict(), getNullabilityProperties())) { + return getDefinition().apply(CelFunctionOverload.reflectNullability(args, getNullabilityProperties())); } throw new CelOverloadNotFoundException(getFunctionName(), ImmutableList.of(getOverloadId())); } public Object invoke(Object arg) throws CelEvaluationException { if (isDynamicDispatch() - || CelFunctionOverload.canHandle(arg, getParameterTypes(), isStrict(), isNullable())) { - return getOptimizedDefinition().apply(CelFunctionOverload.reflectNullability(arg, isNullable())); + || CelFunctionOverload.canHandle(arg, getParameterTypes(), isStrict(), getNullabilityProperties())) { + return getOptimizedDefinition().apply(CelFunctionOverload.reflectNullability(arg, getNullabilityProperties())); } throw new CelOverloadNotFoundException(getFunctionName(), ImmutableList.of(getOverloadId())); } public Object invoke(Object arg1, Object arg2) throws CelEvaluationException { if (isDynamicDispatch() - || CelFunctionOverload.canHandle(arg1, arg2, getParameterTypes(), isStrict(), isNullable())) { + || CelFunctionOverload.canHandle(arg1, arg2, getParameterTypes(), isStrict(), getNullabilityProperties())) { return getOptimizedDefinition().apply( - CelFunctionOverload.reflectNullability(arg1, isNullable()), - CelFunctionOverload.reflectNullability(arg2, isNullable())); + CelFunctionOverload.reflectNullability(arg1, getNullabilityProperties()), + CelFunctionOverload.reflectNullability(arg2, getNullabilityProperties())); } throw new CelOverloadNotFoundException(getFunctionName(), ImmutableList.of(getOverloadId())); } @@ -97,9 +97,9 @@ public static CelResolvedOverload of( String overloadId, CelFunctionOverload definition, boolean isStrict, - boolean isNullable, + NullabilityProperties nullabilityProperties, Class... parameterTypes) { - return of(functionName, overloadId, definition, isStrict, isNullable, ImmutableList.copyOf(parameterTypes)); + return of(functionName, overloadId, definition, isStrict, nullabilityProperties, ImmutableList.copyOf(parameterTypes)); } /** @@ -111,7 +111,7 @@ public static CelResolvedOverload of( String overloadId, CelFunctionOverload definition, boolean isStrict, - boolean isNullable, + NullabilityProperties nullabilityProperties, List> parameterTypes) { OptimizedFunctionOverload optimizedDef = (definition instanceof OptimizedFunctionOverload) @@ -122,7 +122,7 @@ public static CelResolvedOverload of( overloadId, ImmutableList.copyOf(parameterTypes), isStrict, - isNullable, + nullabilityProperties, definition, optimizedDef); } @@ -131,7 +131,7 @@ public static CelResolvedOverload of( * Returns true if the overload's expected argument types match the types of the given arguments. */ boolean canHandle(Object[] arguments) { - return CelFunctionOverload.canHandle(arguments, getParameterTypes(), isStrict(), isNullable()); + return CelFunctionOverload.canHandle(arguments, getParameterTypes(), isStrict(), getNullabilityProperties()); } private boolean isDynamicDispatch() { diff --git a/runtime/src/main/java/dev/cel/runtime/CelRuntimeImpl.java b/runtime/src/main/java/dev/cel/runtime/CelRuntimeImpl.java index 4a2318892..adafbac3b 100644 --- a/runtime/src/main/java/dev/cel/runtime/CelRuntimeImpl.java +++ b/runtime/src/main/java/dev/cel/runtime/CelRuntimeImpl.java @@ -443,7 +443,7 @@ private static DefaultDispatcher newDispatcher( binding.getOverloadId(), binding.getArgTypes(), binding.isStrict(), - binding.isNullable(), + binding.getNullabilityProperties(), binding.getDefinition()); } @@ -457,7 +457,7 @@ private static DefaultDispatcher newDispatcher( binding.getOverloadId(), binding.getArgTypes(), binding.isStrict(), - binding.isNullable(), + binding.getNullabilityProperties(), binding.getDefinition()); } diff --git a/runtime/src/main/java/dev/cel/runtime/CelRuntimeLegacyImpl.java b/runtime/src/main/java/dev/cel/runtime/CelRuntimeLegacyImpl.java index 582d2a51f..e42f5801f 100644 --- a/runtime/src/main/java/dev/cel/runtime/CelRuntimeLegacyImpl.java +++ b/runtime/src/main/java/dev/cel/runtime/CelRuntimeLegacyImpl.java @@ -315,7 +315,7 @@ public CelRuntimeLegacyImpl build() { standardFunctionBinding.getOverloadId(), standardFunctionBinding.getArgTypes(), standardFunctionBinding.isStrict(), - standardFunctionBinding.isNullable(), + standardFunctionBinding.getNullabilityProperties(), standardFunctionBinding.getDefinition()); } @@ -329,7 +329,7 @@ public CelRuntimeLegacyImpl build() { customBinding.getOverloadId(), customBinding.getArgTypes(), customBinding.isStrict(), - customBinding.isNullable(), + customBinding.getNullabilityProperties(), customBinding.getDefinition()); } diff --git a/runtime/src/main/java/dev/cel/runtime/DefaultDispatcher.java b/runtime/src/main/java/dev/cel/runtime/DefaultDispatcher.java index d40e987fd..55c1b0f55 100644 --- a/runtime/src/main/java/dev/cel/runtime/DefaultDispatcher.java +++ b/runtime/src/main/java/dev/cel/runtime/DefaultDispatcher.java @@ -91,9 +91,7 @@ static Optional findOverloadMatchingArgs( if (matchingOverloadCount > 1) { if (isCaseOfNullOverload(args, candidates)) { - return Optional.of( - CelResolvedNullOverload.of(match.getParameterTypes()) - ); + return Optional.of(determineNullOverloadOrThrow(functionName, args, candidates)); } else { throw CelEvaluationExceptionBuilder.newBuilder( "Ambiguous overloads for function '%s'. Matching candidates: %s", @@ -110,12 +108,42 @@ static Optional findOverloadMatchingArgs( return Optional.ofNullable(match); } - private static boolean isCaseOfNullOverload(Object[] args, List candidates) { - if (args.length < 1) { - return false; + private static boolean isCaseOfNullOverload(Object[] args, List candidates) { + if (args.length < 1) { + return false; + } + return CelFunctionOverload.isNullEquivalent(args[0]); + } + + private static CelResolvedOverload determineNullOverloadOrThrow( + String functionName, + Object[] args, + List candidates) + throws CelEvaluationException { + CelResolvedOverload matchedOverload = null; + for (CelResolvedOverload candidate : candidates) { + if (!candidate.getNullabilityProperties().isNullable()) { + continue; + } + if (matchedOverload == null) { + matchedOverload = candidate; + } else { + if (!candidate.getNullabilityProperties().equals(matchedOverload.getNullabilityProperties())) { + throw CelEvaluationExceptionBuilder.newBuilder( + "Ambiguous overloads for function '%s' due to mismatch in nullability properties. Matching candidates: %s", + functionName, + candidates + .stream() + .map(CelResolvedOverload::getOverloadId) + .collect(Collectors.joining(", ")) + ) + .setErrorCode(CelErrorCode.AMBIGUOUS_OVERLOAD) + .build(); + } } - return CelFunctionOverload.isNullEquivalent(args[0]); } + return CelResolvedNullOverload.of(matchedOverload.getParameterTypes(), matchedOverload.getNullabilityProperties()); + } /** * Finds the single registered overload iff it's marked as a non-strict function. @@ -159,7 +187,7 @@ abstract static class OverloadEntry { abstract boolean isStrict(); - abstract boolean isNullable(); + abstract NullabilityProperties nullabilityProperties(); abstract CelFunctionOverload overload(); @@ -167,10 +195,10 @@ private static OverloadEntry of( String functionName, ImmutableList> argTypes, boolean isStrict, - boolean isNullable, + NullabilityProperties nullabilityProperties, CelFunctionOverload overload) { return new AutoValue_DefaultDispatcher_Builder_OverloadEntry( - functionName, argTypes, isStrict, isNullable, overload); + functionName, argTypes, isStrict, nullabilityProperties, overload); } } @@ -182,7 +210,7 @@ public Builder addOverload( String overloadId, ImmutableList> argTypes, boolean isStrict, - boolean isNullable, + NullabilityProperties nullabilityProperties, CelFunctionOverload overload) { checkNotNull(functionName); checkArgument(!functionName.isEmpty(), "Function name cannot be empty."); @@ -191,7 +219,7 @@ public Builder addOverload( checkNotNull(argTypes); checkNotNull(overload); - OverloadEntry newEntry = OverloadEntry.of(functionName, argTypes, isStrict, isNullable, overload); + OverloadEntry newEntry = OverloadEntry.of(functionName, argTypes, isStrict, nullabilityProperties, overload); overloads.merge( overloadId, @@ -219,10 +247,14 @@ private OverloadEntry mergeDynamicDispatchesOrThrow( boolean isStrict = mergedOverload.getOverloadBindings().stream().allMatch(CelFunctionBinding::isStrict); - boolean isNullable = - mergedOverload.getOverloadBindings().stream().allMatch(CelFunctionBinding::isNullable); + if (mergedOverload.getOverloadBindings().stream().map(CelFunctionBinding::getNullabilityProperties).distinct().count() > 1) { + throw new IllegalArgumentException("Cannot merge overload '" + overloadId + "' due to mismatch in nullability properties"); + } + NullabilityProperties nullabilityProperties = CelFunctionBinding.mergeNullabilityProperties( + mergedOverload.getOverloadBindings().stream() + .map(CelFunctionBinding::getNullabilityProperties).collect(Collectors.toList())); - return OverloadEntry.of(overloadId, incoming.argTypes(), isStrict, isNullable, mergedOverload); + return OverloadEntry.of(overloadId, incoming.argTypes(), isStrict, nullabilityProperties, mergedOverload); } throw new IllegalArgumentException("Duplicate overload ID binding: " + overloadId); @@ -242,7 +274,7 @@ public DefaultDispatcher build() { overloadId, overloadImpl, overloadEntry.isStrict(), - overloadEntry.isNullable(), + overloadEntry.nullabilityProperties(), overloadEntry.argTypes())); } diff --git a/runtime/src/main/java/dev/cel/runtime/DescriptorMessageProvider.java b/runtime/src/main/java/dev/cel/runtime/DescriptorMessageProvider.java index ecbba5e7e..2993ab2b9 100644 --- a/runtime/src/main/java/dev/cel/runtime/DescriptorMessageProvider.java +++ b/runtime/src/main/java/dev/cel/runtime/DescriptorMessageProvider.java @@ -109,6 +109,10 @@ public DescriptorMessageProvider(ProtoMessageFactory protoMessageFactory, CelOpt message = optionalMessage.get(); } + if (celOptions.enableEasyNull() && message instanceof dev.cel.common.values.NullValue) { + return dev.cel.common.values.NullValue.NULL_VALUE; + } + if (message instanceof Map) { Map map = (Map) message; if (map.containsKey(fieldName)) { @@ -118,6 +122,8 @@ public DescriptorMessageProvider(ProtoMessageFactory protoMessageFactory, CelOpt if (isOptionalMessage) { return Optional.empty(); + } else if (celOptions.enableEasyNull()) { + return dev.cel.common.values.NullValue.NULL_VALUE; } else { throw CelAttributeNotFoundException.forMissingMapKey(fieldName); } diff --git a/runtime/src/main/java/dev/cel/runtime/FunctionBindingImpl.java b/runtime/src/main/java/dev/cel/runtime/FunctionBindingImpl.java index d7f7e7519..f6fc66957 100644 --- a/runtime/src/main/java/dev/cel/runtime/FunctionBindingImpl.java +++ b/runtime/src/main/java/dev/cel/runtime/FunctionBindingImpl.java @@ -23,6 +23,8 @@ import com.google.errorprone.annotations.Immutable; import dev.cel.common.exceptions.CelOverloadNotFoundException; +import java.util.stream.Collectors; + @Immutable final class FunctionBindingImpl implements InternalCelFunctionBinding { @@ -36,7 +38,7 @@ final class FunctionBindingImpl implements InternalCelFunctionBinding { private final boolean isStrict; - private final boolean isNullable; + private final NullabilityProperties nullabilityProperties; @Override public String getFunctionName() { @@ -64,8 +66,8 @@ public boolean isStrict() { } @Override - public boolean isNullable() { - return isNullable; + public NullabilityProperties getNullabilityProperties() { + return nullabilityProperties; } FunctionBindingImpl( @@ -74,13 +76,13 @@ public boolean isNullable() { ImmutableList> argTypes, CelFunctionOverload definition, boolean isStrict, - boolean isNullable) { + NullabilityProperties nullabilityProperties) { this.functionName = functionName; this.overloadId = overloadId; this.argTypes = argTypes; this.definition = definition; this.isStrict = isStrict; - this.isNullable = isNullable; + this.nullabilityProperties = nullabilityProperties; } FunctionBindingImpl( @@ -88,8 +90,8 @@ public boolean isNullable() { ImmutableList> argTypes, CelFunctionOverload definition, boolean isStrict, - boolean isNullable) { - this(overloadId, overloadId, argTypes, definition, isStrict, isNullable); + NullabilityProperties nullabilityProperties) { + this(overloadId, overloadId, argTypes, definition, isStrict, nullabilityProperties); } static ImmutableSet groupOverloadsToFunction( @@ -98,7 +100,7 @@ static ImmutableSet groupOverloadsToFunction( for (CelFunctionBinding b : overloadBindings) { builder.add( new FunctionBindingImpl( - functionName, b.getOverloadId(), b.getArgTypes(), b.getDefinition(), b.isStrict(), b.isNullable())); + functionName, b.getOverloadId(), b.getArgTypes(), b.getDefinition(), b.isStrict(), b.getNullabilityProperties())); } // If there is already a binding with the same name as the function, we treat it as a @@ -117,7 +119,7 @@ static ImmutableSet groupOverloadsToFunction( singleBinding.getArgTypes(), singleBinding.getDefinition(), singleBinding.isStrict(), - singleBinding.isNullable())); + singleBinding.getNullabilityProperties())); } else if (overloadBindings.size() > 1) { builder.add(new DynamicDispatchBinding(functionName, overloadBindings)); } @@ -130,7 +132,7 @@ static ImmutableSet groupOverloadsToFunction( static final class DynamicDispatchBinding implements InternalCelFunctionBinding { private final boolean isStrict; - private final boolean isNullable; + private final NullabilityProperties nullabilityProperties; private final DynamicDispatchOverload dynamicDispatchOverload; @Override @@ -158,14 +160,15 @@ public boolean isStrict() { return isStrict; } - public boolean isNullable() { - return isNullable; + public NullabilityProperties getNullabilityProperties() { + return nullabilityProperties; } private DynamicDispatchBinding( String functionName, ImmutableSet overloadBindings) { this.isStrict = overloadBindings.stream().allMatch(CelFunctionBinding::isStrict); - this.isNullable = overloadBindings.stream().allMatch(CelFunctionBinding::isNullable); + this.nullabilityProperties = CelFunctionBinding.mergeNullabilityProperties( + overloadBindings.stream().map(CelFunctionBinding::getNullabilityProperties).collect(Collectors.toList())); this.dynamicDispatchOverload = new DynamicDispatchOverload(functionName, overloadBindings); } } @@ -178,7 +181,7 @@ static final class DynamicDispatchOverload implements OptimizedFunctionOverload @Override public Object apply(Object[] args) throws CelEvaluationException { for (CelFunctionBinding overload : overloadBindings) { - if (CelFunctionOverload.canHandle(args, overload.getArgTypes(), overload.isStrict(), overload.isNullable())) { + if (CelFunctionOverload.canHandle(args, overload.getArgTypes(), overload.isStrict(), overload.getNullabilityProperties())) { return overload.getDefinition().apply(args); } } @@ -193,7 +196,7 @@ public Object apply(Object[] args) throws CelEvaluationException { @Override public Object apply(Object arg) throws CelEvaluationException { for (CelFunctionBinding overload : overloadBindings) { - if (CelFunctionOverload.canHandle(arg, overload.getArgTypes(), overload.isStrict(), overload.isNullable())) { + if (CelFunctionOverload.canHandle(arg, overload.getArgTypes(), overload.isStrict(), overload.getNullabilityProperties())) { OptimizedFunctionOverload def = (OptimizedFunctionOverload) overload.getDefinition(); return def.apply(arg); } @@ -209,7 +212,7 @@ public Object apply(Object arg) throws CelEvaluationException { public Object apply(Object arg1, Object arg2) throws CelEvaluationException { for (CelFunctionBinding overload : overloadBindings) { if (CelFunctionOverload.canHandle( - arg1, arg2, overload.getArgTypes(), overload.isStrict(), overload.isNullable())) { + arg1, arg2, overload.getArgTypes(), overload.isStrict(), overload.getNullabilityProperties())) { OptimizedFunctionOverload def = (OptimizedFunctionOverload) overload.getDefinition(); return def.apply(arg1, arg2); } diff --git a/runtime/src/main/java/dev/cel/runtime/InternalFunctionBinder.java b/runtime/src/main/java/dev/cel/runtime/InternalFunctionBinder.java index 1a47856b5..bfcde3bc9 100644 --- a/runtime/src/main/java/dev/cel/runtime/InternalFunctionBinder.java +++ b/runtime/src/main/java/dev/cel/runtime/InternalFunctionBinder.java @@ -32,8 +32,8 @@ public final class InternalFunctionBinder { */ @SuppressWarnings("unchecked") public static CelFunctionBinding from( - String overloadId, Class arg, CelFunctionOverload.Unary impl, boolean isStrict, boolean isNullable) { - return from(overloadId, ImmutableList.of(arg), (args) -> impl.apply((T) args[0]), isStrict, isNullable); + String overloadId, Class arg, CelFunctionOverload.Unary impl, boolean isStrict, NullabilityProperties nullabilityProperties) { + return from(overloadId, ImmutableList.of(arg), (args) -> impl.apply((T) args[0]), isStrict, nullabilityProperties); } /** @@ -41,8 +41,8 @@ public static CelFunctionBinding from( * {@code isStrict}. */ public static CelFunctionBinding from( - String overloadId, Iterable> argTypes, CelFunctionOverload impl, boolean isStrict, boolean isNullable) { - return new FunctionBindingImpl(overloadId, ImmutableList.copyOf(argTypes), impl, isStrict, isNullable); + String overloadId, Iterable> argTypes, CelFunctionOverload impl, boolean isStrict, NullabilityProperties nullabilityProperties) { + return new FunctionBindingImpl(overloadId, ImmutableList.copyOf(argTypes), impl, isStrict, nullabilityProperties); } private InternalFunctionBinder() {} diff --git a/runtime/src/main/java/dev/cel/runtime/LiteRuntimeImpl.java b/runtime/src/main/java/dev/cel/runtime/LiteRuntimeImpl.java index a39c8149f..04187296f 100644 --- a/runtime/src/main/java/dev/cel/runtime/LiteRuntimeImpl.java +++ b/runtime/src/main/java/dev/cel/runtime/LiteRuntimeImpl.java @@ -172,7 +172,7 @@ public CelLiteRuntime build() { overloadId, func.getArgTypes(), func.isStrict(), - func.isNullable(), + func.getNullabilityProperties(), func.getDefinition()); }); diff --git a/runtime/src/main/java/dev/cel/runtime/NullabilityProperties.java b/runtime/src/main/java/dev/cel/runtime/NullabilityProperties.java new file mode 100644 index 000000000..c1b2590b2 --- /dev/null +++ b/runtime/src/main/java/dev/cel/runtime/NullabilityProperties.java @@ -0,0 +1,57 @@ +// Copyright 2026 Evolveum +// +// 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.runtime; + +import com.google.errorprone.annotations.Immutable; +import dev.cel.common.values.NullValue; + +import java.util.Objects; + +/** Interface describing the general signature of all CEL custom function implementations. */ +@Immutable +public class NullabilityProperties { + + public static final NullabilityProperties NOT_NULLABLE = new NullabilityProperties(false, null); + public static final NullabilityProperties NULLABLE_NULL = new NullabilityProperties(true, args -> NullValue.NULL_VALUE); + public static final NullabilityProperties NULLABLE_FALSE = new NullabilityProperties(true, args -> false); + + private final boolean isNullable; + private final CelFunctionOverload defaultFunction; + + public NullabilityProperties(boolean isNullable, CelFunctionOverload defaultFunction) { + this.isNullable = isNullable; + this.defaultFunction = defaultFunction; + } + + public boolean isNullable() { + return isNullable; + } + + public CelFunctionOverload getDefaultFunction() { + return defaultFunction; + } + + @Override + public boolean equals(Object o) { + if (!(o instanceof NullabilityProperties)) return false; + NullabilityProperties that = (NullabilityProperties) o; + return isNullable == that.isNullable && Objects.equals(defaultFunction, that.defaultFunction); + } + + @Override + public int hashCode() { + return Objects.hash(isNullable, defaultFunction); + } +} diff --git a/runtime/src/main/java/dev/cel/runtime/standard/BUILD.bazel b/runtime/src/main/java/dev/cel/runtime/standard/BUILD.bazel index 0a76b6135..de2261267 100644 --- a/runtime/src/main/java/dev/cel/runtime/standard/BUILD.bazel +++ b/runtime/src/main/java/dev/cel/runtime/standard/BUILD.bazel @@ -204,6 +204,7 @@ java_library( "//runtime:function_binding", "//runtime:runtime_equality", "//runtime/standard:standard_function", + "//runtime/src/main/java/dev/cel/runtime:function_overload", "@maven//:com_google_guava_guava", ], ) @@ -219,6 +220,7 @@ cel_android_library( "//common:options", "//runtime:function_binding_android", "//runtime:runtime_equality_android", + "//runtime/src/main/java/dev/cel/runtime:function_overload_android", "@maven_android//:com_google_guava_guava", ], ) @@ -332,6 +334,7 @@ java_library( "//runtime:function_binding", "//runtime:runtime_equality", "//runtime/standard:standard_function", + "//runtime/src/main/java/dev/cel/runtime:function_overload", "@maven//:com_google_guava_guava", ], ) @@ -347,6 +350,7 @@ cel_android_library( "//common:options", "//runtime:function_binding_android", "//runtime:runtime_equality_android", + "//runtime/src/main/java/dev/cel/runtime:function_overload_android", "@maven_android//:com_google_guava_guava", ], ) @@ -1292,6 +1296,7 @@ java_library( "//runtime:function_binding", "//runtime:runtime_equality", "//runtime/standard:standard_function", + "//runtime/src/main/java/dev/cel/runtime:function_overload", "@maven//:com_google_guava_guava", ], ) @@ -1307,6 +1312,7 @@ cel_android_library( "//common:options", "//runtime:function_binding_android", "//runtime:runtime_equality_android", + "//runtime/src/main/java/dev/cel/runtime:function_overload_android", "@maven_android//:com_google_guava_guava", ], ) @@ -1472,6 +1478,7 @@ java_library( "//runtime:internal_function_binder", "//runtime:runtime_equality", "//runtime/standard:standard_function", + "//runtime/src/main/java/dev/cel/runtime:function_overload", "@maven//:com_google_guava_guava", ], ) @@ -1487,6 +1494,7 @@ cel_android_library( "//common:operator_android", "//common:options", "//runtime:function_binding_android", + "//runtime/src/main/java/dev/cel/runtime:function_overload_android", "//runtime:internal_function_binder_android", "//runtime:runtime_equality_android", "@maven_android//:com_google_guava_guava", diff --git a/runtime/src/main/java/dev/cel/runtime/standard/ContainsFunction.java b/runtime/src/main/java/dev/cel/runtime/standard/ContainsFunction.java index bf3f9f7a7..c3081f644 100644 --- a/runtime/src/main/java/dev/cel/runtime/standard/ContainsFunction.java +++ b/runtime/src/main/java/dev/cel/runtime/standard/ContainsFunction.java @@ -1,4 +1,5 @@ // Copyright 2025 Google LLC +// Portions copyright 2026 Evolveum // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. @@ -17,6 +18,7 @@ import com.google.common.collect.ImmutableSet; import dev.cel.common.CelOptions; import dev.cel.runtime.CelFunctionBinding; +import dev.cel.runtime.NullabilityProperties; import dev.cel.runtime.RuntimeEquality; import java.util.Arrays; @@ -41,7 +43,8 @@ public enum ContainsOverload implements CelStandardOverload { CONTAINS_STRING( (celOptions, runtimeEquality) -> CelFunctionBinding.from( - "contains_string", String.class, String.class, String::contains)), + "contains_string", String.class, String.class, String::contains, + NullabilityProperties.NULLABLE_FALSE)), ; private final CelStandardOverload standardOverload; diff --git a/runtime/src/main/java/dev/cel/runtime/standard/EndsWithFunction.java b/runtime/src/main/java/dev/cel/runtime/standard/EndsWithFunction.java index 7f4e8c035..42ae324ae 100644 --- a/runtime/src/main/java/dev/cel/runtime/standard/EndsWithFunction.java +++ b/runtime/src/main/java/dev/cel/runtime/standard/EndsWithFunction.java @@ -17,6 +17,7 @@ import com.google.common.collect.ImmutableSet; import dev.cel.common.CelOptions; import dev.cel.runtime.CelFunctionBinding; +import dev.cel.runtime.NullabilityProperties; import dev.cel.runtime.RuntimeEquality; import java.util.Arrays; @@ -41,7 +42,8 @@ public enum EndsWithOverload implements CelStandardOverload { ENDS_WITH_STRING( (celOptions, runtimeEquality) -> CelFunctionBinding.from( - "ends_with_string", String.class, String.class, String::endsWith)), + "ends_with_string", String.class, String.class, String::endsWith, + NullabilityProperties.NULLABLE_FALSE)), ; private final CelStandardOverload standardOverload; diff --git a/runtime/src/main/java/dev/cel/runtime/standard/NotStrictlyFalseFunction.java b/runtime/src/main/java/dev/cel/runtime/standard/NotStrictlyFalseFunction.java index fc5ea1c2f..f0804f71e 100644 --- a/runtime/src/main/java/dev/cel/runtime/standard/NotStrictlyFalseFunction.java +++ b/runtime/src/main/java/dev/cel/runtime/standard/NotStrictlyFalseFunction.java @@ -20,6 +20,7 @@ import dev.cel.common.CelOptions; import dev.cel.runtime.CelFunctionBinding; import dev.cel.runtime.InternalFunctionBinder; +import dev.cel.runtime.NullabilityProperties; import dev.cel.runtime.RuntimeEquality; /** @@ -48,7 +49,7 @@ public enum NotStrictlyFalseOverload implements CelStandardOverload { return true; }, - /* isStrict= */ false, /* isNullable= */ false)), + /* isStrict= */ false, NullabilityProperties.NOT_NULLABLE)), ; private final CelStandardOverload bindingCreator; diff --git a/runtime/src/main/java/dev/cel/runtime/standard/StartsWithFunction.java b/runtime/src/main/java/dev/cel/runtime/standard/StartsWithFunction.java index 457dd3cf1..87d8db126 100644 --- a/runtime/src/main/java/dev/cel/runtime/standard/StartsWithFunction.java +++ b/runtime/src/main/java/dev/cel/runtime/standard/StartsWithFunction.java @@ -17,6 +17,7 @@ import com.google.common.collect.ImmutableSet; import dev.cel.common.CelOptions; import dev.cel.runtime.CelFunctionBinding; +import dev.cel.runtime.NullabilityProperties; import dev.cel.runtime.RuntimeEquality; import java.util.Arrays; @@ -42,7 +43,8 @@ public enum StartsWithOverload implements CelStandardOverload { STARTS_WITH_STRING( (celOptions, runtimeEquality) -> CelFunctionBinding.from( - "starts_with_string", String.class, String.class, String::startsWith)); + "starts_with_string", String.class, String.class, String::startsWith, + NullabilityProperties.NULLABLE_FALSE)); private final CelStandardOverload standardOverload; diff --git a/runtime/src/test/java/dev/cel/runtime/CelResolvedOverloadTest.java b/runtime/src/test/java/dev/cel/runtime/CelResolvedOverloadTest.java index d99a2fc5a..efc416674 100644 --- a/runtime/src/test/java/dev/cel/runtime/CelResolvedOverloadTest.java +++ b/runtime/src/test/java/dev/cel/runtime/CelResolvedOverloadTest.java @@ -35,7 +35,7 @@ CelResolvedOverload getIncrementIntOverload() { return arg + 1; }, /* isStrict= */ true, - /* isNullable= */ false, + NullabilityProperties.NOT_NULLABLE, Long.class); } @@ -52,7 +52,7 @@ public void canHandle_nullMessageType_returnsFalse() { /* overloadId= */ "identity_overload", (CelFunctionOverload) (args) -> args[0], /* isStrict= */ true, - /* isNullable= */ false, + NullabilityProperties.NOT_NULLABLE, TestAllTypes.class); assertThat(overload.canHandle(new Object[] {null})).isFalse(); } @@ -65,7 +65,7 @@ public void canHandle_nullPrimitive_returnsFalse() { /* overloadId= */ "identity_overload", (CelFunctionOverload) (args) -> args[0], /* isStrict= */ true, - /* isNullable= */ false, + NullabilityProperties.NOT_NULLABLE, Long.class); assertThat(overload.canHandle(new Object[] {null})).isFalse(); } @@ -91,7 +91,7 @@ public void canHandle_nonStrictOverload_returnsTrue() { return false; }, /* isStrict= */ false, - /* isNullable= */ false, + NullabilityProperties.NOT_NULLABLE, Long.class, Long.class); assertThat( @@ -111,7 +111,7 @@ public void canHandle_nonStrictOverload_returnsFalse() { return false; }, /* isStrict= */ false, - /* isNullable= */ false, + NullabilityProperties.NOT_NULLABLE, Long.class, Long.class); assertThat(nonStrictOverload.canHandle(new Object[] {new RuntimeException(), "Foo"})).isFalse(); diff --git a/runtime/src/test/java/dev/cel/runtime/DefaultDispatcherTest.java b/runtime/src/test/java/dev/cel/runtime/DefaultDispatcherTest.java index 8f56ad605..eefbc9f4f 100644 --- a/runtime/src/test/java/dev/cel/runtime/DefaultDispatcherTest.java +++ b/runtime/src/test/java/dev/cel/runtime/DefaultDispatcherTest.java @@ -41,7 +41,7 @@ public void setup() { /* overloadId= */ "overload_1", args -> (Long) args[0] + 1, /* isStrict= */ true, - /* isNullable= */ false, + NullabilityProperties.NOT_NULLABLE, Long.class)); overloads.put( "overload_2", @@ -50,7 +50,7 @@ public void setup() { /* overloadId= */ "overload_2", args -> (Long) args[0] + 2, /* isStrict= */ true, - /* isNullable= */ false, + NullabilityProperties.NOT_NULLABLE, Long.class)); } diff --git a/runtime/src/test/java/dev/cel/runtime/DefaultInterpreterTest.java b/runtime/src/test/java/dev/cel/runtime/DefaultInterpreterTest.java index b935330a4..3484f5d34 100644 --- a/runtime/src/test/java/dev/cel/runtime/DefaultInterpreterTest.java +++ b/runtime/src/test/java/dev/cel/runtime/DefaultInterpreterTest.java @@ -82,7 +82,7 @@ public Object adapt(String messageName, Object message) { /* overloadId= */ "error_overload", ImmutableList.>of(long.class), /* isStrict= */ true, - /* isNullable= */ false, + NullabilityProperties.NOT_NULLABLE, (args) -> new IllegalArgumentException("Always throws")); CelFunctionBinding notStrictlyFalseBinding = NotStrictlyFalseOverload.NOT_STRICTLY_FALSE.newFunctionBinding( @@ -97,7 +97,7 @@ public Object adapt(String messageName, Object message) { notStrictlyFalseBinding.getOverloadId(), notStrictlyFalseBinding.getArgTypes(), notStrictlyFalseBinding.isStrict(), - notStrictlyFalseBinding.isNullable(), + notStrictlyFalseBinding.getNullabilityProperties(), notStrictlyFalseBinding.getDefinition()); DefaultInterpreter defaultInterpreter = new DefaultInterpreter( diff --git a/runtime/src/test/java/dev/cel/runtime/planner/ProgramPlannerTest.java b/runtime/src/test/java/dev/cel/runtime/planner/ProgramPlannerTest.java index fbe2c9c44..247f34c6f 100644 --- a/runtime/src/test/java/dev/cel/runtime/planner/ProgramPlannerTest.java +++ b/runtime/src/test/java/dev/cel/runtime/planner/ProgramPlannerTest.java @@ -251,7 +251,7 @@ private static void addBindingsToDispatcher( overload.getOverloadId(), overload.getArgTypes(), overload.isStrict(), - overload.isNullable(), + overload.getNullabilityProperties(), overload.getDefinition())); } From 227cd3784f4e866bd73961604e2e0276e525ac42 Mon Sep 17 00:00:00 2001 From: Radovan Semancik Date: Tue, 7 Jul 2026 13:59:41 +0200 Subject: [PATCH 05/13] build.sh --- build.sh | 3 +++ 1 file changed, 3 insertions(+) create mode 100755 build.sh diff --git a/build.sh b/build.sh new file mode 100755 index 000000000..6d471e54e --- /dev/null +++ b/build.sh @@ -0,0 +1,3 @@ +#!/bin/bash + +bazel build ... --java_language_version=8 --java_runtime_version=8 --build_tag_filters=-conformance_maven From c98295721a0b150f2d94ff3f66e414d01715d1a0 Mon Sep 17 00:00:00 2001 From: Radovan Semancik Date: Wed, 8 Jul 2026 14:15:05 +0200 Subject: [PATCH 06/13] Extending the "nullability" implementation --- .../src/main/java/dev/cel/runtime/BUILD.bazel | 2 ++ .../dev/cel/runtime/CelFunctionBinding.java | 2 +- .../dev/cel/runtime/DefaultInterpreter.java | 18 +++++++++++++++++- .../dev/cel/runtime/NullabilityProperties.java | 6 ++++++ 4 files changed, 26 insertions(+), 2 deletions(-) diff --git a/runtime/src/main/java/dev/cel/runtime/BUILD.bazel b/runtime/src/main/java/dev/cel/runtime/BUILD.bazel index 9a9f1b5b7..7fe6b8739 100644 --- a/runtime/src/main/java/dev/cel/runtime/BUILD.bazel +++ b/runtime/src/main/java/dev/cel/runtime/BUILD.bazel @@ -320,6 +320,7 @@ java_library( "//common/types", "//common/types:type_providers", "//common/values:cel_byte_string", + "//common/src/main/java/dev/cel/common/values", "@maven//:com_google_code_findbugs_annotations", "@maven//:com_google_errorprone_error_prone_annotations", "@maven//:com_google_guava_guava", @@ -358,6 +359,7 @@ cel_android_library( "//common/types:type_providers_android", "//common/types:types_android", "//common/values:cel_byte_string", + "//common/src/main/java/dev/cel/common/values", "@maven//:com_google_code_findbugs_annotations", "@maven//:com_google_errorprone_error_prone_annotations", "@maven//:com_google_guava_guava", diff --git a/runtime/src/main/java/dev/cel/runtime/CelFunctionBinding.java b/runtime/src/main/java/dev/cel/runtime/CelFunctionBinding.java index c54f9ef11..a7bd4cb1f 100644 --- a/runtime/src/main/java/dev/cel/runtime/CelFunctionBinding.java +++ b/runtime/src/main/java/dev/cel/runtime/CelFunctionBinding.java @@ -136,7 +136,7 @@ static NullabilityProperties determineNullability(Iterable> argTypes) { return NullabilityProperties.NOT_NULLABLE; } } - return NullabilityProperties.NULLABLE_NULL; + return NullabilityProperties.NULLABLE; } static NullabilityProperties mergeNullabilityProperties(Collection nullabilityProperties) { diff --git a/runtime/src/main/java/dev/cel/runtime/DefaultInterpreter.java b/runtime/src/main/java/dev/cel/runtime/DefaultInterpreter.java index fdab71c3d..bfb839fd9 100644 --- a/runtime/src/main/java/dev/cel/runtime/DefaultInterpreter.java +++ b/runtime/src/main/java/dev/cel/runtime/DefaultInterpreter.java @@ -504,7 +504,23 @@ private IntermediateResult dispatchCall( CelResolvedOverload overload = findOverloadOrThrow(frame, expr, callExpr.function(), overloadIds, argArray); try { - Object dispatchResult = overload.getDefinition().apply(argArray); + Object dispatchResult; + if (celOptions.enableEasyNull()) { + Object[] nullableArgArray = Arrays.stream(argArray).map( + arg -> arg instanceof dev.cel.common.values.NullValue ? null : arg + ).toArray(); + if (argArray.length > 0 && nullableArgArray[0] == null + && overload.getNullabilityProperties() != null && overload.getNullabilityProperties().getDefaultFunction() != null) { + dispatchResult = overload.getNullabilityProperties().getDefaultFunction().apply(nullableArgArray); + } else { + dispatchResult = overload.getDefinition().apply(nullableArgArray); + } + if (dispatchResult == null) { + dispatchResult = dev.cel.common.values.NullValue.NULL_VALUE; + } + } else { + dispatchResult = overload.getDefinition().apply(argArray); + } // CustomFunctions themselves can return a CelUnknownSet directly. dispatchResult = InterpreterUtil.maybeAdaptToAccumulatedUnknowns(dispatchResult); if (celOptions.unwrapWellKnownTypesOnFunctionDispatch()) { diff --git a/runtime/src/main/java/dev/cel/runtime/NullabilityProperties.java b/runtime/src/main/java/dev/cel/runtime/NullabilityProperties.java index c1b2590b2..f23665030 100644 --- a/runtime/src/main/java/dev/cel/runtime/NullabilityProperties.java +++ b/runtime/src/main/java/dev/cel/runtime/NullabilityProperties.java @@ -14,6 +14,7 @@ package dev.cel.runtime; +import com.google.common.collect.ImmutableList; import com.google.errorprone.annotations.Immutable; import dev.cel.common.values.NullValue; @@ -24,8 +25,13 @@ public class NullabilityProperties { public static final NullabilityProperties NOT_NULLABLE = new NullabilityProperties(false, null); + public static final NullabilityProperties NULLABLE = new NullabilityProperties(true, null); public static final NullabilityProperties NULLABLE_NULL = new NullabilityProperties(true, args -> NullValue.NULL_VALUE); public static final NullabilityProperties NULLABLE_FALSE = new NullabilityProperties(true, args -> false); + public static final NullabilityProperties NULLABLE_TRUE = new NullabilityProperties(true, args -> true); + public static final NullabilityProperties NULLABLE_ZERO = new NullabilityProperties(true, args -> 0); + public static final NullabilityProperties NULLABLE_NEGATIVE_ONE = new NullabilityProperties(true, args -> -1); + public static final NullabilityProperties NULLABLE_EMPTY_LIST = new NullabilityProperties(true, args -> ImmutableList.of()); private final boolean isNullable; private final CelFunctionOverload defaultFunction; From 6047c0dec934dad32c19aff1227269dd6e98f82f Mon Sep 17 00:00:00 2001 From: Radovan Semancik Date: Thu, 9 Jul 2026 09:32:28 +0200 Subject: [PATCH 07/13] Nullable size() function --- .../java/dev/cel/runtime/standard/BUILD.bazel | 2 ++ .../cel/runtime/standard/SizeFunction.java | 31 +++++++++++++------ 2 files changed, 23 insertions(+), 10 deletions(-) diff --git a/runtime/src/main/java/dev/cel/runtime/standard/BUILD.bazel b/runtime/src/main/java/dev/cel/runtime/standard/BUILD.bazel index de2261267..25511aa37 100644 --- a/runtime/src/main/java/dev/cel/runtime/standard/BUILD.bazel +++ b/runtime/src/main/java/dev/cel/runtime/standard/BUILD.bazel @@ -1263,6 +1263,7 @@ java_library( "//runtime:function_binding", "//runtime:runtime_equality", "//runtime/standard:standard_function", + "//runtime/src/main/java/dev/cel/runtime:function_overload", "@maven//:com_google_guava_guava", "@maven//:com_google_protobuf_protobuf_java", ], @@ -1280,6 +1281,7 @@ cel_android_library( "//common/values:cel_byte_string", "//runtime:function_binding_android", "//runtime:runtime_equality_android", + "//runtime/src/main/java/dev/cel/runtime:function_overload", "@maven_android//:com_google_guava_guava", "@maven_android//:com_google_protobuf_protobuf_javalite", ], diff --git a/runtime/src/main/java/dev/cel/runtime/standard/SizeFunction.java b/runtime/src/main/java/dev/cel/runtime/standard/SizeFunction.java index a8f787665..906dda882 100644 --- a/runtime/src/main/java/dev/cel/runtime/standard/SizeFunction.java +++ b/runtime/src/main/java/dev/cel/runtime/standard/SizeFunction.java @@ -19,6 +19,7 @@ import dev.cel.common.CelOptions; import dev.cel.common.values.CelByteString; import dev.cel.runtime.CelFunctionBinding; +import dev.cel.runtime.NullabilityProperties; import dev.cel.runtime.RuntimeEquality; import java.util.Arrays; import java.util.List; @@ -47,42 +48,52 @@ public enum SizeOverload implements CelStandardOverload { (celOptions, runtimeEquality) -> { if (celOptions.evaluateCanonicalTypesToNativeValues()) { return CelFunctionBinding.from( - "size_bytes", CelByteString.class, (CelByteString bytes) -> (long) bytes.size()); + "size_bytes", CelByteString.class, (CelByteString bytes) -> (long) bytes.size(), + NullabilityProperties.NULLABLE_ZERO); } else { return CelFunctionBinding.from( - "size_bytes", ByteString.class, (ByteString bytes) -> (long) bytes.size()); + "size_bytes", ByteString.class, (ByteString bytes) -> (long) bytes.size(), + NullabilityProperties.NULLABLE_ZERO); } }), BYTES_SIZE( (celOptions, runtimeEquality) -> { if (celOptions.evaluateCanonicalTypesToNativeValues()) { return CelFunctionBinding.from( - "bytes_size", CelByteString.class, (CelByteString bytes) -> (long) bytes.size()); + "bytes_size", CelByteString.class, (CelByteString bytes) -> (long) bytes.size(), + NullabilityProperties.NULLABLE_ZERO); } else { return CelFunctionBinding.from( - "bytes_size", ByteString.class, (ByteString bytes) -> (long) bytes.size()); + "bytes_size", ByteString.class, (ByteString bytes) -> (long) bytes.size(), + NullabilityProperties.NULLABLE_ZERO); } }), SIZE_LIST( (celOptions, runtimeEquality) -> - CelFunctionBinding.from("size_list", List.class, (List list1) -> (long) list1.size())), + CelFunctionBinding.from("size_list", List.class, (List list1) -> (long) list1.size(), + NullabilityProperties.NULLABLE_ZERO)), LIST_SIZE( (celOptions, runtimeEquality) -> - CelFunctionBinding.from("list_size", List.class, (List list1) -> (long) list1.size())), + CelFunctionBinding.from("list_size", List.class, (List list1) -> (long) list1.size(), + NullabilityProperties.NULLABLE_ZERO)), SIZE_STRING( (celOptions, runtimeEquality) -> CelFunctionBinding.from( - "size_string", String.class, (String s) -> (long) s.codePointCount(0, s.length()))), + "size_string", String.class, (String s) -> (long) s.codePointCount(0, s.length()), + NullabilityProperties.NULLABLE_ZERO)), STRING_SIZE( (celOptions, runtimeEquality) -> CelFunctionBinding.from( - "string_size", String.class, (String s) -> (long) s.codePointCount(0, s.length()))), + "string_size", String.class, (String s) -> (long) s.codePointCount(0, s.length()), + NullabilityProperties.NULLABLE_ZERO)), SIZE_MAP( (celOptions, runtimeEquality) -> - CelFunctionBinding.from("size_map", Map.class, (Map map1) -> (long) map1.size())), + CelFunctionBinding.from("size_map", Map.class, (Map map1) -> (long) map1.size(), + NullabilityProperties.NULLABLE_ZERO)), MAP_SIZE( (celOptions, runtimeEquality) -> - CelFunctionBinding.from("map_size", Map.class, (Map map1) -> (long) map1.size())); + CelFunctionBinding.from("map_size", Map.class, (Map map1) -> (long) map1.size(), + NullabilityProperties.NULLABLE_ZERO)); private final CelStandardOverload standardOverload; From 1d224b45bad5720bdc0e12c1c81ef420c0fdb338 Mon Sep 17 00:00:00 2001 From: Radovan Semancik Date: Thu, 9 Jul 2026 16:41:08 +0200 Subject: [PATCH 08/13] Pluggable RuntimeEquality --- .../cel/checker/CelStandardDeclarations.java | 4 ++-- .../dev/cel/runtime/CelRuntimeBuilder.java | 2 ++ .../java/dev/cel/runtime/CelRuntimeImpl.java | 17 +++++++++++++- .../dev/cel/runtime/CelRuntimeLegacyImpl.java | 23 ++++++++++++++++++- .../java/dev/cel/runtime/RuntimeEquality.java | 4 ++++ 5 files changed, 46 insertions(+), 4 deletions(-) diff --git a/checker/src/main/java/dev/cel/checker/CelStandardDeclarations.java b/checker/src/main/java/dev/cel/checker/CelStandardDeclarations.java index 53615604f..15f394771 100644 --- a/checker/src/main/java/dev/cel/checker/CelStandardDeclarations.java +++ b/checker/src/main/java/dev/cel/checker/CelStandardDeclarations.java @@ -381,10 +381,10 @@ public CelOverloadDecl celOverloadDecl() { public enum Relation implements StandardOverload { EQUALS( CelOverloadDecl.newGlobalOverload( - "equals", "equality", SimpleType.BOOL, TYPE_PARAM_A, TYPE_PARAM_A)), + "equals", "equality", SimpleType.BOOL, SimpleType.ANY, SimpleType.ANY)), NOT_EQUALS( CelOverloadDecl.newGlobalOverload( - "not_equals", "inequality", SimpleType.BOOL, TYPE_PARAM_A, TYPE_PARAM_A)), + "not_equals", "inequality", SimpleType.BOOL, SimpleType.ANY, SimpleType.ANY)), ; private final CelOverloadDecl celOverloadDecl; diff --git a/runtime/src/main/java/dev/cel/runtime/CelRuntimeBuilder.java b/runtime/src/main/java/dev/cel/runtime/CelRuntimeBuilder.java index 87f11fde2..25ce604c5 100644 --- a/runtime/src/main/java/dev/cel/runtime/CelRuntimeBuilder.java +++ b/runtime/src/main/java/dev/cel/runtime/CelRuntimeBuilder.java @@ -196,6 +196,8 @@ public interface CelRuntimeBuilder { @CanIgnoreReturnValue CelRuntimeBuilder setExtensionRegistry(ExtensionRegistry extensionRegistry); + @CanIgnoreReturnValue + CelRuntimeBuilder setRuntimeEquality(RuntimeEquality runtimeEquality); /** * Set the {@link CelContainer} to use as the namespace for resolving CEL expression variables and diff --git a/runtime/src/main/java/dev/cel/runtime/CelRuntimeImpl.java b/runtime/src/main/java/dev/cel/runtime/CelRuntimeImpl.java index adafbac3b..c67eff34c 100644 --- a/runtime/src/main/java/dev/cel/runtime/CelRuntimeImpl.java +++ b/runtime/src/main/java/dev/cel/runtime/CelRuntimeImpl.java @@ -95,6 +95,9 @@ public abstract class CelRuntimeImpl implements CelRuntime { @AutoValue.CopyAnnotations abstract @Nullable ExtensionRegistry extensionRegistry(); + @AutoValue.CopyAnnotations + abstract @Nullable RuntimeEquality runtimeEquality(); + @Override public Program createProgram(CelAbstractSyntaxTree ast) throws CelEvaluationException { return toRuntimeProgram(planner().plan(ast)); @@ -271,6 +274,9 @@ public abstract static class Builder implements CelRuntimeBuilder { @Override public abstract Builder setExtensionRegistry(ExtensionRegistry extensionRegistry); + @Override + public abstract Builder setRuntimeEquality(RuntimeEquality runtimeEquality); + @Override public abstract Builder setTypeProvider(CelTypeProvider celTypeProvider); @@ -292,6 +298,8 @@ public abstract static class Builder implements CelRuntimeBuilder { abstract ExtensionRegistry extensionRegistry(); + abstract RuntimeEquality runtimeEquality(); + abstract ImmutableMap functionBindings(); abstract ImmutableSet.Builder fileDescriptorsBuilder(); @@ -489,7 +497,14 @@ public CelRuntime build() { DynamicProto dynamicProto = DynamicProto.create(defaultMessageFactory); CelValueProvider protoMessageValueProvider = ProtoMessageValueProvider.newInstance(options(), dynamicProto); - RuntimeEquality runtimeEquality = ProtoMessageRuntimeEquality.create(dynamicProto, options()); + + RuntimeEquality runtimeEquality; + if (runtimeEquality() == null) { + runtimeEquality = ProtoMessageRuntimeEquality.create(dynamicProto, options()); + } else { + runtimeEquality = runtimeEquality(); + } + ImmutableSet runtimeLibraries = runtimeLibrariesBuilder().build(); // Add libraries, such as extensions for (CelRuntimeLibrary celLibrary : runtimeLibraries) { diff --git a/runtime/src/main/java/dev/cel/runtime/CelRuntimeLegacyImpl.java b/runtime/src/main/java/dev/cel/runtime/CelRuntimeLegacyImpl.java index e42f5801f..fbb10d2d6 100644 --- a/runtime/src/main/java/dev/cel/runtime/CelRuntimeLegacyImpl.java +++ b/runtime/src/main/java/dev/cel/runtime/CelRuntimeLegacyImpl.java @@ -72,6 +72,8 @@ public final class CelRuntimeLegacyImpl implements CelRuntime { // CEL-Internal-4 private final ExtensionRegistry extensionRegistry; + private final RuntimeEquality runtimeEquality; + // A user-provided custom type factory should presumably be thread-safe. This is documented, but // not enforced. // CEL-Internal-4 @@ -101,6 +103,7 @@ public CelRuntimeBuilder toRuntimeBuilder() { // CEL-Internal-2 .setStandardEnvironmentEnabled(standardEnvironmentEnabled) .setExtensionRegistry(extensionRegistry) + .setRuntimeEquality(runtimeEquality) .addFileTypes(fileDescriptors) .addLibraries(celRuntimeLibraries) .addFunctionBindings(celFunctionBindings); @@ -143,6 +146,8 @@ public static final class Builder implements CelRuntimeBuilder { private ExtensionRegistry extensionRegistry; + private RuntimeEquality runtimeEquality; + private boolean standardEnvironmentEnabled; @Override @@ -251,6 +256,13 @@ public CelRuntimeBuilder setExtensionRegistry(ExtensionRegistry extensionRegistr return this; } + @Override + public CelRuntimeBuilder setRuntimeEquality(RuntimeEquality runtimeEquality) { + checkNotNull(runtimeEquality); + this.runtimeEquality = runtimeEquality; + return this; + } + @Override public CelRuntimeBuilder setContainer(CelContainer container) { throw new UnsupportedOperationException( @@ -290,7 +302,13 @@ public CelRuntimeLegacyImpl build() { runtimeTypeFactory, DefaultMessageFactory.create(celDescriptorPool)); DynamicProto dynamicProto = DynamicProto.create(runtimeTypeFactory); - RuntimeEquality runtimeEquality = ProtoMessageRuntimeEquality.create(dynamicProto, options); + RuntimeEquality runtimeEquality; + if (this.runtimeEquality == null) { + runtimeEquality = ProtoMessageRuntimeEquality.create(dynamicProto, options); + } else { + runtimeEquality = this.runtimeEquality; + } +// RuntimeEquality runtimeEquality = ProtoMessageRuntimeEquality.create(dynamicProto, options); ImmutableSet runtimeLibraries = celRuntimeLibraries.build(); // Add libraries, such as extensions @@ -364,6 +382,7 @@ public CelRuntimeLegacyImpl build() { options, standardEnvironmentEnabled, extensionRegistry, + runtimeEquality, customTypeFactory, overriddenStandardFunctions, celValueProvider, @@ -452,6 +471,7 @@ private CelRuntimeLegacyImpl( CelOptions options, boolean standardEnvironmentEnabled, ExtensionRegistry extensionRegistry, + RuntimeEquality runtimeEquality, @Nullable Function customTypeFactory, @Nullable CelStandardFunctions overriddenStandardFunctions, @Nullable CelValueProvider celValueProvider, @@ -462,6 +482,7 @@ private CelRuntimeLegacyImpl( this.options = options; this.standardEnvironmentEnabled = standardEnvironmentEnabled; this.extensionRegistry = extensionRegistry; + this.runtimeEquality = runtimeEquality; this.customTypeFactory = customTypeFactory; this.overriddenStandardFunctions = overriddenStandardFunctions; this.celValueProvider = celValueProvider; diff --git a/runtime/src/main/java/dev/cel/runtime/RuntimeEquality.java b/runtime/src/main/java/dev/cel/runtime/RuntimeEquality.java index 56a8761cd..ad033297d 100644 --- a/runtime/src/main/java/dev/cel/runtime/RuntimeEquality.java +++ b/runtime/src/main/java/dev/cel/runtime/RuntimeEquality.java @@ -280,4 +280,8 @@ private static Optional unsignedToLongLossless(UnsignedLong v) { this.runtimeHelpers = runtimeHelpers; this.celOptions = celOptions; } + + protected RuntimeEquality(CelOptions celOptions) { + this(RuntimeHelpers.create(), celOptions); + } } From 9ee08074293de36b318fc8523204c3f7c0227faa Mon Sep 17 00:00:00 2001 From: Radovan Semancik Date: Fri, 10 Jul 2026 17:10:15 +0200 Subject: [PATCH 09/13] hasField() nullability fix --- .../main/java/dev/cel/runtime/DescriptorMessageProvider.java | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/runtime/src/main/java/dev/cel/runtime/DescriptorMessageProvider.java b/runtime/src/main/java/dev/cel/runtime/DescriptorMessageProvider.java index 2993ab2b9..c20275c83 100644 --- a/runtime/src/main/java/dev/cel/runtime/DescriptorMessageProvider.java +++ b/runtime/src/main/java/dev/cel/runtime/DescriptorMessageProvider.java @@ -165,6 +165,10 @@ public Object hasField(Object message, String fieldName) { message = optionalMessage.get(); } + if (celOptions.enableEasyNull() && message instanceof dev.cel.common.values.NullValue) { + return false; + } + if (message instanceof Map) { Map map = (Map) message; return map.containsKey(fieldName); From ef808d683e8dad38b456fe1783a082e48683d005 Mon Sep 17 00:00:00 2001 From: Radovan Semancik Date: Fri, 10 Jul 2026 17:18:21 +0200 Subject: [PATCH 10/13] Null-safe string add operation --- .../dev/cel/runtime/standard/AddOperator.java | 17 +++++++++++++++-- 1 file changed, 15 insertions(+), 2 deletions(-) diff --git a/runtime/src/main/java/dev/cel/runtime/standard/AddOperator.java b/runtime/src/main/java/dev/cel/runtime/standard/AddOperator.java index c4e0bec42..1140af1d3 100644 --- a/runtime/src/main/java/dev/cel/runtime/standard/AddOperator.java +++ b/runtime/src/main/java/dev/cel/runtime/standard/AddOperator.java @@ -134,7 +134,7 @@ public enum AddOverload implements CelStandardOverload { ADD_STRING( (celOptions, runtimeEquality) -> CelFunctionBinding.from( - "add_string", String.class, String.class, (String x, String y) -> x + y)), + "add_string", String.class, String.class, AddOperator::addString)), ADD_DURATION_TIMESTAMP( (celOptions, runtimeEquality) -> { if (celOptions.evaluateCanonicalTypesToNativeValues()) { @@ -169,7 +169,20 @@ public CelFunctionBinding newFunctionBinding( } } - private AddOperator(ImmutableSet overloads) { + private AddOperator(ImmutableSet overloads) { super(ADD.getFunction(), overloads); } + + private static Object addString(String x, String y) { + if (x == null && y == null) { + return null; + } + if (x == null) { + return y; + } + if (y == null) { + return x; + } + return x + y; + } } From 8a3135bc979210cf9fd99571c694fc82887a8cf3 Mon Sep 17 00:00:00 2001 From: Radovan Semancik Date: Fri, 17 Jul 2026 14:20:08 +0200 Subject: [PATCH 11/13] Null-safe comprehensions --- runtime/src/main/java/dev/cel/runtime/DefaultInterpreter.java | 2 ++ 1 file changed, 2 insertions(+) diff --git a/runtime/src/main/java/dev/cel/runtime/DefaultInterpreter.java b/runtime/src/main/java/dev/cel/runtime/DefaultInterpreter.java index bfb839fd9..7b0a72a3e 100644 --- a/runtime/src/main/java/dev/cel/runtime/DefaultInterpreter.java +++ b/runtime/src/main/java/dev/cel/runtime/DefaultInterpreter.java @@ -1019,6 +1019,8 @@ private IntermediateResult evalComprehension( iterRange = (List) iterRangeRaw.value(); } else if (iterRangeRaw.value() instanceof Map) { iterRange = ((Map) iterRangeRaw.value()).keySet(); + } else if (iterRangeRaw.value() instanceof dev.cel.common.values.NullValue) { + iterRange = ImmutableList.of(); } else { throw CelEvaluationExceptionBuilder.newBuilder( "expected a list or a map for iteration range but got '%s'", From 1cb93aa8593a9a140d19a2e87aa55d78482f11e0 Mon Sep 17 00:00:00 2001 From: Radovan Semancik Date: Mon, 20 Jul 2026 12:12:50 +0200 Subject: [PATCH 12/13] README, etc. --- CODE_OF_CONDUCT.md | 93 ----------- CONTRIBUTING.md | 32 +--- README.md | 403 ++++----------------------------------------- 3 files changed, 35 insertions(+), 493 deletions(-) delete mode 100644 CODE_OF_CONDUCT.md diff --git a/CODE_OF_CONDUCT.md b/CODE_OF_CONDUCT.md deleted file mode 100644 index 7fb48ef20..000000000 --- a/CODE_OF_CONDUCT.md +++ /dev/null @@ -1,93 +0,0 @@ -# Code of Conduct - -## Our Pledge - -In the interest of fostering an open and welcoming environment, we as -contributors and maintainers pledge to making participation in our project and -our community a harassment-free experience for everyone, regardless of age, body -size, disability, ethnicity, gender identity and expression, level of -experience, education, socio-economic status, nationality, personal appearance, -race, religion, or sexual identity and orientation. - -## Our Standards - -Examples of behavior that contributes to creating a positive environment -include: - -* Using welcoming and inclusive language -* Being respectful of differing viewpoints and experiences -* Gracefully accepting constructive criticism -* Focusing on what is best for the community -* Showing empathy towards other community members - -Examples of unacceptable behavior by participants include: - -* The use of sexualized language or imagery and unwelcome sexual attention or - advances -* Trolling, insulting/derogatory comments, and personal or political attacks -* Public or private harassment -* Publishing others' private information, such as a physical or electronic - address, without explicit permission -* Other conduct which could reasonably be considered inappropriate in a - professional setting - -## Our Responsibilities - -Project maintainers are responsible for clarifying the standards of acceptable -behavior and are expected to take appropriate and fair corrective action in -response to any instances of unacceptable behavior. - -Project maintainers have the right and responsibility to remove, edit, or reject -comments, commits, code, wiki edits, issues, and other contributions that are -not aligned to this Code of Conduct, or to ban temporarily or permanently any -contributor for other behaviors that they deem inappropriate, threatening, -offensive, or harmful. - -## Scope - -This Code of Conduct applies both within project spaces and in public spaces -when an individual is representing the project or its community. Examples of -representing a project or community include using an official project e-mail -address, posting via an official social media account, or acting as an appointed -representative at an online or offline event. Representation of a project may be -further defined and clarified by project maintainers. - -This Code of Conduct also applies outside the project spaces when the Project -Steward has a reasonable belief that an individual's behavior may have a -negative impact on the project or its community. - -## Conflict Resolution - -We do not believe that all conflict is bad; healthy debate and disagreement -often yield positive results. However, it is never okay to be disrespectful or -to engage in behavior that violates the project’s code of conduct. - -If you see someone violating the code of conduct, you are encouraged to address -the behavior directly with those involved. Many issues can be resolved quickly -and easily, and this gives people more control over the outcome of their -dispute. If you are unable to resolve the matter for any reason, or if the -behavior is threatening or harassing, report it. We are dedicated to providing -an environment where participants feel welcome and safe. - -Reports should be directed to *cel-conduct@google.com*, the Project Steward(s) -for *Common Expression Language (CEL)*. It is the Project Steward’s duty to -receive and address reported violations of the code of conduct. They will then -work with a committee consisting of representatives from the Open Source -Programs Office and the Google Open Source Strategy team. If for any reason you -are uncomfortable reaching out to the Project Steward, please email -opensource@google.com. - -We will investigate every complaint, but you may not receive a direct response. -We will use our discretion in determining when and how to follow up on reported -incidents, which may range from not taking action to permanent expulsion from -the project and project-sponsored spaces. We will notify the accused of the -report and provide them an opportunity to discuss it before any action is taken. -The identity of the reporter will be omitted from the details of the report -supplied to the accused. In potentially harmful situations, such as ongoing -harassment or threats to anyone's safety, we may take action without notice. - -## Attribution - -This Code of Conduct is adapted from the Contributor Covenant, version 1.4, -available at -https://www.contributor-covenant.org/version/1/4/code-of-conduct.html \ No newline at end of file diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 47634bc4c..7a5964ff1 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -1,33 +1,3 @@ # How to Contribute -We'd love to accept your patches and contributions to this project. - -## Before you begin - -### Sign our Contributor License Agreement - -Contributions to this project must be accompanied by a -[Contributor License Agreement](https://cla.developers.google.com/about) (CLA). -You (or your employer) retain the copyright to your contribution; this simply -gives us permission to use and redistribute your contributions as part of the -project. - -If you or your current employer have already signed the Google CLA (even if it -was for a different project), you probably don't need to do it again. - -Visit to see your current agreements or to -sign a new one. - -### Review our Community Guidelines - -This project follows [Google's Open Source Community -Guidelines](https://opensource.google/conduct/). - -## Contribution process - -### Code Reviews - -All submissions, including submissions by project members, require review. We -use GitHub pull requests for this purpose. Consult -[GitHub Help](https://help.github.com/articles/about-pull-requests/) for more -information on using pull requests. \ No newline at end of file +Contact Evolveum at info@evolveum.com. diff --git a/README.md b/README.md index e2424a85c..0c8154c41 100644 --- a/README.md +++ b/README.md @@ -1,389 +1,54 @@ -# Common Expression Language for Java +# Evolveum Fork of cel-java -The Common Expression Language (CEL) is a non-Turing complete language designed -for simplicity, speed, safety, and portability. CEL's C-like [syntax][1] looks -nearly identical to equivalent expressions in C++, Go, Java, and TypeScript. +This project contains fork of [cel-java project](https://github.com/cel-expr/cel-java/), a Java implementation of Common Expression Language. -```java -// Check whether a resource name starts with a group name. -resource.name.startsWith("/groups/"+auth.claims.group) -``` - -```go -// Determine whether the request is in the permitted time window. -request.time - resource.age < duration("24h") -``` - -```typescript -// Check whether all resource names in a list match a given filter. -auth.claims.email_verified && resources.all(r, r.startsWith(auth.claims.email)) -``` - -A CEL "program" is a single expression. The examples have been tagged as -`java`, `go`, and `typescript` within the markdown to showcase the commonality -of the syntax. - -CEL is ideal for lightweight expression evaluation when a fully sandboxed -scripting language is too resource intensive. - ---- - -* [Quick Start](#quick-start) -* [Overview](#overview) - * [Environment Setup](#environment-setup) - * [Parsing](#parsing) - * [Checking](#checking) - * [Macros](#macros) - * [Evaluation](#evaluation) - * [Errors](#errors) - * [Extensions](#extensions) -* [Install](#install) -* [Common Questions](#common-questions) -* [License](#license) - ---- - -## Quick Start - -### Install - -CEL-Java is available in Maven Central Repository. [Download the JARs here][8] or add the following to your build dependencies: - -**Maven (pom.xml)**: - -```xml - - dev.cel - cel - 0.13.1 - -``` - -**Gradle** - -```gradle -implementation 'dev.cel:cel:0.13.1' -``` - -Then run this example: - -```java -import dev.cel.common.CelAbstractSyntaxTree; -import dev.cel.common.CelValidationException; -import dev.cel.common.types.SimpleType; -import dev.cel.compiler.CelCompiler; -import dev.cel.compiler.CelCompilerFactory; -import dev.cel.runtime.CelEvaluationException; -import dev.cel.runtime.CelRuntime; -import dev.cel.runtime.CelRuntimeFactory; -import java.util.Map; +## How Is This Fork Different? -public class HelloWorld { - // Construct the compilation and runtime environments. - // These instances are immutable and thus trivially thread-safe and amenable to caching. - private static final CelCompiler CEL_COMPILER = - CelCompilerFactory.standardCelCompilerBuilder().addVar("my_var", SimpleType.STRING).build(); - private static final CelRuntime CEL_RUNTIME = - CelRuntimeFactory.standardCelRuntimeBuilder().build(); +This fork brings **nullability** to the CEL world. +The code of the CEL engine is modified to work seamlessly with null values in CEL expressions. +E.g., following CEL expression fails miserably in original CEL implementation, in case there are null values in the "focus" data structure. - public void run() throws CelValidationException, CelEvaluationException { - // Compile the expression into an Abstract Syntax Tree. - CelAbstractSyntaxTree ast = CEL_COMPILER.compile("my_var + '!'").getAst(); - - // Plan an executable program instance. - CelRuntime.Program program = CEL_RUNTIME.createProgram(ast); - - // Evaluate the program with an input variable. - String result = (String) program.eval(Map.of("my_var", "Hello World")); - System.out.println(result); // 'Hello World!' - } -} +```cel +focus.activation.administrativeStatus == 'enabled' ``` -## Overview - -Determine the variables and functions you want to provide to CEL. Parse and -check an expression to make sure it's valid. Then evaluate the output AST -against some input. Checking is optional, but strongly encouraged. - -### Environment Setup - -Configuration for the entire CEL stack can be done all at once via the -`CelFactory.standardCelBuilder()`, or can be composed into compilation and -evaluation via the `CelCompilerFactory` and `CelRuntimeFactory`. - -The simplest form of CEL usage is as follows: - -```java -Cel cel = CelFactory.standardCelBuilder().build(); -``` - -More commonly, your application will want to configure type-checking separately -from the runtime. Use `CelCompilerFactory` to construct a compilation -environment and declare the types, macros, variables, and functions to use with -your CEL application: - -```java -// Example environment for the following expression: -// resource.name.startsWith('/groups/' + group) -CelCompiler cel = CelCompilerFactory.standardCelCompilerBuilder() - .setStandardMacros(CelStandardMacro.HAS) - .setContainer("google.rpc.context.AttributeContext") - .addMessageTypes(AttributeContext.getDescriptor()) - .addVar("resource", - StructTypeReference.create("google.rpc.context.AttributeContext.Resource")) - .addVar("group", SimpleType.STRING) - .build(); -``` - -More information about the features which are supported on the builder may be -found in the [`CelCompilerBuilder`][9]. - -### Parsing - -Some CEL use cases only require parsing of an expression in order to be useful. -For example, one example might want to check whether the expression contains any -nested comprehensions, or possibly to pass the parsed expression to a C++ or Go -binary for evaluation. Presently, Java does not support parse-only evaluation. - -```java -CelValidationResult parseResult = - cel.parse("resource.name.startsWith('/groups/' + group)"); -try { - return parseResult.getAst(); -} catch (CelValidationException e) { - // Handle exception... -} -``` - -### Checking - -Type-checking is performed on `CelAbstractSyntaxTree` values to ensure that the -expression is well formed and all variable and function references are defined. - -Type-checking can be performed immediately after parsing an expression: - -```java -try { - CelValidationResult parseResult = - cel.parse("resource.name.startsWith('/groups/' + group)"); - CelValidationResult checkResult = cel.check(parseResult.getAst()); - return checkResult.getAst(); -} catch (CelValidationException e) { - // Handle exception... -} -``` - -Or, the parse and type-check can be combined into the `compile` call. This is -likely the more common need. - -```java -CelValidationResult compileResult = - cel.compile("resource.name.startsWith('/groups/' + group)"); -try { - return compileResult.getAst(); -} catch (CelValidationException e) { - // Handle exception... -} -``` - -#### Macros - -Macros were introduced to support optional CEL features that might not be -desired in all use cases without the syntactic burden and complexity such -features might desire if they were part of the core CEL syntax. Macros are -expanded at parse time and their expansions are type-checked at check time. - -For example, when macros are enabled it is possible to support bounded iteration -/ fold operators. The macros `all`, `exists`, `exists_one`, `filter`, and `map` -are particularly useful for evaluating a single predicate against list and map -values. - -```javascript -// Ensure all tweets are less than 140 chars -tweets.all(t, t.size() <= 140) -``` - -The `has` macro is useful for unifying field presence testing logic across -protobuf types and dynamic (JSON-like) types. - -```javascript -// Test whether the field is a non-default value if proto-based, or defined -// in the JSON case. -has(message.field) -``` - -Both cases traditionally require special syntax at the language level, but these -features are exposed via macros in CEL. - -Refer to the [CEL Specification][10] for full listings of available macros. To -leverage them, simply set the desired macros via `setStandardMacros` on the -builder: - -```java -CelCompiler.standardCelBuilder() - .setStandardMacros(CelStandardMacro.STANDARD_MACROS) -``` - -### Evaluation - -Expressions can be evaluated using once they are type-checked/compiled by -creating a `CelRuntime.Program` from a `CelAbstractSyntaxTree`: - -```java -CelRuntime celRuntime = CelRuntimeFactory.standardCelRuntimeBuilder().build(); -try { - CelRuntime.Program program = celRuntime.createProgram(compileResult.getAst()); - return program.eval( - ImmutableMap.of( - "resource", Resource.newBuilder().setName("/groups/").build(), - "group", "admin" - )); -} catch (CelEvaluationException e) { - // Handle evaluation exceptions ... -} -``` - -The evaluation is thread-safe and side effect free thus many different inputs can -be sent to the same `cel.Program`. - -#### Partial State - -In distributed apps it is not uncommon to have edge caches and central services. -If possible, evaluation should happen at the edge, but it isn't always possible -to know the full state required for all values and functions present in the CEL -expression. - -To improve the odds of successful evaluation with partial state, CEL uses -commutative logical operators `&&`, `||`. If an error or unknown value (not the -same thing) is encountered on the left-hand side, the right-hand side is -evaluated also to determine the outcome. While it is possible to implement -evaluation with partial state without this feature, this method was chosen -because it aligns with the semantics of SQL evaluation and because it's more -robust to evaluation against dynamic data types such as JSON inputs. - -In the following truth-table, the symbols `` and `` represent error or -unknown values, with the `?` indicating that the branch is not taken due to -short-circuiting. When the result is `` this means that both the args are -possibly relevant to the result. - -| Expression | Result | -|---------------------|----------| -| `false && ?` | `false` | -| `true && false` | `false` | -| ` && false` | `false` | -| `true && true` | `true` | -| `true && ` | `` | -| ` && true` | `` | -| ` && ` | `` | -| `true \|\| ?` | `true` | -| `false \|\| true` | `true` | -| ` \|\| true` | `true` | -| `false \|\| false` | `false` | -| `false \|\| ` | `` | -| ` \|\| false` | `` | -| ` \|\| ` | `` | - -### Errors - -Parse and check errors have friendly error messages with pointers to where the -issues occur in source: - -```sh -ERROR: :1:40: undefined field 'undefined' - | TestAllTypes{single_int32: 1, undefined: 2} - | .......................................^`, -``` - -Both the parsed and checked expressions contain source position information -about each node that appears in the output AST. This information can be used -to determine error locations at evaluation time as well. - -### Extensions - -CEL-Java offers a suite of [canonical extensions][11] to support commonly -needed features that falls outside the CEL specification. - -Examples: - -```java -// String manipulation -'hello hello'.replace('he', 'we') // returns 'wello wello' -'hello hello hello'.split(' ') // returns ['hello', 'hello', 'hello'] - -// Math extensions -math.greatest(-42.0, -21.5, -100.0) // -21.5 -math.least(-42.0, -21.5, -100.0) // -100.0 - -// Proto extensions -proto.getExt(msg, google.expr.proto2.test.int32_ext) // returns int value - -// Local bindings -cel.bind(a, 'hello', - cel.bind(b, 'world', a + b + b + a)) // "helloworldworldhello" -``` - -## Common Questions - -### Why not JavaScript, Lua, or WASM? - -JavaScript and Lua are rich languages that require sandboxing to execute safely. -Sandboxing is costly and factors into the "what will I let users evaluate?" -question heavily when the answer is anything more than O(n) -complexity. +This fork brings null-tolerant implementation of CEL. +The expression above works well when interpreted by the code in this fork, with any combination of null values. +Expression authors do not need to care about null values most of the time, and they can focus on the business aspects of the expression, as they are supposed to. -CEL evaluates linearly with respect to the size of the expression and the input -being evaluated when macros are disabled. The only functions beyond the -built-ins that may be invoked are provided by the host environment. While -extension functions may be more complex, this is a choice by the application -embedding CEL. +## Purpose, Security and Quality -But, why not WASM? WASM is an excellent choice for certain applications and is -far superior to embedded JavaScript and Lua, but it does not have support for -garbage collection and non-primitive object types require semi-expensive calls -across modules. In most cases CEL will be faster and just as portable for its -intended use case, though for node.js and web-based execution CEL too may offer -a WASM evaluator with direct to WASM compilation. +This code is maintained solely for the use in [midPoint](https://github.com/Evolveum/midpoint). +Tests in this fork are not maintained, as we were not able to make the test work. +Honestly, we do not think it is work the effort to fight a serious battle with Bazel just to be able to run the tests. +The code in this fork in tested indirectly, using tests in midPoint. +However, these tests are testing only the scenarios that midPoint is using. -### Do I need to Parse _and_ Check? +Therefore, there are **no guarantees** that this code will work without midPoint, and absolutely no guarantees regarding security of the code when used outside midPoint. -Checking is an optional, but strongly suggested, step in CEL expression -validation. It is sufficient in some cases to simply Parse and rely on the -runtime bindings and error handling to do the right thing. +Use at your own risk. -### Where can I learn more about the language? +## Why Fork? -* See the [CEL Spec][1] for the specification and conformance test suite. -* Ask for support on the [CEL Java Discuss][2] Google group. +Why are we forking and not contributing back to the original project instead? -### How can I contribute? +Simply speaking, mostly due to the Google CLA. +We are not willing to sign it, which is showstopper for any contribution. -* See [CONTRIBUTING.md](./CONTRIBUTING.md) to get started. -* Use [GitHub Issues][7] to request features or report bugs. +However, there seems to be also deeper incompatibilities of this fork with the approach of original team, which makes the contribution unlikely to be accepted even if we did sign the CLA. -### Dependencies +Overall, CEL seems to suffer from serious nullophobia (irrational fear of null values). +This fork is fixing that. -Java 8 or newer is required. +To be fair, CEL introduced "optionals" to make working with null values possible. +However, using optionals is not easy, especially for people who are not expert programmers. +This would not work for us. +Null-tolerant language is much better way that optionals for our use cases. -| Library | Details | -|-----------------------|----------------------| -| [Guava][3] | N/A | -| [RE2/J][4] | N/A | -| [Protocol Buffers][5] | Full or lite runtime | -| [ANTLR4][6] | Java runtime | +Also, we have to make a serious butchery of the basel-based build configuration, to be able to build the project at all, without dependency on Google infrastructure. -## License +## Build -Released under the [Apache License](LICENSE). +Use `build.sh` to build the project (Bazel is required). -[1]: https://github.com/cel-expr/cel-spec -[2]: https://groups.google.com/forum/#!forum/cel-java-discuss -[3]: https://github.com/google/guava -[4]: https://github.com/google/re2j -[5]: https://github.com/protocolbuffers/protobuf/tree/master/java -[6]: https://github.com/antlr/antlr4/tree/master/runtime/Java -[7]: https://github.com/cel-expr/cel-java/issues -[8]: https://search.maven.org/search?q=g:dev.cel -[9]: https://github.com/cel-expr/cel-java/blob/main/compiler/src/main/java/dev/cel/compiler/CelCompilerBuilder.java -[10]: https://github.com/cel-expr/cel-spec/blob/master/doc/langdef.md#macros -[11]: https://github.com/cel-expr/cel-java/blob/main/extensions/src/main/java/dev/cel/extensions/README.md +Use `publish/publish.sh` to publish Maven artifacts. From 6377e4c2f9997d8af6ed412a334a196d55d81067 Mon Sep 17 00:00:00 2001 From: Radovan Semancik Date: Mon, 20 Jul 2026 16:55:43 +0200 Subject: [PATCH 13/13] Better field selection error message. --- .../CelAttributeNotFoundException.java | 17 +++++++++++++---- .../cel/runtime/DescriptorMessageProvider.java | 5 +++++ 2 files changed, 18 insertions(+), 4 deletions(-) diff --git a/common/src/main/java/dev/cel/common/exceptions/CelAttributeNotFoundException.java b/common/src/main/java/dev/cel/common/exceptions/CelAttributeNotFoundException.java index d805c9cf4..eee283251 100644 --- a/common/src/main/java/dev/cel/common/exceptions/CelAttributeNotFoundException.java +++ b/common/src/main/java/dev/cel/common/exceptions/CelAttributeNotFoundException.java @@ -36,7 +36,17 @@ public static CelAttributeNotFoundException forFieldResolution(String... fields) } public static CelAttributeNotFoundException forFieldResolution(Collection fields) { - return new CelAttributeNotFoundException(formatErrorMessage(fields)); + return new CelAttributeNotFoundException(formatErrorMessage(fields, + "Error resolving field%s '%s'. Field selections must be performed on messages or maps.")); + } + + public static CelAttributeNotFoundException forFieldResolutionList(String... fields) { + return forFieldResolutionList(Arrays.asList(fields)); + } + + public static CelAttributeNotFoundException forFieldResolutionList(Collection fields) { + return new CelAttributeNotFoundException(formatErrorMessage(fields, + "Error resolving field%s '%s'. Attempted field selections on multi-valued item.")); } public static CelAttributeNotFoundException forMissingAttributes(Collection attributes) { @@ -44,15 +54,14 @@ public static CelAttributeNotFoundException forMissingAttributes(Collection fields) { + private static String formatErrorMessage(Collection fields, String format) { String maybePlural = ""; if (fields.size() > 1) { maybePlural = "s"; } return String.format( - "Error resolving field%s '%s'. Field selections must be performed on messages or maps.", - maybePlural, String.join(", ", fields)); + format, maybePlural, String.join(", ", fields)); } private CelAttributeNotFoundException(String message) { diff --git a/runtime/src/main/java/dev/cel/runtime/DescriptorMessageProvider.java b/runtime/src/main/java/dev/cel/runtime/DescriptorMessageProvider.java index c20275c83..b97386559 100644 --- a/runtime/src/main/java/dev/cel/runtime/DescriptorMessageProvider.java +++ b/runtime/src/main/java/dev/cel/runtime/DescriptorMessageProvider.java @@ -29,6 +29,8 @@ import dev.cel.common.internal.ProtoMessageFactory; import dev.cel.common.types.CelTypes; import dev.cel.common.values.CelByteString; + +import java.util.List; import java.util.Map; import java.util.Optional; import org.jspecify.annotations.Nullable; @@ -209,6 +211,9 @@ private FieldDescriptor findField(Descriptor descriptor, String fieldName) { private static MessageOrBuilder assertFullProtoMessage(Object candidate, String fieldName) { if (!(candidate instanceof MessageOrBuilder)) { + if (candidate instanceof List) { + throw CelAttributeNotFoundException.forFieldResolutionList(fieldName); + } // This can happen when the field selection is done on dyn, and it is not a message. throw CelAttributeNotFoundException.forFieldResolution(fieldName); }