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 d572bb2bc..5ccc498fd 100644 --- a/common/src/main/java/dev/cel/common/values/BUILD.bazel +++ b/common/src/main/java/dev/cel/common/values/BUILD.bazel @@ -60,7 +60,6 @@ java_library( deps = [ "//common/values", "@maven//:com_google_errorprone_error_prone_annotations", - "@maven//:com_google_guava_guava", ], ) @@ -72,7 +71,6 @@ cel_android_library( deps = [ "//common/values:values_android", "@maven//:com_google_errorprone_error_prone_annotations", - "@maven_android//:com_google_guava_guava", ], ) @@ -118,7 +116,6 @@ java_library( deps = [ ":values", "//common/annotations", - "@maven//:com_google_errorprone_error_prone_annotations", "@maven//:com_google_guava_guava", "@maven//:org_jspecify_jspecify", ], @@ -134,12 +131,31 @@ cel_android_library( deps = [ ":values_android", "//common/annotations", - "@maven//:com_google_errorprone_error_prone_annotations", "@maven//:org_jspecify_jspecify", "@maven_android//:com_google_guava_guava", ], ) +java_library( + name = "preadapted_list", + srcs = [ + "CelPreAdaptedList.java", + ], + tags = [ + ], + deps = ["//common/annotations"], +) + +cel_android_library( + name = "preadapted_list_android", + srcs = [ + "CelPreAdaptedList.java", + ], + tags = [ + ], + deps = ["//common/annotations"], +) + java_library( name = "values", srcs = CEL_VALUES_SOURCES, @@ -148,6 +164,7 @@ java_library( deps = [ ":cel_byte_string", ":cel_value", + ":preadapted_list", "//:auto_value", "//common/annotations", "//common/types", @@ -198,6 +215,7 @@ cel_android_library( deps = [ ":cel_byte_string", ":cel_value_android", + ":preadapted_list_android", "//:auto_value", "//common/annotations", "//common/types:type_providers_android", @@ -226,7 +244,6 @@ java_library( ], deps = [ ":cel_byte_string", - ":values", "//common/annotations", "//common/internal:proto_time_utils", "//common/internal:well_known_proto", @@ -261,6 +278,7 @@ java_library( ], deps = [ ":base_proto_cel_value_converter", + ":preadapted_list", ":values", "//:auto_value", "//common:options", @@ -273,7 +291,6 @@ java_library( "@maven//:com_google_errorprone_error_prone_annotations", "@maven//:com_google_guava_guava", "@maven//:com_google_protobuf_protobuf_java", - "@maven//:org_jspecify_jspecify", ], ) @@ -316,8 +333,6 @@ java_library( "//protobuf:cel_lite_descriptor", "@maven//:com_google_errorprone_error_prone_annotations", "@maven//:com_google_guava_guava", - "@maven//:com_google_protobuf_protobuf_java", - "@maven//:org_jspecify_jspecify", "@maven_android//:com_google_protobuf_protobuf_javalite", ], ) @@ -343,7 +358,6 @@ cel_android_library( "//protobuf:cel_lite_descriptor", "@maven//:com_google_errorprone_error_prone_annotations", "@maven//:com_google_guava_guava", - "@maven//:org_jspecify_jspecify", "@maven_android//:com_google_guava_guava", "@maven_android//:com_google_protobuf_protobuf_javalite", ], diff --git a/common/src/main/java/dev/cel/common/values/CelPreAdaptedList.java b/common/src/main/java/dev/cel/common/values/CelPreAdaptedList.java new file mode 100644 index 000000000..c0ff25e45 --- /dev/null +++ b/common/src/main/java/dev/cel/common/values/CelPreAdaptedList.java @@ -0,0 +1,49 @@ +// Copyright 2026 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package dev.cel.common.values; + +import dev.cel.common.annotations.Internal; +import java.util.AbstractList; +import java.util.List; +import java.util.RandomAccess; + +/** + * A zero-allocation view over a list we know is already adapted. + * + *

This class purely exists as an optimization scheme to avoid redundant collection traversals in + * {@link CelValueConverter}, and is not intended for general use. + */ +@Internal +final class CelPreAdaptedList extends AbstractList implements RandomAccess { + private final List delegate; + + private CelPreAdaptedList(List delegate) { + this.delegate = delegate; + } + + static CelPreAdaptedList wrap(List safeList) { + return new CelPreAdaptedList<>(safeList); + } + + @Override + public E get(int index) { + return delegate.get(index); + } + + @Override + public int size() { + return delegate.size(); + } +} diff --git a/common/src/main/java/dev/cel/common/values/CelValueConverter.java b/common/src/main/java/dev/cel/common/values/CelValueConverter.java index 89f5ab100..20deef1d3 100644 --- a/common/src/main/java/dev/cel/common/values/CelValueConverter.java +++ b/common/src/main/java/dev/cel/common/values/CelValueConverter.java @@ -20,8 +20,11 @@ import com.google.errorprone.annotations.Immutable; import dev.cel.common.annotations.Internal; import java.util.Collection; +import java.util.Iterator; +import java.util.List; import java.util.Map; import java.util.Optional; +import java.util.RandomAccess; import java.util.function.Function; /** @@ -53,16 +56,11 @@ public static CelValueConverter getDefaultInstance() { *

The value may be a {@link CelValue}, a {@link Collection} or a {@link Map}. */ public Object maybeUnwrap(Object value) { - if (value instanceof CelValue) { - return unwrap((CelValue) value); + if (value instanceof CelValue || value instanceof CelPreAdaptedList) { + return value instanceof CelValue ? unwrap((CelValue) value) : value; } - Object mapped = mapContainer(value, maybeUnwrapFunction); - if (mapped != value) { - return mapped; - } - - return value; + return mapContainer(value, maybeUnwrapFunction); } /** @@ -70,6 +68,34 @@ public Object maybeUnwrap(Object value) { * Returns the original value if it's not a supported container. */ protected Object mapContainer(Object value, Function mapper) { + + // Zero allocation path for standard lists that support O(1) indexing + // Generally, protobuf lists (backed by arrays) fall into this category + if (value instanceof List && value instanceof RandomAccess) { + List list = (List) value; + for (int i = 0; i < list.size(); i++) { + Object element = list.get(i); + Object mapped = mapper.apply(element); + + if (mapped != element) { + ImmutableList.Builder builder = + ImmutableList.builderWithExpectedSize(list.size()); + for (int j = 0; j < i; j++) { + builder.add(list.get(j)); + } + builder.add(mapped); + for (int j = i + 1; j < list.size(); j++) { + builder.add(mapper.apply(list.get(j))); + } + return builder.build(); + } + } + + // Zero allocations if unmodified + return value; + } + + // Fallback for lists that are unordered if (value instanceof Collection) { Collection collection = (Collection) value; ImmutableList.Builder builder = @@ -82,12 +108,32 @@ protected Object mapContainer(Object value, Function mapper) { if (value instanceof Map) { Map map = (Map) value; - ImmutableMap.Builder builder = - ImmutableMap.builderWithExpectedSize(map.size()); - for (Map.Entry entry : map.entrySet()) { - builder.put(mapper.apply(entry.getKey()), mapper.apply(entry.getValue())); + Iterator> iterator = map.entrySet().iterator(); + + while (iterator.hasNext()) { + Map.Entry entry = iterator.next(); + Object mappedKey = mapper.apply(entry.getKey()); + Object mappedValue = mapper.apply(entry.getValue()); + + if (mappedKey != entry.getKey() || mappedValue != entry.getValue()) { + ImmutableMap.Builder builder = + ImmutableMap.builderWithExpectedSize(map.size()); + + for (Map.Entry prevEntry : map.entrySet()) { + if (prevEntry.getKey() == entry.getKey()) { + break; + } + builder.put(mapper.apply(prevEntry.getKey()), mapper.apply(prevEntry.getValue())); + } + builder.put(mappedKey, mappedValue); + while (iterator.hasNext()) { + Map.Entry nextEntry = iterator.next(); + builder.put(mapper.apply(nextEntry.getKey()), mapper.apply(nextEntry.getValue())); + } + return builder.buildOrThrow(); + } } - return builder.buildOrThrow(); + return value; } return value; @@ -96,7 +142,7 @@ protected Object mapContainer(Object value, Function mapper) { public Object toRuntimeValue(Object value) { Preconditions.checkNotNull(value); - if (value instanceof CelValue) { + if (value instanceof CelValue || value instanceof CelPreAdaptedList) { return value; } diff --git a/common/src/main/java/dev/cel/common/values/ProtoCelValueConverter.java b/common/src/main/java/dev/cel/common/values/ProtoCelValueConverter.java index 565c65438..948df759c 100644 --- a/common/src/main/java/dev/cel/common/values/ProtoCelValueConverter.java +++ b/common/src/main/java/dev/cel/common/values/ProtoCelValueConverter.java @@ -167,6 +167,18 @@ public Object fromProtoMessageFieldToCelValue(Message message, FieldDescriptor f break; } + if (fieldDescriptor.isRepeated()) { + switch (fieldDescriptor.getType()) { + case INT64: + case BOOL: + case STRING: + case DOUBLE: + return CelPreAdaptedList.wrap((List) result); + default: + break; + } + } + return toRuntimeValue(result); }