of("id", overloadSelector.id()));
}
}
+
+ private final class RepresentFeatureFlag implements Represent {
+
+ @Override
+ public Node representData(Object data) {
+ CelEnvironment.FeatureFlag featureFlag = (CelEnvironment.FeatureFlag) data;
+ return represent(
+ ImmutableMap.builder()
+ .put("name", featureFlag.name())
+ .put("enabled", featureFlag.enabled())
+ .buildOrThrow());
+ }
+ }
+
+ private final class RepresentLimit implements Represent {
+
+ @Override
+ public Node representData(Object data) {
+ CelEnvironment.Limit limit = (CelEnvironment.Limit) data;
+ return represent(
+ ImmutableMap.builder()
+ .put("name", limit.name())
+ .put("value", limit.value() < 0 ? -1 : limit.value())
+ .buildOrThrow());
+ }
+ }
}
diff --git a/bundle/src/main/java/dev/cel/bundle/CelFactory.java b/bundle/src/main/java/dev/cel/bundle/CelFactory.java
index 6cc6d8192..79acccc93 100644
--- a/bundle/src/main/java/dev/cel/bundle/CelFactory.java
+++ b/bundle/src/main/java/dev/cel/bundle/CelFactory.java
@@ -14,12 +14,14 @@
package dev.cel.bundle;
+import com.google.errorprone.annotations.InlineMe;
import dev.cel.checker.CelCheckerLegacyImpl;
import dev.cel.common.CelOptions;
import dev.cel.compiler.CelCompiler;
import dev.cel.compiler.CelCompilerImpl;
import dev.cel.parser.CelParserImpl;
import dev.cel.runtime.CelRuntime;
+import dev.cel.runtime.CelRuntimeImpl;
import dev.cel.runtime.CelRuntimeLegacyImpl;
/** Helper class to configure the entire CEL stack in a common interface. */
@@ -33,8 +35,23 @@ private CelFactory() {}
*
* Note, the {@link CelOptions#current}, standard CEL function libraries, and linked message
* evaluation are enabled by default.
+ *
+ *
Note: This standard builder currently proxies the legacy builder, which will be deprecated.
+ * Callers are strongly encouraged to migrate to the planner ({@link #plannerCelBuilder()}).
*/
+ @InlineMe(replacement = "CelFactory.legacyCelBuilder()", imports = "dev.cel.bundle.CelFactory")
public static CelBuilder standardCelBuilder() {
+ return legacyCelBuilder();
+ }
+
+ /**
+ * Creates a builder for configuring a legacy CEL using current parser for the parse, type-check,
+ * and eval of expressions.
+ *
+ *
Note: This legacy builder will be deprecated. Callers are strongly encouraged to migrate to
+ * the planner ({@link #plannerCelBuilder()}).
+ */
+ public static CelBuilder legacyCelBuilder() {
return CelImpl.newBuilder(
CelCompilerImpl.newBuilder(
CelParserImpl.newBuilder(), CelCheckerLegacyImpl.newBuilder()),
@@ -44,6 +61,30 @@ public static CelBuilder standardCelBuilder() {
.setStandardEnvironmentEnabled(true);
}
+ /**
+ * Creates a builder for configuring CEL for the parsing, optional type-checking, and evaluation
+ * of expressions using the Program Planner.
+ *
+ *
The {@code ProgramPlanner} architecture provides key benefits over the {@link
+ * #standardCelBuilder()}:
+ *
+ *
+ * Performance: Programs can be cached for improving evaluation speed.
+ * Parsed-only expression evaluation: Unlike the traditional stack which required
+ * supplying type-checked expressions, this architecture handles both parsed-only and
+ * type-checked expressions.
+ *
+ */
+ public static CelBuilder plannerCelBuilder() {
+ return CelImpl.newBuilder(
+ CelCompilerImpl.newBuilder(
+ CelParserImpl.newBuilder(),
+ CelCheckerLegacyImpl.newBuilder().setStandardEnvironmentEnabled(true)),
+ CelRuntimeImpl.newBuilder())
+ // CEL-Internal-2
+ .setOptions(CelOptions.current().enableHeterogeneousNumericComparisons(true).build());
+ }
+
/** Combines a prebuilt {@link CelCompiler} and {@link CelRuntime} into {@link Cel}. */
public static Cel combine(CelCompiler celCompiler, CelRuntime celRuntime) {
return CelImpl.combine(celCompiler, celRuntime);
diff --git a/bundle/src/main/java/dev/cel/bundle/CelImpl.java b/bundle/src/main/java/dev/cel/bundle/CelImpl.java
index bc92cca7a..b8c7c36e9 100644
--- a/bundle/src/main/java/dev/cel/bundle/CelImpl.java
+++ b/bundle/src/main/java/dev/cel/bundle/CelImpl.java
@@ -54,6 +54,7 @@
import dev.cel.runtime.CelEvaluationException;
import dev.cel.runtime.CelRuntime;
import dev.cel.runtime.CelRuntimeBuilder;
+import dev.cel.runtime.CelRuntimeImpl;
import dev.cel.runtime.CelRuntimeLibrary;
import dev.cel.runtime.CelStandardFunctions;
import java.util.Arrays;
@@ -142,6 +143,8 @@ static CelImpl combine(CelCompiler compiler, CelRuntime runtime) {
* Create a new builder for constructing a {@code CelImpl} instance.
*
* By default, {@link CelOptions#DEFAULT} are enabled, as is the CEL standard environment.
+ *
+ *
CEL Library Internals. Do Not Use. Consumers should use {@code CelFactory} instead.
*/
static CelBuilder newBuilder(
CelCompilerBuilder compilerBuilder, CelRuntimeBuilder celRuntimeBuilder) {
@@ -199,6 +202,10 @@ public CelContainer container() {
@Override
public CelBuilder setContainer(CelContainer container) {
compilerBuilder.setContainer(container);
+ if (runtimeBuilder instanceof CelRuntimeImpl.Builder) {
+ runtimeBuilder.setContainer(container);
+ }
+
return this;
}
@@ -274,6 +281,18 @@ public CelBuilder addFunctionBindings(Iterable lateBoundFunctionNames) {
+ runtimeBuilder.addLateBoundFunctions(lateBoundFunctionNames);
+ return this;
+ }
+
@Override
public CelBuilder setResultType(CelType resultType) {
checkNotNull(resultType);
@@ -298,6 +317,11 @@ public CelBuilder setValueProvider(CelValueProvider celValueProvider) {
return this;
}
+ @Override
+ public CelValueProvider valueProvider() {
+ return runtimeBuilder.valueProvider();
+ }
+
@Override
@Deprecated
public Builder setTypeProvider(TypeProvider typeProvider) {
@@ -308,6 +332,9 @@ public Builder setTypeProvider(TypeProvider typeProvider) {
@Override
public CelBuilder setTypeProvider(CelTypeProvider celTypeProvider) {
compilerBuilder.setTypeProvider(celTypeProvider);
+ if (runtimeBuilder instanceof CelRuntimeImpl.Builder) {
+ runtimeBuilder.setTypeProvider(celTypeProvider);
+ }
return this;
}
@@ -352,6 +379,7 @@ public CelBuilder addFileTypes(FileDescriptorSet fileDescriptorSet) {
}
@Override
+ @Deprecated
public CelBuilder setStandardEnvironmentEnabled(boolean value) {
compilerBuilder.setStandardEnvironmentEnabled(value);
runtimeBuilder.setStandardEnvironmentEnabled(value);
diff --git a/bundle/src/main/java/dev/cel/bundle/TypeSpecifierParser.java b/bundle/src/main/java/dev/cel/bundle/TypeSpecifierParser.java
new file mode 100644
index 000000000..ca96a955f
--- /dev/null
+++ b/bundle/src/main/java/dev/cel/bundle/TypeSpecifierParser.java
@@ -0,0 +1,200 @@
+// 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.bundle;
+
+import static com.google.common.base.Preconditions.checkNotNull;
+
+import com.google.common.collect.ImmutableList;
+import dev.cel.bundle.CelEnvironment.TypeDecl;
+import dev.cel.common.formats.ParserContext;
+
+/**
+ * Parses a type specifier shorthand string (e.g. {@code "map"}, {@code "list<~T>"},
+ * {@code "int"}) into a {@link TypeDecl}.
+ */
+final class TypeSpecifierParser {
+ private static final int MAX_RECURSION_DEPTH = 64;
+ static final TypeDecl ERROR_TYPE_DECL = TypeDecl.create("*error*");
+
+ private final String text;
+ private final int length;
+ private int pos;
+
+ static TypeDecl parse(String text) {
+ checkNotNull(text);
+ TypeSpecifierParser parser = new TypeSpecifierParser(text);
+ return parser.parse();
+ }
+
+ static TypeDecl parse(ParserContext> ctx, long nodeId, String text) {
+ checkNotNull(ctx);
+ checkNotNull(text);
+ try {
+ return parse(text);
+ } catch (IllegalArgumentException e) {
+ ctx.reportError(nodeId, e.getMessage());
+ return ERROR_TYPE_DECL;
+ }
+ }
+
+ private TypeDecl parse() {
+ TypeDecl res = parseTypeElem(0);
+ skipWhitespace();
+ if (pos < length) {
+ throw new IllegalArgumentException(
+ String.format(
+ "unexpected character '%c' at position %d in %s",
+ text.charAt(pos), pos, formatQuoted(text)));
+ }
+ return res;
+ }
+
+ private TypeSpecifierParser(String text) {
+ this.text = text;
+ this.length = text.length();
+ this.pos = 0;
+ }
+
+ private TypeDecl parseTypeElem(int depth) {
+ if (depth > MAX_RECURSION_DEPTH) {
+ throw new IllegalArgumentException(
+ String.format("exceeded maximum type specifier recursion depth at position %d", pos));
+ }
+ skipWhitespace();
+ if (pos < length && text.charAt(pos) == '~') {
+ pos++; // consume '~'
+ String id = parseTypeParamIdent();
+ return TypeDecl.ofTypeParam(id);
+ }
+ return parseConcreteType(depth);
+ }
+
+ private TypeDecl parseConcreteType(int depth) {
+ String id = parseNamespaceIdentifier();
+ skipWhitespace();
+ if (pos < length && text.charAt(pos) == '<') {
+ pos++; // consume '<'
+ ImmutableList.Builder params = ImmutableList.builder();
+ while (true) {
+ TypeDecl param = parseTypeElem(depth + 1);
+ params.add(param);
+ skipWhitespace();
+ if (pos < length && text.charAt(pos) == ',') {
+ pos++; // consume ','
+ continue;
+ }
+ if (pos < length && text.charAt(pos) == '>') {
+ pos++; // consume '>'
+ break;
+ }
+ throw new IllegalArgumentException(
+ String.format("expected ',' or '>' at position %d", pos));
+ }
+ return TypeDecl.newBuilder().setName(id).addParams(params.build()).build();
+ }
+ return TypeDecl.create(id);
+ }
+
+ private String parseNamespaceIdentifier() {
+ StringBuilder id = new StringBuilder();
+ while (pos < length && text.charAt(pos) != '<') {
+ char c = text.charAt(pos);
+ if (c == '.') {
+ id.append('.');
+ pos++; // consume '.'
+ }
+ String ident = parseIdentifier();
+ id.append(ident);
+ if (pos < length && text.charAt(pos) != '.') {
+ break;
+ }
+ }
+ String identifier = id.toString();
+ if (identifier.isEmpty()) {
+ throw new IllegalArgumentException(String.format("missing identifier at position %d", pos));
+ }
+ return identifier;
+ }
+
+ private String parseIdentifier() {
+ if (pos >= length) {
+ throw new IllegalArgumentException("unexpected end of input");
+ }
+ int start = pos;
+ while (pos < length) {
+ char c = text.charAt(pos);
+ boolean isValid = (pos == start) ? (isAlpha(c) || c == '_') : (isAlphaNumeric(c) || c == '_');
+ if (isValid) {
+ pos++;
+ continue;
+ }
+ if (pos == start) {
+ throw new IllegalArgumentException(
+ String.format("identifier is expected, but '%c' was found at position %d", c, pos));
+ }
+ break;
+ }
+ return text.substring(start, pos);
+ }
+
+ private String parseTypeParamIdent() {
+ if (pos >= length) {
+ throw new IllegalArgumentException("unexpected end of input");
+ }
+ char c = text.charAt(pos);
+ if (c < 'A' || c > 'Z') {
+ throw new IllegalArgumentException(
+ String.format(
+ "invalid type parameter identifier '%c' at position %d, must be a single character"
+ + " from A-Z",
+ c, pos));
+ }
+ pos++;
+ if (pos < length) {
+ char next = text.charAt(pos);
+ if (isAlphaNumeric(next) || next == '_') {
+ throw new IllegalArgumentException(
+ String.format(
+ "invalid type parameter identifier '%c' at position %d, must be a single character"
+ + " from A-Z",
+ next, pos));
+ }
+ }
+ return String.valueOf(c);
+ }
+
+ private void skipWhitespace() {
+ while (pos < length) {
+ char c = text.charAt(pos);
+ if (c == ' ' || c == '\t' || c == '\n' || c == '\r') {
+ pos++;
+ } else {
+ break;
+ }
+ }
+ }
+
+ private static boolean isAlpha(char c) {
+ return (c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z');
+ }
+
+ private static boolean isAlphaNumeric(char c) {
+ return isAlpha(c) || (c >= '0' && c <= '9');
+ }
+
+ private static String formatQuoted(String s) {
+ return "\"" + s + "\"";
+ }
+}
diff --git a/bundle/src/test/java/dev/cel/bundle/BUILD.bazel b/bundle/src/test/java/dev/cel/bundle/BUILD.bazel
index cd33dd67d..ddd2e7285 100644
--- a/bundle/src/test/java/dev/cel/bundle/BUILD.bazel
+++ b/bundle/src/test/java/dev/cel/bundle/BUILD.bazel
@@ -1,9 +1,11 @@
load("@rules_java//java:defs.bzl", "java_library")
load("//:testing.bzl", "junit4_test_suites")
-package(default_applicable_licenses = [
- "//:license",
-])
+package(
+ default_applicable_licenses = [
+ "//:license",
+ ],
+)
java_library(
name = "tests",
@@ -17,6 +19,7 @@ java_library(
deps = [
"//:java_truth",
"//bundle:cel",
+ "//bundle:cel_impl",
"//bundle:environment",
"//bundle:environment_exception",
"//bundle:environment_exporter",
@@ -24,6 +27,7 @@ java_library(
"//checker",
"//checker:checker_legacy_environment",
"//checker:proto_type_mask",
+ "//checker:standard_decl",
"//common:cel_ast",
"//common:cel_descriptor_util",
"//common:cel_source",
@@ -53,7 +57,10 @@ java_library(
"//runtime:evaluation_exception_builder",
"//runtime:evaluation_listener",
"//runtime:function_binding",
+ "//runtime:standard_functions",
"//runtime:unknown_attributes",
+ "//testing:cel_runtime_flavor",
+ "//testing/protos:single_file_extension_java_proto",
"//testing/protos:single_file_java_proto",
"@cel_spec//proto/cel/expr:checked_java_proto",
"@cel_spec//proto/cel/expr:syntax_java_proto",
diff --git a/bundle/src/test/java/dev/cel/bundle/CelEnvironmentExporterTest.java b/bundle/src/test/java/dev/cel/bundle/CelEnvironmentExporterTest.java
index d6608a9d4..7560a12aa 100644
--- a/bundle/src/test/java/dev/cel/bundle/CelEnvironmentExporterTest.java
+++ b/bundle/src/test/java/dev/cel/bundle/CelEnvironmentExporterTest.java
@@ -36,8 +36,10 @@
import dev.cel.common.CelOptions;
import dev.cel.common.CelOverloadDecl;
import dev.cel.common.CelVarDecl;
+import dev.cel.common.types.ListType;
import dev.cel.common.types.OpaqueType;
import dev.cel.common.types.SimpleType;
+import dev.cel.common.types.TypeParamType;
import dev.cel.extensions.CelExtensions;
import java.net.URL;
import java.util.HashSet;
@@ -176,6 +178,20 @@ public void customFunctions() {
"math.isFinite",
CelOverloadDecl.newGlobalOverload(
"math_isFinite_int64", SimpleType.BOOL, SimpleType.INT)),
+ CelFunctionDecl.newFunctionDeclaration(
+ "zipGeneric",
+ CelOverloadDecl.newGlobalOverload(
+ "zip_list_list",
+ ListType.create(ListType.create(TypeParamType.create("T"))),
+ ListType.create(TypeParamType.create("T")),
+ ListType.create(TypeParamType.create("T")))),
+ CelFunctionDecl.newFunctionDeclaration(
+ "zip",
+ CelOverloadDecl.newGlobalOverload(
+ "zip_list_int_list_int",
+ ListType.create(ListType.create(SimpleType.INT)),
+ ListType.create(SimpleType.INT),
+ ListType.create(SimpleType.INT))),
CelFunctionDecl.newFunctionDeclaration(
"addWeeks",
CelOverloadDecl.newMemberOverload(
@@ -207,6 +223,68 @@ public void customFunctions() {
.setTarget(TypeDecl.create("google.protobuf.Timestamp"))
.setArguments(ImmutableList.of(TypeDecl.create("int")))
.setReturnType(TypeDecl.create("bool"))
+ .build())),
+ FunctionDecl.create(
+ "zipGeneric",
+ ImmutableSet.of(
+ OverloadDecl.newBuilder()
+ .setId("zip_list_list")
+ .setArguments(
+ ImmutableList.of(
+ TypeDecl.newBuilder()
+ .setName("list")
+ .addParams(
+ TypeDecl.newBuilder()
+ .setName("T")
+ .setIsTypeParam(true)
+ .build())
+ .build(),
+ TypeDecl.newBuilder()
+ .setName("list")
+ .addParams(
+ TypeDecl.newBuilder()
+ .setName("T")
+ .setIsTypeParam(true)
+ .build())
+ .build()))
+ .setReturnType(
+ TypeDecl.newBuilder()
+ .setName("list")
+ .addParams(
+ TypeDecl.newBuilder()
+ .setName("list")
+ .addParams(
+ TypeDecl.newBuilder()
+ .setName("T")
+ .setIsTypeParam(true)
+ .build())
+ .build())
+ .build())
+ .build())),
+ FunctionDecl.create(
+ "zip",
+ ImmutableSet.of(
+ OverloadDecl.newBuilder()
+ .setId("zip_list_int_list_int")
+ .setArguments(
+ ImmutableList.of(
+ TypeDecl.newBuilder()
+ .setName("list")
+ .addParams(TypeDecl.create("int"))
+ .build(),
+ TypeDecl.newBuilder()
+ .setName("list")
+ .addParams(TypeDecl.create("int"))
+ .build()))
+ .setReturnType(
+ TypeDecl.newBuilder()
+ .setName("list")
+ .addParams(
+ TypeDecl.newBuilder()
+ .setName("list")
+ .addParams(TypeDecl.create("int"))
+ .build())
+ .build())
.build())));
// Random-check some standard functions: we don't want to see them explicitly defined.
@@ -255,10 +333,40 @@ public void container() {
CelEnvironmentExporter exporter = CelEnvironmentExporter.newBuilder().build();
CelEnvironment celEnvironment = exporter.export(cel);
- CelContainer container = celEnvironment.container();
+ CelContainer container = celEnvironment.container().get();
assertThat(container.name()).isEqualTo("cntnr");
assertThat(container.abbreviations()).containsExactly("foo.Bar", "baz.Qux").inOrder();
assertThat(container.aliases()).containsAtLeast("nm", "user.name", "id", "user.id").inOrder();
}
-}
+ @Test
+ public void options() {
+ Cel cel =
+ CelFactory.standardCelBuilder()
+ .setOptions(
+ CelOptions.current()
+ .maxExpressionCodePointSize(100)
+ .maxParseErrorRecoveryLimit(10)
+ .maxParseRecursionDepth(10)
+ .maxParseExpressionNodeCount(500)
+ .enableQuotedIdentifierSyntax(true)
+ .enableHeterogeneousNumericComparisons(true)
+ .populateMacroCalls(true)
+ .build())
+ .build();
+
+ CelEnvironmentExporter exporter = CelEnvironmentExporter.newBuilder().build();
+ CelEnvironment celEnvironment = exporter.export(cel);
+ assertThat(celEnvironment.features())
+ .containsExactly(
+ CelEnvironment.FeatureFlag.create("cel.feature.backtick_escape_syntax", true),
+ CelEnvironment.FeatureFlag.create("cel.feature.cross_type_numeric_comparisons", true),
+ CelEnvironment.FeatureFlag.create("cel.feature.macro_call_tracking", true));
+ assertThat(celEnvironment.limits())
+ .containsExactly(
+ CelEnvironment.Limit.create("cel.limit.expression_code_points", 100),
+ CelEnvironment.Limit.create("cel.limit.parse_error_recovery", 10),
+ CelEnvironment.Limit.create("cel.limit.parse_recursion_depth", 10),
+ CelEnvironment.Limit.create("cel.limit.expression_node_count", 500));
+ }
+}
diff --git a/bundle/src/test/java/dev/cel/bundle/CelEnvironmentTest.java b/bundle/src/test/java/dev/cel/bundle/CelEnvironmentTest.java
index 6bc84a48f..a48ea0ff8 100644
--- a/bundle/src/test/java/dev/cel/bundle/CelEnvironmentTest.java
+++ b/bundle/src/test/java/dev/cel/bundle/CelEnvironmentTest.java
@@ -28,6 +28,10 @@
import dev.cel.common.CelOptions;
import dev.cel.common.CelValidationException;
import dev.cel.common.CelValidationResult;
+import dev.cel.common.types.CelType;
+import dev.cel.common.types.CelTypeProvider;
+import dev.cel.common.types.SimpleType;
+import dev.cel.common.types.TypeType;
import dev.cel.compiler.CelCompiler;
import dev.cel.compiler.CelCompilerFactory;
import dev.cel.parser.CelStandardMacro;
@@ -44,9 +48,7 @@ public void newBuilder_defaults() {
assertThat(environment.source()).isEmpty();
assertThat(environment.name()).isEmpty();
assertThat(environment.description()).isEmpty();
- assertThat(environment.container().name()).isEmpty();
- assertThat(environment.container().abbreviations()).isEmpty();
- assertThat(environment.container().aliases()).isEmpty();
+ assertThat(environment.container()).isEmpty();
assertThat(environment.extensions()).isEmpty();
assertThat(environment.variables()).isEmpty();
assertThat(environment.functions()).isEmpty();
@@ -65,10 +67,10 @@ public void container() {
.build())
.build();
- assertThat(environment.container().name()).isEqualTo("cntr");
- assertThat(environment.container().abbreviations()).containsExactly("foo.Bar", "baz.Qux");
- assertThat(environment.container().aliases())
- .containsExactly("nm", "user.name", "id", "user.id");
+ CelContainer container = environment.container().get();
+ assertThat(container.name()).isEqualTo("cntr");
+ assertThat(container.abbreviations()).containsExactly("foo.Bar", "baz.Qux");
+ assertThat(container.aliases()).containsExactly("nm", "user.name", "id", "user.id");
}
@Test
@@ -81,9 +83,10 @@ public void extend_allExtensions() throws Exception {
ExtensionConfig.latest("math"),
ExtensionConfig.latest("optional"),
ExtensionConfig.latest("protos"),
+ ExtensionConfig.latest("regex"),
ExtensionConfig.latest("sets"),
ExtensionConfig.latest("strings"),
- ExtensionConfig.latest("comprehensions"));
+ ExtensionConfig.latest("two-var-comprehensions"));
CelEnvironment environment =
CelEnvironment.newBuilder().addExtensions(extensionConfigs).build();
@@ -100,6 +103,122 @@ public void extend_allExtensions() throws Exception {
assertThat(result).isTrue();
}
+ @Test
+ public void extend_allFeatureFlags() throws Exception {
+ CelEnvironment environment =
+ CelEnvironment.newBuilder()
+ .setFeatures(
+ CelEnvironment.FeatureFlag.create("cel.feature.macro_call_tracking", true),
+ CelEnvironment.FeatureFlag.create("cel.feature.backtick_escape_syntax", true),
+ CelEnvironment.FeatureFlag.create(
+ "cel.feature.cross_type_numeric_comparisons", true))
+ .build();
+
+ Cel cel =
+ environment.extend(
+ CelFactory.standardCelBuilder()
+ .setStandardMacros(CelStandardMacro.STANDARD_MACROS)
+ .build(),
+ CelOptions.DEFAULT);
+ CelAbstractSyntaxTree ast =
+ cel.compile("[{'foo.bar': 1}, {'foo.bar': 2}].all(e, e.`foo.bar` < 2.5)").getAst();
+ assertThat(ast.getSource().getMacroCalls()).hasSize(1);
+ boolean result = (boolean) cel.createProgram(ast).eval();
+ assertThat(result).isTrue();
+ }
+
+ @Test
+ public void extend_allLimits() throws Exception {
+ CelEnvironment environment =
+ CelEnvironment.newBuilder()
+ .setLimits(
+ CelEnvironment.Limit.create("cel.limit.expression_code_points", 20),
+ CelEnvironment.Limit.create("cel.limit.parse_error_recovery", 10),
+ CelEnvironment.Limit.create("cel.limit.parse_recursion_depth", 10),
+ CelEnvironment.Limit.create("cel.limit.expression_node_count", 500))
+ .build();
+
+ Cel cel =
+ environment.extend(
+ CelFactory.standardCelBuilder()
+ .setStandardMacros(CelStandardMacro.STANDARD_MACROS)
+ .build(),
+ CelOptions.DEFAULT);
+ CelOptions checkerOptions = cel.toCheckerBuilder().options();
+ assertThat(checkerOptions.maxExpressionCodePointSize()).isEqualTo(20);
+ assertThat(checkerOptions.maxParseErrorRecoveryLimit()).isEqualTo(10);
+ assertThat(checkerOptions.maxParseRecursionDepth()).isEqualTo(10);
+ assertThat(checkerOptions.maxParseExpressionNodeCount()).isEqualTo(500);
+
+ CelAbstractSyntaxTree ast = cel.compile("1 + 2 + 3 + 4 + 5").getAst();
+ Long result = (Long) cel.createProgram(ast).eval();
+ assertThat(result).isEqualTo(15L);
+
+ CelValidationResult validationResult = cel.compile("1 + 2 + 3 + 4 + 5 + 6");
+ assertThat(validationResult.hasError()).isTrue();
+ assertThat(validationResult.getErrorString())
+ .contains("expression code point size exceeds limit: size: 21, limit 20");
+ }
+
+ @Test
+ public void extend_expressionNodeCountLimit() throws Exception {
+ CelEnvironment environment =
+ CelEnvironment.newBuilder()
+ .setLimits(CelEnvironment.Limit.create("cel.limit.expression_node_count", 2))
+ .build();
+
+ Cel cel =
+ environment.extend(
+ CelFactory.legacyCelBuilder()
+ .setStandardMacros(CelStandardMacro.STANDARD_MACROS)
+ .build(),
+ CelOptions.DEFAULT);
+ CelOptions checkerOptions = cel.toCheckerBuilder().options();
+ assertThat(checkerOptions.maxParseExpressionNodeCount()).isEqualTo(2);
+
+ CelValidationResult validationResult = cel.compile("1 + 2 + 3");
+ assertThat(validationResult.hasError()).isTrue();
+ assertThat(validationResult.getErrorString()).contains("expression node limit (2) exceeded");
+ }
+
+ @Test
+ public void extend_unsupportedFeatureFlag_throws() throws Exception {
+ CelEnvironment environment =
+ CelEnvironment.newBuilder()
+ .setFeatures(CelEnvironment.FeatureFlag.create("unknown.feature", true))
+ .build();
+
+ IllegalArgumentException e =
+ assertThrows(
+ IllegalArgumentException.class,
+ () ->
+ environment.extend(
+ CelFactory.standardCelBuilder()
+ .setStandardMacros(CelStandardMacro.STANDARD_MACROS)
+ .build(),
+ CelOptions.DEFAULT));
+ assertThat(e).hasMessageThat().contains("Unknown feature flag: unknown.feature");
+ }
+
+ @Test
+ public void extend_unsupportedLimit_throws() throws Exception {
+ CelEnvironment environment =
+ CelEnvironment.newBuilder()
+ .setLimits(CelEnvironment.Limit.create("unknown.limit", 5))
+ .build();
+
+ IllegalArgumentException e =
+ assertThrows(
+ IllegalArgumentException.class,
+ () ->
+ environment.extend(
+ CelFactory.standardCelBuilder()
+ .setStandardMacros(CelStandardMacro.STANDARD_MACROS)
+ .build(),
+ CelOptions.DEFAULT));
+ assertThat(e).hasMessageThat().contains("Unknown limit: unknown.limit");
+ }
+
@Test
public void extensionVersion_specific() throws Exception {
CelEnvironment environment =
@@ -342,4 +461,30 @@ public void stdlibSubset_functionOverloadExcluded() throws Exception {
result = extendedCompiler.compile("1 == 1 && 1 != 1 + 1");
assertThat(result.getErrorString()).contains("found no matching overload for '_+_'");
}
+
+ @Test
+ public void typeDecl_toCelType_type() {
+ CelTypeProvider typeProvider =
+ CelCompilerFactory.standardCelCompilerBuilder().build().getTypeProvider();
+ CelEnvironment.TypeDecl typeDecl =
+ CelEnvironment.TypeDecl.newBuilder()
+ .setName("type")
+ .addParams(CelEnvironment.TypeDecl.create("int"))
+ .build();
+
+ CelType celType = typeDecl.toCelType(typeProvider);
+
+ assertThat(celType).isEqualTo(TypeType.create(SimpleType.INT));
+ }
+
+ @Test
+ public void typeDecl_toCelType_type_wrongParamCount_throws() {
+ CelTypeProvider typeProvider =
+ CelCompilerFactory.standardCelCompilerBuilder().build().getTypeProvider();
+ CelEnvironment.TypeDecl typeDecl = CelEnvironment.TypeDecl.newBuilder().setName("type").build();
+
+ IllegalStateException e =
+ assertThrows(IllegalStateException.class, () -> typeDecl.toCelType(typeProvider));
+ assertThat(e).hasMessageThat().contains("Expected 1 parameter for type, got 0");
+ }
}
diff --git a/bundle/src/test/java/dev/cel/bundle/CelEnvironmentYamlParserTest.java b/bundle/src/test/java/dev/cel/bundle/CelEnvironmentYamlParserTest.java
index d69d0517b..9a07a854d 100644
--- a/bundle/src/test/java/dev/cel/bundle/CelEnvironmentYamlParserTest.java
+++ b/bundle/src/test/java/dev/cel/bundle/CelEnvironmentYamlParserTest.java
@@ -19,12 +19,14 @@
import static org.junit.Assert.assertThrows;
import com.google.common.base.Ascii;
+import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.ImmutableSet;
import com.google.common.io.Resources;
import com.google.rpc.context.AttributeContext;
import com.google.testing.junit.testparameterinjector.TestParameter;
import com.google.testing.junit.testparameterinjector.TestParameterInjector;
+import dev.cel.bundle.CelEnvironment.ContextVariable;
import dev.cel.bundle.CelEnvironment.ExtensionConfig;
import dev.cel.bundle.CelEnvironment.FunctionDecl;
import dev.cel.bundle.CelEnvironment.LibrarySubset;
@@ -40,8 +42,8 @@
import dev.cel.common.types.SimpleType;
import dev.cel.parser.CelUnparserFactory;
import dev.cel.runtime.CelEvaluationListener;
-import dev.cel.runtime.CelLateFunctionBindings;
import dev.cel.runtime.CelFunctionBinding;
+import dev.cel.runtime.CelLateFunctionBindings;
import java.io.IOException;
import java.net.URL;
import java.util.Optional;
@@ -81,6 +83,62 @@ public void environment_setBasicProperties() throws Exception {
.build());
}
+ @Test
+ public void environment_setFeatures() throws Exception {
+ String yamlConfig =
+ "name: hello\n"
+ + "description: empty\n"
+ + "features:\n"
+ + " - name: 'cel.feature.macro_call_tracking'\n"
+ + " enabled: true\n"
+ + " - name: 'cel.feature.backtick_escape_syntax'\n"
+ + " enabled: false";
+
+ CelEnvironment environment = ENVIRONMENT_PARSER.parse(yamlConfig);
+
+ assertThat(environment)
+ .isEqualTo(
+ CelEnvironment.newBuilder()
+ .setSource(environment.source().get())
+ .setName("hello")
+ .setDescription("empty")
+ .setFeatures(
+ ImmutableSet.of(
+ CelEnvironment.FeatureFlag.create("cel.feature.macro_call_tracking", true),
+ CelEnvironment.FeatureFlag.create(
+ "cel.feature.backtick_escape_syntax", false)))
+ .build());
+ }
+
+ @Test
+ public void environment_setLimits() throws Exception {
+ String yamlConfig =
+ "name: hello\n"
+ + "description: empty\n"
+ + "limits:\n"
+ + " - name: 'cel.limit.expression_code_points'\n"
+ + " value: 1000\n"
+ + " - name: 'cel.limit.parse_error_recovery'\n"
+ + " value: 10\n"
+ + " - name: 'cel.limit.parse_recursion_depth'\n"
+ + " value: 7";
+
+ CelEnvironment environment = ENVIRONMENT_PARSER.parse(yamlConfig);
+
+ assertThat(environment)
+ .isEqualTo(
+ CelEnvironment.newBuilder()
+ .setSource(environment.source().get())
+ .setName("hello")
+ .setDescription("empty")
+ .setLimits(
+ ImmutableSet.of(
+ CelEnvironment.Limit.create("cel.limit.expression_code_points", 1000),
+ CelEnvironment.Limit.create("cel.limit.parse_error_recovery", 10),
+ CelEnvironment.Limit.create("cel.limit.parse_recursion_depth", 7)))
+ .build());
+ }
+
@Test
public void environment_setExtensions() throws Exception {
String yamlConfig =
@@ -322,6 +380,256 @@ public void environment_setMessageVariable() throws Exception {
assertThat(environment.extend(CEL_WITH_MESSAGE_TYPES, CelOptions.DEFAULT)).isNotNull();
}
+ @Test
+ public void environment_setListVariable_shorthand() throws Exception {
+ String yamlConfig =
+ "variables:\n" //
+ + "- name: 'request'\n" //
+ + " type: 'list'";
+
+ CelEnvironment environment = ENVIRONMENT_PARSER.parse(yamlConfig);
+
+ assertThat(environment)
+ .isEqualTo(
+ CelEnvironment.newBuilder()
+ .setSource(environment.source().get())
+ .setVariables(
+ ImmutableSet.of(
+ VariableDecl.create(
+ "request",
+ TypeDecl.newBuilder()
+ .setName("list")
+ .addParams(TypeDecl.create("string"))
+ .build())))
+ .build());
+ assertThat(environment.extend(CEL_WITH_MESSAGE_TYPES, CelOptions.DEFAULT)).isNotNull();
+ }
+
+ @Test
+ public void environment_setMapVariable_shorthand() throws Exception {
+ String yamlConfig =
+ "variables:\n" //
+ + "- name: 'request'\n" //
+ + " type: 'map'";
+
+ CelEnvironment environment = ENVIRONMENT_PARSER.parse(yamlConfig);
+
+ assertThat(environment)
+ .isEqualTo(
+ CelEnvironment.newBuilder()
+ .setSource(environment.source().get())
+ .setVariables(
+ ImmutableSet.of(
+ VariableDecl.create(
+ "request",
+ TypeDecl.newBuilder()
+ .setName("map")
+ .addParams(TypeDecl.create("string"), TypeDecl.create("dyn"))
+ .build())))
+ .build());
+ assertThat(environment.extend(CEL_WITH_MESSAGE_TYPES, CelOptions.DEFAULT)).isNotNull();
+ }
+
+ @Test
+ public void environment_withTypeSpecifiersEnabled_handlesStructuredMapTypeDecl()
+ throws Exception {
+ String yamlConfig =
+ "variables:\n" //
+ + "- name: 'request'\n" //
+ + " type:\n" //
+ + " type_name: 'map'\n" //
+ + " params:\n" //
+ + " - type_name: 'string'\n" //
+ + " - type_name: 'dyn'";
+
+ CelEnvironment environment = ENVIRONMENT_PARSER.parse(yamlConfig);
+
+ assertThat(environment)
+ .isEqualTo(
+ CelEnvironment.newBuilder()
+ .setSource(environment.source().get())
+ .setVariables(
+ ImmutableSet.of(
+ VariableDecl.create(
+ "request",
+ TypeDecl.newBuilder()
+ .setName("map")
+ .addParams(TypeDecl.create("string"), TypeDecl.create("dyn"))
+ .build())))
+ .build());
+ assertThat(environment.extend(CEL_WITH_MESSAGE_TYPES, CelOptions.DEFAULT)).isNotNull();
+ }
+
+ @Test
+ public void environment_withTypeSpecifiersEnabled_handlesBlockScalarTextTypeDecl()
+ throws Exception {
+ String yamlConfig =
+ "variables:\n" //
+ + "- name: 'request'\n" //
+ + " type: >-\n" //
+ + " list";
+
+ CelEnvironment environment = ENVIRONMENT_PARSER.parse(yamlConfig);
+
+ assertThat(environment)
+ .isEqualTo(
+ CelEnvironment.newBuilder()
+ .setSource(environment.source().get())
+ .setVariables(
+ ImmutableSet.of(
+ VariableDecl.create(
+ "request",
+ TypeDecl.newBuilder()
+ .setName("list")
+ .addParams(TypeDecl.create("string"))
+ .build())))
+ .build());
+ assertThat(environment.extend(CEL_WITH_MESSAGE_TYPES, CelOptions.DEFAULT)).isNotNull();
+ }
+
+ @Test
+ public void environment_setMessageVariable_shorthand() throws Exception {
+ String yamlConfig =
+ "variables:\n" //
+ + "- name: 'request'\n" //
+ + " type: 'google.rpc.context.AttributeContext.Request'";
+
+ CelEnvironment environment = ENVIRONMENT_PARSER.parse(yamlConfig);
+
+ assertThat(environment)
+ .isEqualTo(
+ CelEnvironment.newBuilder()
+ .setSource(environment.source().get())
+ .setVariables(
+ ImmutableSet.of(
+ VariableDecl.create(
+ "request",
+ TypeDecl.create("google.rpc.context.AttributeContext.Request"))))
+ .build());
+ assertThat(environment.extend(CEL_WITH_MESSAGE_TYPES, CelOptions.DEFAULT)).isNotNull();
+ }
+
+ @Test
+ public void environment_setContextVariable_type() throws Exception {
+ String yamlConfig =
+ "context_variable:\n" //
+ + " type: 'google.rpc.context.AttributeContext.Request'";
+
+ CelEnvironment environment = ENVIRONMENT_PARSER.parse(yamlConfig);
+
+ assertThat(environment)
+ .isEqualTo(
+ CelEnvironment.newBuilder()
+ .setSource(environment.source().get())
+ .setContextVariable(
+ ContextVariable.create("google.rpc.context.AttributeContext.Request"))
+ .build());
+ assertThat(environment.extend(CEL_WITH_MESSAGE_TYPES, CelOptions.DEFAULT)).isNotNull();
+ }
+
+ @Test
+ public void environment_setFunctions_shorthand() throws Exception {
+ String yamlConfig =
+ "functions:\n" //
+ + "- name: 'isEmpty'\n" //
+ + " overloads:\n" //
+ + " - id: 'list_isEmpty'\n" //
+ + " target: 'list<~T>'\n" //
+ + " return: 'bool'\n" //
+ + "- name: 'getOrDefault'\n" //
+ + " overloads:\n" //
+ + " - id: 'map_getOrDefault'\n" //
+ + " target: 'map<~K, ~V>'\n" //
+ + " args:\n" //
+ + " - '~K'\n" //
+ + " - '~V'\n" //
+ + " return: '~V'";
+
+ CelEnvironment environment = ENVIRONMENT_PARSER.parse(yamlConfig);
+
+ assertThat(environment)
+ .isEqualTo(
+ CelEnvironment.newBuilder()
+ .setSource(environment.source().get())
+ .setFunctions(
+ ImmutableSet.of(
+ FunctionDecl.create(
+ "isEmpty",
+ ImmutableSet.of(
+ OverloadDecl.newBuilder()
+ .setId("list_isEmpty")
+ .setTarget(
+ TypeDecl.newBuilder()
+ .setName("list")
+ .addParams(TypeDecl.ofTypeParam("T"))
+ .build())
+ .setReturnType(TypeDecl.create("bool"))
+ .build())),
+ FunctionDecl.create(
+ "getOrDefault",
+ ImmutableSet.of(
+ OverloadDecl.newBuilder()
+ .setId("map_getOrDefault")
+ .setTarget(
+ TypeDecl.newBuilder()
+ .setName("map")
+ .addParams(
+ TypeDecl.ofTypeParam("K"),
+ TypeDecl.ofTypeParam("V"))
+ .build())
+ .addArguments(
+ TypeDecl.ofTypeParam("K"), TypeDecl.ofTypeParam("V"))
+ .setReturnType(TypeDecl.ofTypeParam("V"))
+ .build()))))
+ .build());
+ assertThat(environment.extend(CEL_WITH_MESSAGE_TYPES, CelOptions.DEFAULT)).isNotNull();
+ }
+
+ @Test
+ public void environment_withTypeSpecifier_invalidSyntaxError() {
+ String yamlConfig =
+ "variables:\n" //
+ + "- name: 'request'\n" //
+ + " type: 'list<'";
+
+ CelEnvironmentException e =
+ assertThrows(CelEnvironmentException.class, () -> ENVIRONMENT_PARSER.parse(yamlConfig));
+ assertThat(e).hasMessageThat().contains("missing identifier at position 5");
+ }
+
+ @Test
+ public void environment_withTypeSpecifier_invalidYamlNodeError() {
+ String yamlConfig =
+ "variables:\n" //
+ + "- name: 'request'\n" //
+ + " type: 1";
+
+ CelEnvironmentException e =
+ assertThrows(CelEnvironmentException.class, () -> ENVIRONMENT_PARSER.parse(yamlConfig));
+ assertThat(e)
+ .hasMessageThat()
+ .contains("wanted type(s) [tag:yaml.org,2002:str !txt tag:yaml.org,2002:map]");
+ }
+
+ @Test
+ public void environment_evaluatesShorthandVariable() throws Exception {
+ String yamlConfig =
+ "variables:\n" //
+ + "- name: 'values'\n" //
+ + " type: 'list'";
+
+ CelEnvironment environment = ENVIRONMENT_PARSER.parse(yamlConfig);
+ Cel cel = environment.extend(CelFactory.standardCelBuilder().build(), CelOptions.DEFAULT);
+
+ CelAbstractSyntaxTree ast = cel.compile("values.size() == 2 && values[0] == 'hello'").getAst();
+ boolean result =
+ (boolean)
+ cel.createProgram(ast)
+ .eval(
+ ImmutableMap.of("values", ImmutableList.of("hello", "world")));
+ assertThat(result).isTrue();
+ }
+
@Test
public void environment_setContainer() throws Exception {
String yamlConfig =
@@ -521,7 +829,7 @@ private enum EnvironmentParseErrorTestcase {
+ " - name: foo\n" //
+ " type: 1",
"ERROR: :3:10: Got yaml node type tag:yaml.org,2002:int, wanted type(s)"
- + " [tag:yaml.org,2002:map]\n"
+ + " [tag:yaml.org,2002:str !txt tag:yaml.org,2002:map]\n"
+ " | type: 1\n"
+ " | .........^"),
ILLEGAL_YAML_TYPE_TYPE_VALUE(
@@ -619,9 +927,7 @@ private enum EnvironmentParseErrorTestcase {
+ " | - version: 0\n"
+ " | ..^"),
ILLEGAL_LIBRARY_SUBSET_TAG(
- "name: 'test_suite_name'\n"
- + "stdlib:\n"
- + " unknown_tag: 'test_value'\n",
+ "name: 'test_suite_name'\n" + "stdlib:\n" + " unknown_tag: 'test_value'\n",
"ERROR: :3:3: Unsupported library subset tag: unknown_tag\n"
+ " | unknown_tag: 'test_value'\n"
+ " | ..^"),
@@ -672,6 +978,40 @@ private enum EnvironmentParseErrorTestcase {
"ERROR: :6:7: Unsupported alias tag: unknown_tag\n"
+ " | unknown_tag: 'test_value'\n"
+ " | ......^"),
+ UNSUPPORTED_LIMIT_TAG(
+ "limits:\n"
+ + " - name: 'test_limit'\n"
+ + " unknown_tag: 'test_value'\n"
+ + " value: 100\n",
+ "ERROR: :3:5: Unsupported limits tag: unknown_tag\n"
+ + " | unknown_tag: 'test_value'\n"
+ + " | ....^"),
+ MISSING_LIMIT_NAME(
+ "limits:\n" + " - value: 100\n",
+ "ERROR: :2:5: Missing required attribute(s): name\n"
+ + " | - value: 100\n"
+ + " | ....^"),
+ MISSING_LIMIT_VALUE(
+ "limits:\n" + " - name: 'test_limit'\n",
+ "ERROR: :2:5: Missing required attribute(s): value\n"
+ + " | - name: 'test_limit'\n"
+ + " | ....^"),
+ ILLEGAL_LIMIT_VALUE(
+ "limits:\n" + " - cel.limit.foo: 'not_a_number'\n",
+ "ERROR: :2:21: Got yaml node type tag:yaml.org,2002:str, wanted type(s)"
+ + " [tag:yaml.org,2002:int]\n"
+ + " | - cel.limit.foo: 'not_a_number'\n"
+ + " | ....................^"),
+ ILLEGAL_FEATURE_TAG(
+ "features:\n" + " - name: 'test_feature'\n" + " unknown_tag: 'test_value'\n",
+ "ERROR: :3:5: Unsupported feature tag: unknown_tag\n"
+ + " | unknown_tag: 'test_value'\n"
+ + " | ....^"),
+ MISSING_FEATURE_NAME(
+ "features:\n" + " - enabled: true\n",
+ "ERROR: :2:5: Missing required attribute(s): name\n"
+ + " | - enabled: true\n"
+ + " | ....^"),
;
private final String yamlConfig;
@@ -769,30 +1109,87 @@ private enum EnvironmentYamlResourceTestCase {
.setVariables(
VariableDecl.newBuilder()
.setName("msg")
+ .setDescription(
+ "msg represents all possible type permutation which CEL understands from a"
+ + " proto perspective")
.setType(TypeDecl.create("cel.expr.conformance.proto3.TestAllTypes"))
.build())
.setFunctions(
- FunctionDecl.create(
- "isEmpty",
- ImmutableSet.of(
- OverloadDecl.newBuilder()
- .setId("wrapper_string_isEmpty")
- .setTarget(TypeDecl.create("google.protobuf.StringValue"))
- .setReturnType(TypeDecl.create("bool"))
- .build(),
- OverloadDecl.newBuilder()
- .setId("list_isEmpty")
- .setTarget(
- TypeDecl.newBuilder()
- .setName("list")
- .addParams(
- TypeDecl.newBuilder()
- .setName("T")
- .setIsTypeParam(true)
- .build())
- .build())
- .setReturnType(TypeDecl.create("bool"))
- .build())))
+ FunctionDecl.newBuilder()
+ .setName("isEmpty")
+ .setDescription(
+ "determines whether a list is empty,\nor a string has no characters")
+ .setOverloads(
+ ImmutableSet.of(
+ OverloadDecl.newBuilder()
+ .setId("wrapper_string_isEmpty")
+ .setTarget(TypeDecl.create("google.protobuf.StringValue"))
+ .addExamples("''.isEmpty() // true")
+ .setReturnType(TypeDecl.create("bool"))
+ .build(),
+ OverloadDecl.newBuilder()
+ .setId("list_isEmpty")
+ .addExamples("[].isEmpty() // true")
+ .addExamples("[1].isEmpty() // false")
+ .setTarget(
+ TypeDecl.newBuilder()
+ .setName("list")
+ .addParams(
+ TypeDecl.newBuilder()
+ .setName("T")
+ .setIsTypeParam(true)
+ .build())
+ .build())
+ .setReturnType(TypeDecl.create("bool"))
+ .build()))
+ .build(),
+ FunctionDecl.newBuilder()
+ .setName("isEmptyAlt")
+ .setDescription(
+ "determines whether a list is empty,\nor a string has no characters")
+ .setOverloads(
+ ImmutableSet.of(
+ OverloadDecl.newBuilder()
+ .setId("wrapper_string_isEmpty")
+ .setTarget(TypeDecl.create("google.protobuf.StringValue"))
+ .addExamples("''.isEmptyAlt() // true")
+ .setReturnType(TypeDecl.create("bool"))
+ .build(),
+ OverloadDecl.newBuilder()
+ .setId("list_isEmpty")
+ .addExamples("[].isEmptyAlt() // true")
+ .addExamples("[1].isEmptyAlt() // false")
+ .setTarget(
+ TypeDecl.newBuilder()
+ .setName("list")
+ .addParams(TypeDecl.ofTypeParam("T"))
+ .build())
+ .setReturnType(TypeDecl.create("bool"))
+ .build()))
+ .build(),
+ FunctionDecl.newBuilder()
+ .setName("getOrDefault")
+ .setDescription(
+ "Returns the value of a key in a map or the provided\ndefault value.")
+ .setOverloads(
+ ImmutableSet.of(
+ OverloadDecl.newBuilder()
+ .setId("map_getOrDefault")
+ .setTarget(
+ TypeDecl.newBuilder()
+ .setName("map")
+ .addParams(
+ TypeDecl.ofTypeParam("K"), TypeDecl.ofTypeParam("V"))
+ .build())
+ .addArguments(TypeDecl.ofTypeParam("K"), TypeDecl.ofTypeParam("V"))
+ .setReturnType(TypeDecl.ofTypeParam("V"))
+ .build()))
+ .build())
+ .setFeatures(CelEnvironment.FeatureFlag.create("cel.feature.macro_call_tracking", true))
+ .setLimits(
+ ImmutableSet.of(
+ CelEnvironment.Limit.create("cel.limit.expression_code_points", 1000),
+ CelEnvironment.Limit.create("cel.limit.parse_recursion_depth", 7)))
.build()),
LIBRARY_SUBSET_ENV(
diff --git a/bundle/src/test/java/dev/cel/bundle/CelEnvironmentYamlSerializerTest.java b/bundle/src/test/java/dev/cel/bundle/CelEnvironmentYamlSerializerTest.java
index 7e4be0912..aad72a578 100644
--- a/bundle/src/test/java/dev/cel/bundle/CelEnvironmentYamlSerializerTest.java
+++ b/bundle/src/test/java/dev/cel/bundle/CelEnvironmentYamlSerializerTest.java
@@ -106,6 +106,68 @@ public void toYaml_success() throws Exception {
.setReturnType(
TypeDecl.newBuilder().setName("V").setIsTypeParam(true).build())
.build())),
+ FunctionDecl.create(
+ "zip",
+ ImmutableSet.of(
+ OverloadDecl.newBuilder()
+ .setId("zip_list_int_list_int")
+ .setArguments(
+ ImmutableList.of(
+ TypeDecl.newBuilder()
+ .setName("list")
+ .addParams(TypeDecl.create("int"))
+ .build(),
+ TypeDecl.newBuilder()
+ .setName("list")
+ .addParams(TypeDecl.create("int"))
+ .build()))
+ .setReturnType(
+ TypeDecl.newBuilder()
+ .setName("list")
+ .addParams(
+ TypeDecl.newBuilder()
+ .setName("list")
+ .addParams(TypeDecl.create("int"))
+ .build())
+ .build())
+ .build())),
+ FunctionDecl.create(
+ "zipGeneric",
+ ImmutableSet.of(
+ OverloadDecl.newBuilder()
+ .setId("zip_list_list")
+ .setArguments(
+ ImmutableList.of(
+ TypeDecl.newBuilder()
+ .setName("list")
+ .addParams(
+ TypeDecl.newBuilder()
+ .setName("T")
+ .setIsTypeParam(true)
+ .build())
+ .build(),
+ TypeDecl.newBuilder()
+ .setName("list")
+ .addParams(
+ TypeDecl.newBuilder()
+ .setName("T")
+ .setIsTypeParam(true)
+ .build())
+ .build()))
+ .setReturnType(
+ TypeDecl.newBuilder()
+ .setName("list")
+ .addParams(
+ TypeDecl.newBuilder()
+ .setName("list")
+ .addParams(
+ TypeDecl.newBuilder()
+ .setName("T")
+ .setIsTypeParam(true)
+ .build())
+ .build())
+ .build())
+ .build())),
FunctionDecl.create(
"coalesce",
ImmutableSet.of(
@@ -126,6 +188,13 @@ public void toYaml_success() throws Exception {
FunctionSelector.create(
"_+_", ImmutableSet.of("add_bytes", "add_list"))))
.build())
+ .setFeatures(
+ CelEnvironment.FeatureFlag.create("cel.feature.macro_call_tracking", true),
+ CelEnvironment.FeatureFlag.create("cel.feature.backtick_escape_syntax", false))
+ .setLimits(
+ CelEnvironment.Limit.create("cel.limit.expression_code_points", 1000),
+ CelEnvironment.Limit.create("cel.limit.parse_error_recovery", 10),
+ CelEnvironment.Limit.create("cel.limit.parse_recursion_depth", 7))
.build();
String yamlOutput = CelEnvironmentYamlSerializer.toYaml(environment);
diff --git a/bundle/src/test/java/dev/cel/bundle/CelImplTest.java b/bundle/src/test/java/dev/cel/bundle/CelImplTest.java
index 9f7083c92..4f82411a3 100644
--- a/bundle/src/test/java/dev/cel/bundle/CelImplTest.java
+++ b/bundle/src/test/java/dev/cel/bundle/CelImplTest.java
@@ -59,8 +59,8 @@
import com.google.rpc.context.AttributeContext;
import com.google.testing.junit.testparameterinjector.TestParameter;
import com.google.testing.junit.testparameterinjector.TestParameterInjector;
-import com.google.testing.junit.testparameterinjector.TestParameters;
import dev.cel.checker.CelCheckerLegacyImpl;
+import dev.cel.checker.CelStandardDeclarations;
import dev.cel.checker.DescriptorTypeProvider;
import dev.cel.checker.ProtoTypeMask;
import dev.cel.checker.TypeProvider;
@@ -98,6 +98,7 @@
import dev.cel.expr.conformance.proto2.Proto2ExtensionScopedMessage;
import dev.cel.expr.conformance.proto2.TestAllTypesExtensions;
import dev.cel.expr.conformance.proto3.TestAllTypes;
+import dev.cel.extensions.CelExtensions;
import dev.cel.parser.CelParserImpl;
import dev.cel.parser.CelStandardMacro;
import dev.cel.runtime.CelAttribute;
@@ -110,10 +111,13 @@
import dev.cel.runtime.CelRuntime.Program;
import dev.cel.runtime.CelRuntimeFactory;
import dev.cel.runtime.CelRuntimeLegacyImpl;
+import dev.cel.runtime.CelStandardFunctions;
import dev.cel.runtime.CelUnknownSet;
import dev.cel.runtime.CelVariableResolver;
import dev.cel.runtime.UnknownContext;
-import dev.cel.testing.testdata.SingleFileProto.SingleFile;
+import dev.cel.testing.CelRuntimeFlavor;
+import dev.cel.testing.testdata.SingleFile;
+import dev.cel.testing.testdata.SingleFileExtensionsProto;
import dev.cel.testing.testdata.proto3.StandaloneGlobalEnum;
import java.time.Instant;
import java.util.ArrayList;
@@ -229,9 +233,7 @@ public void check() throws Exception {
}
@Test
- @TestParameters("{useProtoResultType: false}")
- @TestParameters("{useProtoResultType: true}")
- public void compile(boolean useProtoResultType) throws Exception {
+ public void compile(@TestParameter boolean useProtoResultType) throws Exception {
CelBuilder celBuilder = standardCelBuilderWithMacros();
if (useProtoResultType) {
celBuilder.setProtoResultType(CelProtoTypes.BOOL);
@@ -243,9 +245,7 @@ public void compile(boolean useProtoResultType) throws Exception {
}
@Test
- @TestParameters("{useProtoResultType: false}")
- @TestParameters("{useProtoResultType: true}")
- public void compile_resultTypeCheckFailure(boolean useProtoResultType) {
+ public void compile_resultTypeCheckFailure(@TestParameter boolean useProtoResultType) {
CelBuilder celBuilder = standardCelBuilderWithMacros();
if (useProtoResultType) {
celBuilder.setProtoResultType(CelProtoTypes.STRING);
@@ -560,23 +560,6 @@ public void program_withVars() throws Exception {
assertThat(program.eval(ImmutableMap.of("variable", "hello"))).isEqualTo(true);
}
- @Test
- public void program_withCelValue() throws Exception {
- Cel cel =
- standardCelBuilderWithMacros()
- .setOptions(CelOptions.current().enableCelValue(true).build())
- .addDeclarations(
- Decl.newBuilder()
- .setName("variable")
- .setIdent(IdentDecl.newBuilder().setType(CelProtoTypes.STRING))
- .build())
- .setResultType(SimpleType.BOOL)
- .build();
-
- CelRuntime.Program program = cel.createProgram(cel.compile("variable == 'hello'").getAst());
-
- assertThat(program.eval(ImmutableMap.of("variable", "hello"))).isEqualTo(true);
- }
@Test
public void program_withProtoVars() throws Exception {
@@ -1003,9 +986,8 @@ public void program_protoActivation() throws Exception {
}
@Test
- @TestParameters("{resolveTypeDependencies: false}")
- @TestParameters("{resolveTypeDependencies: true}")
- public void program_enumTypeDirectResolution(boolean resolveTypeDependencies) throws Exception {
+ public void program_enumTypeDirectResolution(@TestParameter boolean resolveTypeDependencies)
+ throws Exception {
Cel cel =
standardCelBuilderWithMacros()
.addFileTypes(StandaloneGlobalEnum.getDescriptor().getFile())
@@ -1026,9 +1008,7 @@ public void program_enumTypeDirectResolution(boolean resolveTypeDependencies) th
}
@Test
- @TestParameters("{resolveTypeDependencies: false}")
- @TestParameters("{resolveTypeDependencies: true}")
- public void program_enumTypeReferenceResolution(boolean resolveTypeDependencies)
+ public void program_enumTypeReferenceResolution(@TestParameter boolean resolveTypeDependencies)
throws Exception {
Cel cel =
standardCelBuilderWithMacros()
@@ -1426,25 +1406,6 @@ public void programAdvanceEvaluation_nestedSelect() throws Exception {
.isEqualTo(CelUnknownSet.create(CelAttribute.fromQualifiedIdentifier("com.google.a")));
}
- @Test
- public void programAdvanceEvaluation_nestedSelect_withCelValue() throws Exception {
- Cel cel =
- standardCelBuilderWithMacros()
- .setOptions(
- CelOptions.current().enableUnknownTracking(true).enableCelValue(true).build())
- .addVar("com", MapType.create(SimpleType.STRING, SimpleType.DYN))
- .addFunctionBindings()
- .setResultType(SimpleType.BOOL)
- .build();
- CelRuntime.Program program = cel.createProgram(cel.compile("com.google.a || false").getAst());
-
- assertThat(
- program.advanceEvaluation(
- UnknownContext.create(
- fromMap(ImmutableMap.of()),
- ImmutableList.of(CelAttributePattern.fromQualifiedIdentifier("com.google.a")))))
- .isEqualTo(CelUnknownSet.create(CelAttribute.fromQualifiedIdentifier("com.google.a")));
- }
@Test
public void programAdvanceEvaluation_argumentMergeErrorPriority() throws Exception {
@@ -2109,7 +2070,6 @@ public void program_fdsContainsWktDependency_descriptorInstancesMatch() throws E
standardCelBuilderWithMacros()
.addMessageTypes(descriptors)
// CEL-Internal-2
- .setOptions(CelOptions.current().enableTimestampEpoch(true).build())
.setContainer(CelContainer.ofName("cel.expr.conformance.proto3"))
.build();
CelAbstractSyntaxTree ast =
@@ -2143,20 +2103,93 @@ public void toBuilder_isImmutable() {
}
@Test
- public void eval_withJsonFieldName() throws Exception {
- Cel cel =
- standardCelBuilderWithMacros()
- .addVar("file", StructTypeReference.create(SingleFile.getDescriptor().getFullName()))
- .addMessageTypes(SingleFile.getDescriptor())
- .setOptions(CelOptions.current().enableJsonFieldNames(true).build())
- .build();
- CelAbstractSyntaxTree ast = cel.compile("file.camelCased").getAst();
+ public void eval_withJsonFieldName(@TestParameter CelRuntimeFlavor runtimeFlavor)
+ throws Exception {
+ Cel cel = setupEnv(runtimeFlavor.builder());
+ CelAbstractSyntaxTree ast =
+ cel.compile(
+ "file.int32_snake_case_json_name == 1 && "
+ + "file.int64CamelCaseJsonName == 2 && "
+ + "file.uint32DefaultJsonName == 3u && "
+ + "file.`uint64-custom-json-name` == 4u && "
+ + "file.single_string == 'shadows' && "
+ + "file.singleString == 'shadowed'")
+ .getAst();
+
+ boolean result =
+ (boolean)
+ cel.createProgram(ast)
+ .eval(
+ ImmutableMap.of(
+ "file",
+ SingleFile.newBuilder()
+ .setInt32SnakeCaseJsonName(1)
+ .setInt64CamelCaseJsonName(2L)
+ .setUint32DefaultJsonName(3)
+ .setUint64CustomJsonName(4)
+ .setStringJsonNameShadows("shadows")
+ .setSingleString("shadowed")
+ .setExtension(SingleFileExtensionsProto.int64CamelCaseJsonName, 5L)
+ .build()));
- Object result =
- cel.createProgram(ast)
- .eval(ImmutableMap.of("file", SingleFile.newBuilder().setSnakeCased("foo").build()));
+ assertThat(result).isTrue();
+ }
- assertThat(result).isEqualTo("foo");
+ @Test
+ public void eval_withJsonFieldName_fieldsFallBack(@TestParameter CelRuntimeFlavor runtimeFlavor)
+ throws Exception {
+ Cel cel = setupEnv(runtimeFlavor.builder());
+ CelAbstractSyntaxTree ast =
+ cel.compile(
+ "dyn(file).int32_snake_case_json_name == 1 && "
+ + "dyn(file).`uint64-custom-json-name` == 4u && "
+ + "dyn(file).single_string == 'shadows' && "
+ + "dyn(file).string_json_name_shadows == 'shadows' && "
+ + "dyn(file).singleString == 'shadowed'")
+ .getAst();
+
+ boolean result =
+ (boolean)
+ cel.createProgram(ast)
+ .eval(
+ ImmutableMap.of(
+ "file",
+ SingleFile.newBuilder()
+ .setInt32SnakeCaseJsonName(1)
+ .setInt64CamelCaseJsonName(2L)
+ .setUint32DefaultJsonName(3)
+ .setUint64CustomJsonName(4)
+ .setStringJsonNameShadows("shadows")
+ .setSingleString("shadowed")
+ .build()));
+
+ assertThat(result).isTrue();
+ }
+
+ @Test
+ public void eval_withJsonFieldName_extensionFields(@TestParameter CelRuntimeFlavor runtimeFlavor)
+ throws Exception {
+ Cel cel = setupEnv(runtimeFlavor.builder());
+ CelAbstractSyntaxTree ast =
+ cel.compile(
+ "proto.getExt(file, dev.cel.testing.testdata.int64CamelCaseJsonName) == 5 &&"
+ + " proto.getExt(file, dev.cel.testing.testdata.single_string) == 'foo'")
+ .getAst();
+
+ boolean result =
+ (boolean)
+ cel.createProgram(ast)
+ .eval(
+ ImmutableMap.of(
+ "file",
+ SingleFile.newBuilder()
+ .setInt64CamelCaseJsonName(2L)
+ .setExtension(SingleFileExtensionsProto.int64CamelCaseJsonName, 5L)
+ .setSingleString("This should not be used")
+ .setExtension(SingleFileExtensionsProto.singleString, "foo")
+ .build()));
+
+ assertThat(result).isTrue();
}
@Test
@@ -2172,7 +2205,7 @@ public void eval_withJsonFieldName_runtimeOptionDisabled_throws() throws Excepti
.addMessageTypes(SingleFile.getDescriptor())
.setOptions(CelOptions.current().enableJsonFieldNames(false).build())
.build();
- CelAbstractSyntaxTree ast = celCompiler.compile("file.camelCased").getAst();
+ CelAbstractSyntaxTree ast = celCompiler.compile("file.int64CamelCaseJsonName").getAst();
CelEvaluationException e =
assertThrows(
@@ -2184,7 +2217,8 @@ public void eval_withJsonFieldName_runtimeOptionDisabled_throws() throws Excepti
assertThat(e)
.hasMessageThat()
.contains(
- "field 'camelCased' is not declared in message 'dev.cel.testing.testdata.SingleFile");
+ "field 'int64CamelCaseJsonName' is not declared in message"
+ + " 'dev.cel.testing.testdata.SingleFile");
}
@Test
@@ -2195,7 +2229,7 @@ public void compile_withJsonFieldName_astTagged() throws Exception {
.addMessageTypes(SingleFile.getDescriptor())
.setOptions(CelOptions.current().enableJsonFieldNames(true).build())
.build();
- CelAbstractSyntaxTree ast = cel.compile("file.camelCased").getAst();
+ CelAbstractSyntaxTree ast = cel.compile("file.int64CamelCaseJsonName").getAst();
assertThat(ast.getSource().getExtensions())
.contains(
@@ -2244,4 +2278,45 @@ private static TypeProvider aliasingProvider(ImmutableMap typeAlia
}
};
}
+
+ private static Cel setupEnv(CelBuilder celBuilder) {
+ ExtensionRegistry extensionRegistry = ExtensionRegistry.newInstance();
+ SingleFileExtensionsProto.registerAllExtensions(extensionRegistry);
+ return celBuilder
+ .addVar("file", StructTypeReference.create(SingleFile.getDescriptor().getFullName()))
+ .addMessageTypes(SingleFile.getDescriptor())
+ .addFileTypes(SingleFileExtensionsProto.getDescriptor())
+ .addCompilerLibraries(CelExtensions.protos())
+ .setExtensionRegistry(extensionRegistry)
+ .setOptions(
+ CelOptions.current()
+ .enableJsonFieldNames(true)
+ .enableHeterogeneousNumericComparisons(true)
+ .enableQuotedIdentifierSyntax(true)
+ .build())
+ .build();
+ }
+
+ @Test
+ public void plannerCelBuilder_setStandardDeclarationsAndFunctions_subsetsEnvironment()
+ throws Exception {
+ Cel cel =
+ CelFactory.plannerCelBuilder()
+ .setStandardDeclarations(
+ CelStandardDeclarations.newBuilder()
+ .includeFunctions(CelStandardDeclarations.StandardFunction.ADD)
+ .build())
+ .setStandardFunctions(
+ CelStandardFunctions.newBuilder()
+ .includeFunctions(CelStandardFunctions.StandardFunction.ADD)
+ .build())
+ .build();
+
+ CelAbstractSyntaxTree ast = cel.compile("1 + 1").getAst();
+ assertThat(cel.createProgram(ast).eval()).isEqualTo(2L);
+
+ CelValidationException validationException =
+ assertThrows(CelValidationException.class, () -> cel.compile("1 - 1").getAst());
+ assertThat(validationException).hasMessageThat().contains("undeclared reference to '_-_'");
+ }
}
diff --git a/bundle/src/test/java/dev/cel/bundle/TypeSpecifierParserTest.java b/bundle/src/test/java/dev/cel/bundle/TypeSpecifierParserTest.java
new file mode 100644
index 000000000..ee3dab9c6
--- /dev/null
+++ b/bundle/src/test/java/dev/cel/bundle/TypeSpecifierParserTest.java
@@ -0,0 +1,250 @@
+// 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.bundle;
+
+import static com.google.common.truth.Truth.assertThat;
+import static org.junit.Assert.assertThrows;
+
+import com.google.testing.junit.testparameterinjector.TestParameter;
+import com.google.testing.junit.testparameterinjector.TestParameterInjector;
+import dev.cel.bundle.CelEnvironment.TypeDecl;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+@RunWith(TestParameterInjector.class)
+public final class TypeSpecifierParserTest {
+
+ @Test
+ public void parse_concreteSimpleType() {
+ assertThat(TypeDecl.parse("int")).isEqualTo(TypeDecl.create("int"));
+ assertThat(TypeDecl.parse("string")).isEqualTo(TypeDecl.create("string"));
+ assertThat(TypeDecl.parse("bool")).isEqualTo(TypeDecl.create("bool"));
+ assertThat(TypeDecl.parse("double")).isEqualTo(TypeDecl.create("double"));
+ assertThat(TypeDecl.parse("uint")).isEqualTo(TypeDecl.create("uint"));
+ assertThat(TypeDecl.parse("bytes")).isEqualTo(TypeDecl.create("bytes"));
+ assertThat(TypeDecl.parse("duration")).isEqualTo(TypeDecl.create("duration"));
+ assertThat(TypeDecl.parse("timestamp")).isEqualTo(TypeDecl.create("timestamp"));
+ assertThat(TypeDecl.parse("dyn")).isEqualTo(TypeDecl.create("dyn"));
+ assertThat(TypeDecl.parse("any")).isEqualTo(TypeDecl.create("any"));
+ assertThat(TypeDecl.parse("null_type")).isEqualTo(TypeDecl.create("null_type"));
+ }
+
+ @Test
+ public void parse_qualifiedMessageType() {
+ assertThat(TypeDecl.parse("google.protobuf.StringValue"))
+ .isEqualTo(TypeDecl.create("google.protobuf.StringValue"));
+ assertThat(TypeDecl.parse("google.rpc.context.AttributeContext.Request"))
+ .isEqualTo(TypeDecl.create("google.rpc.context.AttributeContext.Request"));
+ assertThat(TypeDecl.parse(".com.example.Message"))
+ .isEqualTo(TypeDecl.create(".com.example.Message"));
+ }
+
+ @Test
+ public void parse_parameterizedTypes() {
+ assertThat(TypeDecl.parse("list"))
+ .isEqualTo(TypeDecl.newBuilder().setName("list").addParams(TypeDecl.create("int")).build());
+ assertThat(TypeDecl.parse("map"))
+ .isEqualTo(
+ TypeDecl.newBuilder()
+ .setName("map")
+ .addParams(TypeDecl.create("string"), TypeDecl.create("dyn"))
+ .build());
+ assertThat(TypeDecl.parse("optional_type"))
+ .isEqualTo(
+ TypeDecl.newBuilder()
+ .setName("optional_type")
+ .addParams(TypeDecl.create("string"))
+ .build());
+ assertThat(TypeDecl.parse("type"))
+ .isEqualTo(TypeDecl.newBuilder().setName("type").addParams(TypeDecl.create("int")).build());
+ }
+
+ @Test
+ public void parse_nestedParameterizedTypes() {
+ assertThat(TypeDecl.parse("map>"))
+ .isEqualTo(
+ TypeDecl.newBuilder()
+ .setName("map")
+ .addParams(
+ TypeDecl.create("int"),
+ TypeDecl.newBuilder()
+ .setName("list")
+ .addParams(TypeDecl.create("string"))
+ .build())
+ .build());
+
+ assertThat(TypeDecl.parse("list>>"))
+ .isEqualTo(
+ TypeDecl.newBuilder()
+ .setName("list")
+ .addParams(
+ TypeDecl.newBuilder()
+ .setName("map")
+ .addParams(
+ TypeDecl.create("string"),
+ TypeDecl.newBuilder()
+ .setName("optional_type")
+ .addParams(TypeDecl.create("int"))
+ .build())
+ .build())
+ .build());
+ }
+
+ @Test
+ public void parse_whitespaceTolerance() {
+ assertThat(TypeDecl.parse(" list < int > "))
+ .isEqualTo(TypeDecl.newBuilder().setName("list").addParams(TypeDecl.create("int")).build());
+ assertThat(TypeDecl.parse(" map < string , list < int > > "))
+ .isEqualTo(
+ TypeDecl.newBuilder()
+ .setName("map")
+ .addParams(
+ TypeDecl.create("string"),
+ TypeDecl.newBuilder().setName("list").addParams(TypeDecl.create("int")).build())
+ .build());
+ }
+
+ @Test
+ public void parse_whitespaceWithTabsAndNewlines() {
+ assertThat(TypeDecl.parse("list<\tstring\n>"))
+ .isEqualTo(
+ TypeDecl.newBuilder().setName("list").addParams(TypeDecl.create("string")).build());
+ assertThat(TypeDecl.parse(" map < string ,\t int > "))
+ .isEqualTo(
+ TypeDecl.newBuilder()
+ .setName("map")
+ .addParams(TypeDecl.create("string"), TypeDecl.create("int"))
+ .build());
+ assertThat(TypeDecl.parse("map\t<\nint\r,\tstring\n>\r"))
+ .isEqualTo(
+ TypeDecl.newBuilder()
+ .setName("map")
+ .addParams(TypeDecl.create("int"), TypeDecl.create("string"))
+ .build());
+ assertThat(TypeDecl.parse("\tlist\n<\r~T\t>\n"))
+ .isEqualTo(
+ TypeDecl.newBuilder().setName("list").addParams(TypeDecl.ofTypeParam("T")).build());
+ }
+
+ @Test
+ public void parse_typeParameters() {
+ assertThat(TypeDecl.parse("~T")).isEqualTo(TypeDecl.ofTypeParam("T"));
+ assertThat(TypeDecl.parse(" ~T ")).isEqualTo(TypeDecl.ofTypeParam("T"));
+ assertThat(TypeDecl.parse("list<~T>"))
+ .isEqualTo(
+ TypeDecl.newBuilder().setName("list").addParams(TypeDecl.ofTypeParam("T")).build());
+ assertThat(TypeDecl.parse("list< ~T >"))
+ .isEqualTo(
+ TypeDecl.newBuilder().setName("list").addParams(TypeDecl.ofTypeParam("T")).build());
+ assertThat(TypeDecl.parse("map<~K, ~V>"))
+ .isEqualTo(
+ TypeDecl.newBuilder()
+ .setName("map")
+ .addParams(TypeDecl.ofTypeParam("K"), TypeDecl.ofTypeParam("V"))
+ .build());
+ assertThat(TypeDecl.parse("map< ~K , ~V >"))
+ .isEqualTo(
+ TypeDecl.newBuilder()
+ .setName("map")
+ .addParams(TypeDecl.ofTypeParam("K"), TypeDecl.ofTypeParam("V"))
+ .build());
+ }
+
+ @Test
+ public void parse_maxRecursionDepth_succeedsAtBoundary() {
+ StringBuilder sb = new StringBuilder();
+ for (int i = 0; i < 64; i++) {
+ sb.append("list<");
+ }
+ sb.append("int");
+ for (int i = 0; i < 64; i++) {
+ sb.append(">");
+ }
+ String input = sb.toString();
+ TypeDecl result = TypeDecl.parse(input);
+ assertThat(result).isNotNull();
+ }
+
+ @Test
+ public void parse_exceedsMaxRecursionDepth_throws() {
+ StringBuilder sb = new StringBuilder();
+ for (int i = 0; i < 65; i++) {
+ sb.append("list<");
+ }
+ sb.append("int");
+ for (int i = 0; i < 65; i++) {
+ sb.append(">");
+ }
+ String input = sb.toString();
+ IllegalArgumentException e =
+ assertThrows(IllegalArgumentException.class, () -> TypeDecl.parse(input));
+ assertThat(e).hasMessageThat().contains("exceeded maximum type specifier recursion depth");
+ }
+
+ @Test
+ public void parse_errors(@TestParameter ParseErrorTestCase testCase) {
+ IllegalArgumentException e =
+ assertThrows(IllegalArgumentException.class, () -> TypeDecl.parse(testCase.input));
+ assertThat(e).hasMessageThat().contains(testCase.expectedMessageSubstring);
+ }
+
+ private enum ParseErrorTestCase {
+ EMPTY("", "missing identifier at position 0"),
+ TRAILING_CHARACTERS("int int", "unexpected character 'i' at position 4 in \"int int\""),
+ UNEXPECTED_CLOSING_BRACKET("int>", "unexpected character '>' at position 3 in \"int>\""),
+ TRAILING_DOT(".foo.", "unexpected end of input"),
+ CONSECUTIVE_DOTS("..foo", "identifier is expected, but '.' was found at position 1"),
+ EMPTY_TYPE_PARAM("~", "unexpected end of input"),
+ DIGIT_TYPE_PARAM(
+ "~1",
+ "invalid type parameter identifier '1' at position 1, must be a single character from A-Z"),
+ LOWERCASE_TYPE_PARAM(
+ "~t",
+ "invalid type parameter identifier 't' at position 1, must be a single character from A-Z"),
+ TYPE_PARAM_FOLLOWED_BY_NUMERIC(
+ "~T1",
+ "invalid type parameter identifier '1' at position 2, must be a single character from A-Z"),
+ TYPE_PARAM_FOLLOWED_BY_UNDERSCORE(
+ "~T_",
+ "invalid type parameter identifier '_' at position 2, must be a single character from A-Z"),
+ TYPE_PARAM_FOLLOWED_BY_LOWERCASE(
+ "~Telem",
+ "invalid type parameter identifier 'e' at position 2, must be a single character from A-Z"),
+ MULTI_CHAR_TYPE_PARAM(
+ "~elem",
+ "invalid type parameter identifier 'e' at position 1, must be a single character from A-Z"),
+ WHITESPACE_IN_IDENTIFIER(
+ "google. protobuf.StringValue", "identifier is expected, but ' ' was found at position 7"),
+ WHITESPACE_BEFORE_DOT(
+ "google .protobuf.StringValue",
+ "unexpected character '.' at position 7 in \"google .protobuf.StringValue\""),
+ EXTRA_CLOSING_BRACKET("list>", "unexpected character '>' at position 9 in \"list>\""),
+ CONSECUTIVE_OPENING_BRACKETS("list<", "missing identifier at position 5"),
+ TRAILING_COMMA("map", "identifier is expected, but '>' was found at position 11"),
+ EMPTY_GENERIC_PARAM("map<, int>", "identifier is expected, but ',' was found at position 4"),
+ UNFINISHED_GENERIC("list<", "missing identifier at position 5"),
+ TRAILING_COMMA_GENERIC("map", "identifier is expected, but '>' was found at position 9"),
+ UNCLOSED_GENERIC("map' at position 15"),
+ ;
+
+ private final String input;
+ private final String expectedMessageSubstring;
+
+ ParseErrorTestCase(String input, String expectedMessageSubstring) {
+ this.input = input;
+ this.expectedMessageSubstring = expectedMessageSubstring;
+ }
+ }
+}
diff --git a/cel_android_rules.bzl b/cel_android_rules.bzl
index 5a94a7ef5..9bd2fd8bc 100644
--- a/cel_android_rules.bzl
+++ b/cel_android_rules.bzl
@@ -33,11 +33,13 @@ def cel_android_library(name, **kwargs):
# By default, set visibility to android_allow_list, unless if overridden at the call site.
provided_visibility_or_default = kwargs.get("visibility", ["//:android_allow_list"])
- filtered_kwargs = {k: v for k, v in kwargs.items() if k != "visibility"}
+ provided_compatible_with_or_default = kwargs.get("compatible_with", [])
+ filtered_kwargs = {k: v for k, v in kwargs.items() if k not in ["visibility", "compatible_with"]}
android_library(
name = name,
visibility = provided_visibility_or_default,
+ compatible_with = provided_compatible_with_or_default,
javacopts = all_javacopts,
**filtered_kwargs
)
diff --git a/checker/src/main/java/dev/cel/checker/CelCheckerBuilder.java b/checker/src/main/java/dev/cel/checker/CelCheckerBuilder.java
index e19cf5b70..b14782e27 100644
--- a/checker/src/main/java/dev/cel/checker/CelCheckerBuilder.java
+++ b/checker/src/main/java/dev/cel/checker/CelCheckerBuilder.java
@@ -35,6 +35,9 @@ public interface CelCheckerBuilder {
@CanIgnoreReturnValue
CelCheckerBuilder setOptions(CelOptions options);
+ /** Retrieves the currently configured {@link CelOptions} in the builder. */
+ CelOptions options();
+
/**
* Set the {@link CelContainer} to use as the namespace for resolving CEL expression variables and
* functions.
@@ -152,14 +155,20 @@ public interface CelCheckerBuilder {
@CanIgnoreReturnValue
CelCheckerBuilder addFileTypes(FileDescriptorSet fileDescriptorSet);
- /** Enable or disable the standard CEL library functions and variables */
+ /**
+ * Enable or disable the standard CEL library functions and variables.
+ *
+ * @deprecated Use {@link #setStandardDeclarations(CelStandardDeclarations)} to configure or
+ * subset the standard environment. Use {@link CelStandardDeclarations#EMPTY} to disable all
+ * standard declarations.
+ */
+ @Deprecated
@CanIgnoreReturnValue
CelCheckerBuilder setStandardEnvironmentEnabled(boolean value);
/**
* Override the standard declarations for the type-checker. This can be used to subset the
- * standard environment to only expose the desired declarations to the type-checker. {@link
- * #setStandardEnvironmentEnabled(boolean)} must be set to false for this to take effect.
+ * standard environment to only expose the desired declarations to the type-checker.
*/
@CanIgnoreReturnValue
CelCheckerBuilder setStandardDeclarations(CelStandardDeclarations standardDeclarations);
diff --git a/checker/src/main/java/dev/cel/checker/CelCheckerLegacyImpl.java b/checker/src/main/java/dev/cel/checker/CelCheckerLegacyImpl.java
index df8a82f43..329725e42 100644
--- a/checker/src/main/java/dev/cel/checker/CelCheckerLegacyImpl.java
+++ b/checker/src/main/java/dev/cel/checker/CelCheckerLegacyImpl.java
@@ -162,10 +162,10 @@ public void accept(EnvVisitor envVisitor) {
private Env getEnv(Errors errors) {
Env env;
- if (standardEnvironmentEnabled) {
- env = Env.standard(errors, typeProvider, celOptions);
- } else if (overriddenStandardDeclarations != null) {
+ if (overriddenStandardDeclarations != null) {
env = Env.standard(overriddenStandardDeclarations, errors, typeProvider, celOptions);
+ } else if (standardEnvironmentEnabled) {
+ env = Env.standard(errors, typeProvider, celOptions);
} else {
env = Env.unconfigured(errors, typeProvider, celOptions);
}
@@ -202,6 +202,11 @@ public CelCheckerBuilder setOptions(CelOptions celOptions) {
return this;
}
+ @Override
+ public CelOptions options() {
+ return this.celOptions;
+ }
+
@Override
public CelCheckerBuilder setContainer(CelContainer container) {
checkNotNull(container);
@@ -354,6 +359,7 @@ public CelCheckerBuilder addFileTypes(FileDescriptorSet fileDescriptorSet) {
}
@Override
+ @Deprecated
public CelCheckerBuilder setStandardEnvironmentEnabled(boolean value) {
this.standardEnvironmentEnabled = value;
return this;
@@ -421,11 +427,6 @@ CelStandardDeclarations standardDeclarations() {
return this.standardDeclarations;
}
- @VisibleForTesting
- CelOptions options() {
- return this.celOptions;
- }
-
@VisibleForTesting
CelTypeProvider celTypeProvider() {
return this.celTypeProvider;
@@ -434,12 +435,6 @@ CelTypeProvider celTypeProvider() {
@Override
@CheckReturnValue
public CelCheckerLegacyImpl build() {
- if (standardEnvironmentEnabled && standardDeclarations != null) {
- throw new IllegalArgumentException(
- "setStandardEnvironmentEnabled must be set to false to override standard"
- + " declarations.");
- }
-
// Add libraries, such as extensions
ImmutableSet checkerLibraries = celCheckerLibraries.build();
checkerLibraries.forEach(celLibrary -> celLibrary.setCheckerOptions(this));
diff --git a/checker/src/main/java/dev/cel/checker/CelStandardDeclarations.java b/checker/src/main/java/dev/cel/checker/CelStandardDeclarations.java
index 12ad47c62..bd63c4279 100644
--- a/checker/src/main/java/dev/cel/checker/CelStandardDeclarations.java
+++ b/checker/src/main/java/dev/cel/checker/CelStandardDeclarations.java
@@ -15,9 +15,11 @@
package dev.cel.checker;
import static com.google.common.base.Preconditions.checkNotNull;
+import static com.google.common.collect.ImmutableMap.toImmutableMap;
import static com.google.common.collect.ImmutableSet.toImmutableSet;
import static java.util.Arrays.stream;
+import com.google.common.collect.ImmutableMap;
import com.google.common.collect.ImmutableSet;
import com.google.errorprone.annotations.CanIgnoreReturnValue;
import com.google.errorprone.annotations.Immutable;
@@ -31,6 +33,7 @@
import dev.cel.common.types.SimpleType;
import dev.cel.common.types.TypeParamType;
import dev.cel.common.types.TypeType;
+import java.util.Optional;
/**
* Standard declarations for CEL.
@@ -48,11 +51,15 @@ public final class CelStandardDeclarations {
private static final TypeParamType TYPE_PARAM_B = TypeParamType.create("B");
private static final MapType MAP_OF_AB = MapType.create(TYPE_PARAM_A, TYPE_PARAM_B);
+ /** An empty instance of {@link CelStandardDeclarations} with no functions or identifiers. */
+ public static final CelStandardDeclarations EMPTY =
+ new CelStandardDeclarations(ImmutableSet.of(), ImmutableSet.of());
+
private final ImmutableSet celFunctionDecls;
private final ImmutableSet celIdentDecls;
/** Enumeration of Standard Functions. */
- public enum StandardFunction {
+ public enum StandardFunction implements CelFunctionDecl.Declarer {
// Deprecated - use {@link #IN}
OLD_IN(
true,
@@ -598,6 +605,13 @@ public enum Size implements StandardOverload {
CelOverloadDecl.newMemberOverload("map_size", "map size", SimpleType.INT, MAP_OF_AB)),
;
+ private static final ImmutableMap ID_TO_ENUM =
+ stream(values()).collect(toImmutableMap(e -> e.celOverloadDecl().overloadId(), e -> e));
+
+ public static Optional fromOverloadId(String overloadId) {
+ return Optional.ofNullable(ID_TO_ENUM.get(overloadId));
+ }
+
private final CelOverloadDecl celOverloadDecl;
Size(CelOverloadDecl overloadDecl) {
@@ -1474,6 +1488,16 @@ public boolean isHeterogeneousComparison() {
public CelOverloadDecl celOverloadDecl() {
return this.celOverloadDecl;
}
+
+ /** Finds a Comparison by its overload ID. */
+ public static Optional fromOverloadId(String overloadId) {
+ for (Comparison c : values()) {
+ if (c.celOverloadDecl().overloadId().equals(overloadId)) {
+ return Optional.of(c);
+ }
+ }
+ return Optional.empty();
+ }
}
private Overload() {}
@@ -1484,6 +1508,7 @@ private CelFunctionDecl withOverloads(Iterable overloads) {
return newCelFunctionDecl(functionName, ImmutableSet.copyOf(overloads));
}
+ @Override
public CelFunctionDecl functionDecl() {
return celFunctionDecl;
}
@@ -1559,8 +1584,14 @@ public CelIdentDecl identDecl() {
/** General interface for defining a standard function overload. */
@Immutable
- public interface StandardOverload {
+ public interface StandardOverload extends CelFunctionDecl.Declarer {
CelOverloadDecl celOverloadDecl();
+
+ @Override
+ default CelFunctionDecl functionDecl() {
+ // TODO: Remove default keyword by implementing this for all standard overloads
+ throw new UnsupportedOperationException("Unimplemented");
+ }
}
/** Set of all standard function names. */
diff --git a/checker/src/main/java/dev/cel/checker/Types.java b/checker/src/main/java/dev/cel/checker/Types.java
index 4cc502cdf..f9b82ecb7 100644
--- a/checker/src/main/java/dev/cel/checker/Types.java
+++ b/checker/src/main/java/dev/cel/checker/Types.java
@@ -205,6 +205,19 @@ private static boolean isTypeParam(CelType type) {
return type.kind().equals(CelKind.TYPE_PARAM);
}
+ /** Tests whether the {@code type} contains any type params directly or transitively. */
+ private static boolean hasTypeParam(CelType type) {
+ if (isTypeParam(type)) {
+ return true;
+ }
+ for (CelType param : type.parameters()) {
+ if (hasTypeParam(param)) {
+ return true;
+ }
+ }
+ return false;
+ }
+
/** Returns the more general of two types which are known to unify. */
public static CelType mostGeneral(CelType type1, CelType type2) {
return isEqualOrLessSpecific(type1, type2) ? type1 : type2;
@@ -332,8 +345,21 @@ private static boolean internalIsAssignable(
switch (type1.kind()) {
case TYPE:
- // A type is a type is a type, any additional parameterization of the type cannot affect
- // method resolution or assignability.
+ if (!(type1 instanceof TypeType) || !(type2 instanceof TypeType)) {
+ return type2.isAssignableFrom(type1);
+ }
+ TypeType fromType = (TypeType) type1;
+ TypeType toType = (TypeType) type2;
+ // If either type contains a type parameter (e.g., type(T) in foo(data, type(T)) -> T),
+ // delegate to inner type unification to bind or validate type parameter substitutions.
+ // Returns true if the inner types structurally match, unify with an unbound type param,
+ // or conform to an existing binding in 'subs'. Returns false on structural/kind mismatches
+ // (e.g., int vs list(T)), occurs-check cycles, or conflicting type param bindings.
+
+ if (hasTypeParam(fromType.type()) || hasTypeParam(toType.type())) {
+ return internalIsAssignable(subs, fromType.type(), toType.type());
+ }
+ // Concrete types are coassignable in CEL (e.g., type(1) == type("a"), type([1]) == list).
return true;
case OPAQUE:
case LIST:
diff --git a/checker/src/test/java/dev/cel/checker/BUILD.bazel b/checker/src/test/java/dev/cel/checker/BUILD.bazel
index 1821a5d85..22b70210d 100644
--- a/checker/src/test/java/dev/cel/checker/BUILD.bazel
+++ b/checker/src/test/java/dev/cel/checker/BUILD.bazel
@@ -1,9 +1,11 @@
load("@rules_java//java:defs.bzl", "java_library")
load("//:testing.bzl", "junit4_test_suites")
-package(default_applicable_licenses = [
- "//:license",
-])
+package(
+ default_applicable_licenses = [
+ "//:license",
+ ],
+)
java_library(
name = "tests",
@@ -11,8 +13,6 @@ java_library(
srcs = glob(["*Test.java"]),
resources = ["//checker/src/test/resources:baselines"],
deps = [
- # "//java/com/google/testing/testsize:annotations",
- "//:auto_value",
"//checker",
"//checker:cel_ident_decl",
"//checker:checker_builder",
@@ -42,9 +42,11 @@ java_library(
"//common/types:type_providers",
"//compiler",
"//compiler:compiler_builder",
+ # "//java/com/google/testing/testsize:annotations",
"//parser:macro",
"//testing:adorner",
"//testing:cel_baseline_test_case",
+ "//:auto_value",
"@maven//:junit_junit",
"@maven//:com_google_testparameterinjector_test_parameter_injector",
"//:java_truth",
diff --git a/checker/src/test/java/dev/cel/checker/CelCheckerLegacyImplTest.java b/checker/src/test/java/dev/cel/checker/CelCheckerLegacyImplTest.java
index c0c54381d..92a70c2d6 100644
--- a/checker/src/test/java/dev/cel/checker/CelCheckerLegacyImplTest.java
+++ b/checker/src/test/java/dev/cel/checker/CelCheckerLegacyImplTest.java
@@ -63,7 +63,7 @@ public void toCheckerBuilder_isImmutable() {
public void toCheckerBuilder_singularFields_copied() {
CelStandardDeclarations subsetDecls =
CelStandardDeclarations.newBuilder().includeFunctions(StandardFunction.BOOL).build();
- CelOptions celOptions = CelOptions.current().enableTimestampEpoch(true).build();
+ CelOptions celOptions = CelOptions.current().build();
CelContainer celContainer = CelContainer.ofName("foo");
CelType expectedResultType = SimpleType.BOOL;
CelTypeProvider customTypeProvider =
diff --git a/checker/src/test/java/dev/cel/checker/CelStandardDeclarationsTest.java b/checker/src/test/java/dev/cel/checker/CelStandardDeclarationsTest.java
index 17a7212a1..f867728b0 100644
--- a/checker/src/test/java/dev/cel/checker/CelStandardDeclarationsTest.java
+++ b/checker/src/test/java/dev/cel/checker/CelStandardDeclarationsTest.java
@@ -86,24 +86,63 @@ public void standardDeclaration_moreThanOneIdentifierFilterSet_throws(
}
@Test
- public void compiler_standardEnvironmentEnabled_throwsWhenOverridingDeclarations() {
- IllegalArgumentException e =
- assertThrows(
- IllegalArgumentException.class,
- () ->
- CelCompilerFactory.standardCelCompilerBuilder()
- .setStandardEnvironmentEnabled(true)
- .setStandardDeclarations(
- CelStandardDeclarations.newBuilder()
- .includeFunctions(StandardFunction.ADD, StandardFunction.SUBTRACT)
- .build())
- .build());
+ public void compiler_setStandardDeclarations_overridesDefaultStandardEnvironment()
+ throws Exception {
+ CelCompiler compiler =
+ CelCompilerFactory.standardCelCompilerBuilder()
+ .setStandardDeclarations(
+ CelStandardDeclarations.newBuilder()
+ .includeFunctions(StandardFunction.ADD)
+ .build())
+ .build();
- assertThat(e)
- .hasMessageThat()
- .contains(
- "setStandardEnvironmentEnabled must be set to false to override standard"
- + " declarations.");
+ assertThat(compiler.compile("1 + 1").hasError()).isFalse();
+ assertThat(compiler.compile("1 - 1").hasError()).isTrue();
+ }
+
+ @Test
+ public void compiler_setStandardDeclarations_withStandardEnvironmentExplicitlyEnabled()
+ throws Exception {
+ CelCompiler compiler =
+ CelCompilerFactory.standardCelCompilerBuilder()
+ .setStandardEnvironmentEnabled(true)
+ .setStandardDeclarations(
+ CelStandardDeclarations.newBuilder()
+ .includeFunctions(StandardFunction.ADD)
+ .build())
+ .build();
+
+ assertThat(compiler.compile("1 + 1").hasError()).isFalse();
+ assertThat(compiler.compile("1 - 1").hasError()).isTrue();
+ }
+
+ @Test
+ public void compiler_setStandardDeclarations_withStandardEnvironmentExplicitlyDisabled()
+ throws Exception {
+ CelCompiler compiler =
+ CelCompilerFactory.standardCelCompilerBuilder()
+ .setStandardEnvironmentEnabled(false)
+ .setStandardDeclarations(
+ CelStandardDeclarations.newBuilder()
+ .includeFunctions(StandardFunction.ADD)
+ .build())
+ .build();
+
+ assertThat(compiler.compile("1 + 1").hasError()).isFalse();
+ assertThat(compiler.compile("1 - 1").hasError()).isTrue();
+ }
+
+ @Test
+ public void compiler_setStandardDeclarations_emptyDisablesAllStandardDeclarations()
+ throws Exception {
+ CelCompiler compiler =
+ CelCompilerFactory.standardCelCompilerBuilder()
+ .setStandardDeclarations(CelStandardDeclarations.EMPTY)
+ .build();
+
+ assertThat(compiler.compile("1 + 1").hasError()).isTrue();
+ assertThat(compiler.compile("1 - 1").hasError()).isTrue();
+ assertThat(compiler.compile("size([1])").hasError()).isTrue();
}
@Test
diff --git a/checker/src/test/java/dev/cel/checker/ExprCheckerTest.java b/checker/src/test/java/dev/cel/checker/ExprCheckerTest.java
index d5d5d9a3a..846201d32 100644
--- a/checker/src/test/java/dev/cel/checker/ExprCheckerTest.java
+++ b/checker/src/test/java/dev/cel/checker/ExprCheckerTest.java
@@ -517,6 +517,71 @@ public void jsonType() throws Exception {
runTest();
}
+ @Test
+ public void jsonTypeNullConstruction() throws Exception {
+ // Ok
+ source = "google.protobuf.Value{null_value: google.protobuf.NullValue.NULL_VALUE}";
+ runTest();
+
+ // Error
+ source = "google.protobuf.Value{null_value: null}";
+ runTest();
+
+ // Ok
+ source = "cel.expr.conformance.proto3.TestAllTypes{single_value: null}";
+ runTest();
+
+ // Ok but not expected (int coerced to double/json number 0.0)
+ source =
+ "cel.expr.conformance.proto3.TestAllTypes{single_value:"
+ + " google.protobuf.NullValue.NULL_VALUE}";
+ runTest();
+
+ // Error
+ source = "cel.expr.conformance.proto3.TestAllTypes{null_value: null}";
+ runTest();
+
+ // Ok
+ source =
+ "cel.expr.conformance.proto3.TestAllTypes{null_value:"
+ + " google.protobuf.NullValue.NULL_VALUE}";
+ runTest();
+ }
+
+ @Test
+ public void jsonTypeNullAccess() throws Exception {
+ source = "google.protobuf.Value{null_value: google.protobuf.NullValue.NULL_VALUE} == null";
+ runTest();
+
+ source = "cel.expr.conformance.proto3.TestAllTypes{single_value: null}.single_value == null";
+ runTest();
+
+ source =
+ "cel.expr.conformance.proto3.TestAllTypes{single_value:"
+ + " google.protobuf.NullValue.NULL_VALUE}.single_value == null";
+ runTest();
+
+ // Error
+ source =
+ "cel.expr.conformance.proto3.TestAllTypes{null_value:"
+ + " google.protobuf.NullValue.NULL_VALUE}.null_value == null";
+ runTest();
+
+ // Ok
+ source =
+ "cel.expr.conformance.proto3.TestAllTypes{null_value:"
+ + " google.protobuf.NullValue.NULL_VALUE}.null_value == 0";
+ runTest();
+
+ // Error
+ source = "google.protobuf.NullValue.NULL_VALUE == null";
+ runTest();
+
+ // Ok
+ source = "google.protobuf.NullValue.NULL_VALUE == 0";
+ runTest();
+ }
+
// Call Style and User Functions
// =============================
diff --git a/checker/src/test/java/dev/cel/checker/TypesTest.java b/checker/src/test/java/dev/cel/checker/TypesTest.java
index 960ebec3f..786e50668 100644
--- a/checker/src/test/java/dev/cel/checker/TypesTest.java
+++ b/checker/src/test/java/dev/cel/checker/TypesTest.java
@@ -18,10 +18,21 @@
import dev.cel.expr.Type;
import dev.cel.expr.Type.PrimitiveType;
+import dev.cel.common.CelAbstractSyntaxTree;
+import dev.cel.common.CelFunctionDecl;
+import dev.cel.common.CelOverloadDecl;
import dev.cel.common.types.CelKind;
import dev.cel.common.types.CelProtoTypes;
import dev.cel.common.types.CelType;
+import dev.cel.common.types.ListType;
+import dev.cel.common.types.MapType;
+import dev.cel.common.types.NullableType;
+import dev.cel.common.types.OptionalType;
import dev.cel.common.types.SimpleType;
+import dev.cel.common.types.TypeParamType;
+import dev.cel.common.types.TypeType;
+import dev.cel.compiler.CelCompiler;
+import dev.cel.compiler.CelCompilerFactory;
import java.util.HashMap;
import java.util.Map;
import org.junit.Test;
@@ -54,6 +65,373 @@ public void isAssignable_usingCustomTypes() {
assertThat(Types.isAssignable(subs, customType, intType)).isNull();
}
+ @Test
+ public void isAssignable_typeType_concreteTypes_legacyCoassignability() {
+ Map subs = new HashMap<>();
+ CelType intType = TypeType.create(SimpleType.INT);
+ CelType stringType = TypeType.create(SimpleType.STRING);
+
+ Map result1 = Types.isAssignable(subs, intType, stringType);
+ Map result2 = Types.isAssignable(subs, stringType, intType);
+
+ // Concrete types are coassignable in CEL (e.g. for equality comparison type(1) == type("a"))
+ assertThat(result1).isEmpty();
+ assertThat(result2).isEmpty();
+ }
+
+ @Test
+ public void isAssignable_typeType_mapContainerErasure() {
+ Map subs = new HashMap<>();
+ CelType mapIntUint = TypeType.create(MapType.create(SimpleType.INT, SimpleType.UINT));
+ CelType mapDynDyn = TypeType.create(MapType.create(SimpleType.DYN, SimpleType.DYN));
+
+ Map result = Types.isAssignable(subs, mapIntUint, mapDynDyn);
+
+ // type({1: 2u}) == map
+ assertThat(result).isEmpty();
+ }
+
+ @Test
+ public void isAssignable_typeType_listContainerErasure() {
+ Map subs = new HashMap<>();
+ CelType listInt = TypeType.create(ListType.create(SimpleType.INT));
+ CelType listDyn = TypeType.create(ListType.create(SimpleType.DYN));
+
+ Map result = Types.isAssignable(subs, listInt, listDyn);
+
+ // type([1]) == list
+ assertThat(result).isEmpty();
+ }
+
+ @Test
+ public void isAssignable_typeType_typeParamTarget_bindsConcreteType() {
+ Map subs = new HashMap<>();
+ TypeParamType typeParamT = TypeParamType.create("T");
+ CelType fromType = TypeType.create(SimpleType.INT);
+ CelType toType = TypeType.create(typeParamT);
+
+ Map result = Types.isAssignable(subs, fromType, toType);
+
+ assertThat(result).containsExactly(typeParamT, SimpleType.INT);
+ }
+
+ @Test
+ public void isAssignable_typeType_typeParamSource_bindsConcreteType() {
+ Map subs = new HashMap<>();
+ TypeParamType typeParamT = TypeParamType.create("T");
+ CelType fromType = TypeType.create(typeParamT);
+ CelType toType = TypeType.create(SimpleType.INT);
+
+ Map result = Types.isAssignable(subs, fromType, toType);
+
+ assertThat(result).containsExactly(typeParamT, SimpleType.INT);
+ }
+
+ @Test
+ public void isAssignable_typeType_nestedTypeParam_unifies() {
+ Map subs = new HashMap<>();
+ TypeParamType typeParamT = TypeParamType.create("T");
+ TypeParamType typeParamR = TypeParamType.create("R");
+ CelType fromType = TypeType.create(typeParamT);
+ CelType toType = TypeType.create(TypeType.create(typeParamR));
+
+ // type(T) == type(type(R))
+ Map result = Types.isAssignable(subs, fromType, toType);
+
+ assertThat(result).containsExactly(typeParamT, TypeType.create(typeParamR));
+ }
+
+ @Test
+ public void isAssignable_typeType_deeplyNestedTypeParam_bindsConcreteType() {
+ Map subs = new HashMap<>();
+ TypeParamType typeParamT = TypeParamType.create("T");
+ CelType fromType = TypeType.create(TypeType.create(SimpleType.INT));
+ CelType toType = TypeType.create(TypeType.create(typeParamT));
+
+ Map result = Types.isAssignable(subs, fromType, toType);
+
+ assertThat(result).containsExactly(typeParamT, SimpleType.INT);
+ }
+
+ @Test
+ public void isAssignable_typeType_compositeListTypeParam_bindsConcreteType() {
+ Map subs = new HashMap<>();
+ TypeParamType typeParamT = TypeParamType.create("T");
+ CelType fromType = TypeType.create(ListType.create(SimpleType.INT));
+ CelType toType = TypeType.create(ListType.create(typeParamT));
+
+ Map result = Types.isAssignable(subs, fromType, toType);
+
+ assertThat(result).containsExactly(typeParamT, SimpleType.INT);
+ }
+
+ @Test
+ public void isAssignable_typeType_compositeMapTypeParam_bindsConcreteTypes() {
+ Map subs = new HashMap<>();
+ TypeParamType typeParamK = TypeParamType.create("K");
+ TypeParamType typeParamV = TypeParamType.create("V");
+ CelType fromType = TypeType.create(MapType.create(SimpleType.STRING, SimpleType.INT));
+ CelType toType = TypeType.create(MapType.create(typeParamK, typeParamV));
+
+ Map result = Types.isAssignable(subs, fromType, toType);
+
+ assertThat(result).containsExactly(typeParamK, SimpleType.STRING, typeParamV, SimpleType.INT);
+ }
+
+ @Test
+ public void isAssignable_typeType_nullableTypeParam_unifies() {
+ Map subs = new HashMap<>();
+ TypeParamType typeParamT = TypeParamType.create("T");
+ CelType fromType = TypeType.create(NullableType.create(SimpleType.INT));
+ CelType toType = TypeType.create(NullableType.create(typeParamT));
+
+ Map result = Types.isAssignable(subs, fromType, toType);
+
+ assertThat(result)
+ .containsExactly(NullableType.create(typeParamT), NullableType.create(SimpleType.INT));
+ }
+
+ @Test
+ public void isAssignable_typeType_optionalTypeParam_unifies() {
+ Map subs = new HashMap<>();
+ TypeParamType typeParamT = TypeParamType.create("T");
+ CelType fromType = TypeType.create(OptionalType.create(SimpleType.INT));
+ CelType toType = TypeType.create(OptionalType.create(typeParamT));
+
+ Map result = Types.isAssignable(subs, fromType, toType);
+
+ assertThat(result).containsExactly(typeParamT, SimpleType.INT);
+ }
+
+ @Test
+ public void isAssignable_typeType_incompatibleTypeParams_returnsNull() {
+ Map subs = new HashMap<>();
+ TypeParamType typeParamT = TypeParamType.create("T");
+ CelType fromType = TypeType.create(ListType.create(typeParamT));
+ CelType toType = TypeType.create(SimpleType.INT);
+
+ Map result = Types.isAssignable(subs, fromType, toType);
+
+ assertThat(result).isNull();
+ }
+
+ @Test
+ public void isAssignable_typeType_conflictingBoundTypeParam_returnsNull() {
+ Map subs = new HashMap<>();
+ TypeParamType typeParamT = TypeParamType.create("T");
+ subs.put(typeParamT, SimpleType.STRING);
+ CelType fromType = TypeType.create(typeParamT);
+ CelType toType = TypeType.create(SimpleType.INT);
+
+ Map result = Types.isAssignable(subs, fromType, toType);
+
+ assertThat(result).isNull();
+ }
+
+ @Test
+ public void isAssignable_typeType_occursCheck_failsOnSelfReference() {
+ Map subs = new HashMap<>();
+ TypeParamType typeParamT = TypeParamType.create("T");
+ CelType fromType = TypeType.create(typeParamT);
+ CelType toType = TypeType.create(TypeType.create(typeParamT));
+
+ // Occurs check: T = type(T) is cyclic and must fail
+ Map result = Types.isAssignable(subs, fromType, toType);
+
+ assertThat(result).isNull();
+ }
+
+ @Test
+ public void isAssignable_typeType_occursCheck_failsOnTransitiveCycle() {
+ Map subs = new HashMap<>();
+ TypeParamType typeParamT = TypeParamType.create("T");
+ TypeParamType typeParamR = TypeParamType.create("R");
+ subs.put(typeParamT, TypeType.create(typeParamR));
+ // Trying to assign type(R) to type(T) would produce R = type(R) transitively through T
+ CelType fromType = TypeType.create(typeParamR);
+ CelType toType = TypeType.create(TypeType.create(typeParamT));
+
+ Map result = Types.isAssignable(subs, fromType, toType);
+
+ assertThat(result).isNull();
+ }
+
+ @Test
+ public void isAssignable_typeType_occursCheck_mapTypeParam_to_typeParam() {
+ Map subs = new HashMap<>();
+ TypeParamType typeParamT = TypeParamType.create("T");
+ CelType fromType = TypeType.create(MapType.create(SimpleType.STRING, typeParamT));
+ CelType toType = TypeType.create(typeParamT);
+
+ Map result = Types.isAssignable(subs, fromType, toType);
+
+ assertThat(result).isNull();
+ }
+
+ @Test
+ public void isAssignable_typeType_occursCheck_typeParam_to_mapTypeParam() {
+ Map subs = new HashMap<>();
+ TypeParamType typeParamT = TypeParamType.create("T");
+ CelType fromType = TypeType.create(typeParamT);
+ CelType toType = TypeType.create(MapType.create(SimpleType.STRING, typeParamT));
+
+ Map result = Types.isAssignable(subs, fromType, toType);
+
+ assertThat(result).isNull();
+ }
+
+ @Test
+ public void isAssignable_typeType_occursCheck_mapTypeParamInKey_to_typeParam() {
+ Map subs = new HashMap<>();
+ TypeParamType typeParamT = TypeParamType.create("T");
+ CelType fromType = TypeType.create(MapType.create(typeParamT, SimpleType.STRING));
+ CelType toType = TypeType.create(typeParamT);
+
+ Map result = Types.isAssignable(subs, fromType, toType);
+
+ assertThat(result).isNull();
+ }
+
+ @Test
+ public void isAssignable_typeType_occursCheck_listTypeParam_to_typeParam() {
+ Map subs = new HashMap<>();
+ TypeParamType typeParamT = TypeParamType.create("T");
+ CelType fromType = TypeType.create(ListType.create(typeParamT));
+ CelType toType = TypeType.create(typeParamT);
+
+ Map result = Types.isAssignable(subs, fromType, toType);
+
+ assertThat(result).isNull();
+ }
+
+ @Test
+ public void isAssignable_typeType_occursCheck_optionalTypeParam_to_typeParam() {
+ Map subs = new HashMap<>();
+ TypeParamType typeParamT = TypeParamType.create("T");
+ CelType fromType = TypeType.create(OptionalType.create(typeParamT));
+ CelType toType = TypeType.create(typeParamT);
+
+ Map result = Types.isAssignable(subs, fromType, toType);
+
+ assertThat(result).isNull();
+ }
+
+ @Test
+ public void compiler_typeParamInTypeType_resolvesReturnTypeInt() throws Exception {
+ TypeParamType typeParamT = TypeParamType.create("T");
+ CelCompiler celCompiler =
+ CelCompilerFactory.standardCelCompilerBuilder()
+ .addFunctionDeclarations(
+ CelFunctionDecl.newFunctionDeclaration(
+ "cast",
+ CelOverloadDecl.newGlobalOverload(
+ "cast_t", typeParamT, SimpleType.DYN, TypeType.create(typeParamT))))
+ .build();
+
+ CelAbstractSyntaxTree ast = celCompiler.compile("cast('hello', int)").getAst();
+
+ assertThat(ast.getResultType()).isEqualTo(SimpleType.INT);
+ }
+
+ @Test
+ public void compiler_typeParamInTypeType_resolvesReturnTypeString() throws Exception {
+ TypeParamType typeParamT = TypeParamType.create("T");
+ CelCompiler celCompiler =
+ CelCompilerFactory.standardCelCompilerBuilder()
+ .addFunctionDeclarations(
+ CelFunctionDecl.newFunctionDeclaration(
+ "cast",
+ CelOverloadDecl.newGlobalOverload(
+ "cast_t", typeParamT, SimpleType.DYN, TypeType.create(typeParamT))))
+ .build();
+
+ CelAbstractSyntaxTree ast = celCompiler.compile("cast(123, string)").getAst();
+
+ assertThat(ast.getResultType()).isEqualTo(SimpleType.STRING);
+ }
+
+ @Test
+ public void compiler_typeParamInCompositeTypeType_resolvesReturnType() throws Exception {
+ TypeParamType typeParamT = TypeParamType.create("T");
+ CelCompiler celCompiler =
+ CelCompilerFactory.standardCelCompilerBuilder()
+ .addFunctionDeclarations(
+ CelFunctionDecl.newFunctionDeclaration(
+ "first_elem_type",
+ CelOverloadDecl.newGlobalOverload(
+ "first_elem_type_overload",
+ typeParamT,
+ SimpleType.DYN,
+ TypeType.create(ListType.create(typeParamT)))))
+ .build();
+
+ CelAbstractSyntaxTree ast = celCompiler.compile("first_elem_type('data', type([1]))").getAst();
+
+ assertThat(ast.getResultType()).isEqualTo(SimpleType.INT);
+ }
+
+ @Test
+ public void compiler_typeComparison_mapType_succeeds() throws Exception {
+ CelCompiler celCompiler = CelCompilerFactory.standardCelCompilerBuilder().build();
+
+ CelAbstractSyntaxTree ast = celCompiler.compile("type({}) == map").getAst();
+
+ assertThat(ast.getResultType()).isEqualTo(SimpleType.BOOL);
+ }
+
+ @Test
+ public void compiler_typeComparison_compositeTypes_succeeds() throws Exception {
+ CelCompiler celCompiler = CelCompilerFactory.standardCelCompilerBuilder().build();
+
+ CelAbstractSyntaxTree ast =
+ celCompiler.compile("list == type([1]) && map == type({1:2u})").getAst();
+
+ assertThat(ast.getResultType()).isEqualTo(SimpleType.BOOL);
+ }
+
+ @Test
+ public void compiler_typeComparison_differentTypesEqual_succeeds() throws Exception {
+ CelCompiler celCompiler = CelCompilerFactory.standardCelCompilerBuilder().build();
+
+ CelAbstractSyntaxTree ast = celCompiler.compile("type(1) == type('a')").getAst();
+
+ assertThat(ast.getResultType()).isEqualTo(SimpleType.BOOL);
+ }
+
+ @Test
+ public void compiler_typeComparison_differentTypesNotEqual_succeeds() throws Exception {
+ CelCompiler celCompiler = CelCompilerFactory.standardCelCompilerBuilder().build();
+
+ CelAbstractSyntaxTree ast = celCompiler.compile("type(1) != uint").getAst();
+
+ assertThat(ast.getResultType()).isEqualTo(SimpleType.BOOL);
+ }
+
+ @Test
+ public void compiler_typeComparison_type1NotEqualsType1u_succeeds() throws Exception {
+ CelCompiler celCompiler = CelCompilerFactory.standardCelCompilerBuilder().build();
+
+ CelAbstractSyntaxTree ast = celCompiler.compile("type(1) != type(1u)").getAst();
+
+ assertThat(ast.getResultType()).isEqualTo(SimpleType.BOOL);
+ }
+
+ @Test
+ public void compiler_typeParamEquality_unifiesTypeParams() throws Exception {
+ TypeParamType typeParamT = TypeParamType.create("T");
+ TypeParamType typeParamR = TypeParamType.create("R");
+ CelCompiler celCompiler =
+ CelCompilerFactory.standardCelCompilerBuilder()
+ .addVar("x", TypeType.create(typeParamT))
+ .addVar("y", TypeType.create(TypeType.create(typeParamR)))
+ .build();
+
+ // type(T) == type(type(R))
+ CelAbstractSyntaxTree ast = celCompiler.compile("x == y").getAst();
+
+ assertThat(ast.getResultType()).isEqualTo(SimpleType.BOOL);
+ }
+
private static final class CustomCelType extends CelType {
@Override
diff --git a/checker/src/test/resources/jsonTypeNullAccess.baseline b/checker/src/test/resources/jsonTypeNullAccess.baseline
new file mode 100644
index 000000000..834b8fde8
--- /dev/null
+++ b/checker/src/test/resources/jsonTypeNullAccess.baseline
@@ -0,0 +1,54 @@
+Source: google.protobuf.Value{null_value: google.protobuf.NullValue.NULL_VALUE} == null
+=====>
+_==_(
+ google.protobuf.Value{
+ null_value:google.protobuf.NullValue.NULL_VALUE~int^google.protobuf.NullValue.NULL_VALUE
+ }~dyn^google.protobuf.Value,
+ null~null
+)~bool^equals
+
+Source: cel.expr.conformance.proto3.TestAllTypes{single_value: null}.single_value == null
+=====>
+_==_(
+ cel.expr.conformance.proto3.TestAllTypes{
+ single_value:null~null
+ }~cel.expr.conformance.proto3.TestAllTypes^cel.expr.conformance.proto3.TestAllTypes.single_value~dyn,
+ null~null
+)~bool^equals
+
+Source: cel.expr.conformance.proto3.TestAllTypes{single_value: google.protobuf.NullValue.NULL_VALUE}.single_value == null
+=====>
+_==_(
+ cel.expr.conformance.proto3.TestAllTypes{
+ single_value:google.protobuf.NullValue.NULL_VALUE~int^google.protobuf.NullValue.NULL_VALUE
+ }~cel.expr.conformance.proto3.TestAllTypes^cel.expr.conformance.proto3.TestAllTypes.single_value~dyn,
+ null~null
+)~bool^equals
+
+Source: cel.expr.conformance.proto3.TestAllTypes{null_value: google.protobuf.NullValue.NULL_VALUE}.null_value == null
+=====>
+ERROR: test_location:1:103: found no matching overload for '_==_' applied to '(int, null)' (candidates: (%A0, %A0))
+ | cel.expr.conformance.proto3.TestAllTypes{null_value: google.protobuf.NullValue.NULL_VALUE}.null_value == null
+ | ......................................................................................................^
+
+Source: cel.expr.conformance.proto3.TestAllTypes{null_value: google.protobuf.NullValue.NULL_VALUE}.null_value == 0
+=====>
+_==_(
+ cel.expr.conformance.proto3.TestAllTypes{
+ null_value:google.protobuf.NullValue.NULL_VALUE~int^google.protobuf.NullValue.NULL_VALUE
+ }~cel.expr.conformance.proto3.TestAllTypes^cel.expr.conformance.proto3.TestAllTypes.null_value~int,
+ 0~int
+)~bool^equals
+
+Source: google.protobuf.NullValue.NULL_VALUE == null
+=====>
+ERROR: test_location:1:38: found no matching overload for '_==_' applied to '(int, null)' (candidates: (%A0, %A0))
+ | google.protobuf.NullValue.NULL_VALUE == null
+ | .....................................^
+
+Source: google.protobuf.NullValue.NULL_VALUE == 0
+=====>
+_==_(
+ google.protobuf.NullValue.NULL_VALUE~int^google.protobuf.NullValue.NULL_VALUE,
+ 0~int
+)~bool^equals
\ No newline at end of file
diff --git a/checker/src/test/resources/jsonTypeNullConstruction.baseline b/checker/src/test/resources/jsonTypeNullConstruction.baseline
new file mode 100644
index 000000000..5b9b211a8
--- /dev/null
+++ b/checker/src/test/resources/jsonTypeNullConstruction.baseline
@@ -0,0 +1,35 @@
+Source: google.protobuf.Value{null_value: google.protobuf.NullValue.NULL_VALUE}
+=====>
+google.protobuf.Value{
+ null_value:google.protobuf.NullValue.NULL_VALUE~int^google.protobuf.NullValue.NULL_VALUE
+}~dyn^google.protobuf.Value
+
+Source: google.protobuf.Value{null_value: null}
+=====>
+ERROR: test_location:1:33: expected type of field 'null_value' is 'int' but provided type is 'null'
+ | google.protobuf.Value{null_value: null}
+ | ................................^
+
+Source: cel.expr.conformance.proto3.TestAllTypes{single_value: null}
+=====>
+cel.expr.conformance.proto3.TestAllTypes{
+ single_value:null~null
+}~cel.expr.conformance.proto3.TestAllTypes^cel.expr.conformance.proto3.TestAllTypes
+
+Source: cel.expr.conformance.proto3.TestAllTypes{single_value: google.protobuf.NullValue.NULL_VALUE}
+=====>
+cel.expr.conformance.proto3.TestAllTypes{
+ single_value:google.protobuf.NullValue.NULL_VALUE~int^google.protobuf.NullValue.NULL_VALUE
+}~cel.expr.conformance.proto3.TestAllTypes^cel.expr.conformance.proto3.TestAllTypes
+
+Source: cel.expr.conformance.proto3.TestAllTypes{null_value: null}
+=====>
+ERROR: test_location:1:52: expected type of field 'null_value' is 'int' but provided type is 'null'
+ | cel.expr.conformance.proto3.TestAllTypes{null_value: null}
+ | ...................................................^
+
+Source: cel.expr.conformance.proto3.TestAllTypes{null_value: google.protobuf.NullValue.NULL_VALUE}
+=====>
+cel.expr.conformance.proto3.TestAllTypes{
+ null_value:google.protobuf.NullValue.NULL_VALUE~int^google.protobuf.NullValue.NULL_VALUE
+}~cel.expr.conformance.proto3.TestAllTypes^cel.expr.conformance.proto3.TestAllTypes
\ No newline at end of file
diff --git a/codelab/README.md b/codelab/README.md
index f7d248b13..00d7f729e 100644
--- a/codelab/README.md
+++ b/codelab/README.md
@@ -50,7 +50,7 @@ The code for this codelab lives in the `codelab` folder of the cel-java repo. Th
Clone and cd into the repo:
```
-git clone git@github.com:google/cel-java.git
+git clone git@github.com:cel-expr/cel-java.git
cd cel-java
```
@@ -74,10 +74,10 @@ Tests run: 5, Failures: 5
Each exercise is laid out as `ExerciseN.java` and is accompanied by failing tests. Throughout this codelab, we will modify the main exercise code to make these tests pass.
-- Codelab code: https://github.com/google/cel-java/tree/main/codelab/src/main/codelab
-- Test code for the main codelab: https://github.com/google/cel-java/tree/main/codelab/src/test/codelab
-- Codelab solution code: https://github.com/google/cel-java/tree/main/codelab/src/main/codelab/solutions
-- Test code for the solution: https://github.com/google/cel-java/tree/main/codelab/src/test/codelab/solutions
+- Codelab code: https://github.com/cel-expr/cel-java/tree/main/codelab/src/main/codelab
+- Test code for the main codelab: https://github.com/cel-expr/cel-java/tree/main/codelab/src/test/codelab
+- Codelab solution code: https://github.com/cel-expr/cel-java/tree/main/codelab/src/main/codelab/solutions
+- Test code for the solution: https://github.com/cel-expr/cel-java/tree/main/codelab/src/test/codelab/solutions
We will also be using `google.rpc.context.AttributeContext` in [attribute_context.proto](https://github.com/googleapis/googleapis/blob/master/google/rpc/context/attribute_context.proto) to help with defining inputs for exercises.
@@ -140,7 +140,7 @@ private static final CelCompiler CEL_COMPILER =
// CelRuntime can also be initialized statically and cached just like the
// compiler.
private static final CelRuntime CEL_RUNTIME =
- CelRuntimeFactory.standardCelRuntimeBuilder()
+ CelRuntimeFactory.plannerRuntimeBuilder()
.setOptions(CEL_OPTIONS)
.build();
```
@@ -232,7 +232,7 @@ Copy the following into eval method:
Object eval(CelAbstractSyntaxTree ast) {
// Construct a CelRuntime instance
// CelRuntime is immutable just like the compiler and can be moved to a static final member.
- CelRuntime celRuntime = CelRuntimeFactory.standardCelRuntimeBuilder().build();
+ CelRuntime celRuntime = CelRuntimeFactory.plannerRuntimeBuilder().build();
try {
// Plan the program
@@ -314,7 +314,7 @@ Let's make the evaluation work now. Copy into the eval method:
* @throws IllegalArgumentException If the compiled expression in AST fails to evaluate.
*/
Object eval(CelAbstractSyntaxTree ast, Map parameterValues) {
- CelRuntime celRuntime = CelRuntimeFactory.standardCelRuntimeBuilder().build();
+ CelRuntime celRuntime = CelRuntimeFactory.plannerRuntimeBuilder().build();
try {
CelRuntime.Program program = celRuntime.createProgram(ast);
@@ -510,7 +510,7 @@ CelAbstractSyntaxTree compile(String expression) {
*/
Object eval(CelAbstractSyntaxTree ast, Map parameterValues) {
CelRuntime celRuntime =
- CelRuntimeFactory.standardCelRuntimeBuilder()
+ CelRuntimeFactory.plannerRuntimeBuilder()
// Provide the custom `contains` function implementation here.
.build();
@@ -590,7 +590,7 @@ Provide the function implementation to the runtime using the .addFunctionBinding
*/
Object eval(CelAbstractSyntaxTree ast, Map parameterValues) {
CelRuntime celRuntime =
- CelRuntimeFactory.standardCelRuntimeBuilder()
+ CelRuntimeFactory.plannerRuntimeBuilder()
.addFunctionBindings(
CelFunctionBinding.from(
"map_contains_key_value",
@@ -1136,7 +1136,7 @@ private static final CelCompiler CEL_COMPILER =
.addMessageTypes(AttributeContext.Request.getDescriptor())
.build();
private static final CelRuntime CEL_RUNTIME =
- CelRuntimeFactory.standardCelRuntimeBuilder()
+ CelRuntimeFactory.plannerRuntimeBuilder()
.addMessageTypes(AttributeContext.Request.getDescriptor())
.build();
```
@@ -1362,11 +1362,11 @@ public void optimize_commonSubexpressionElimination_success() throws Exception {
}
```
-CSE will rewrite this expression using a specialized internal function
-`cel.@block`. The first argument contain a list duplicate subexpressions
-and the second argument is the rewritten result expression that is semantically
-the same as the original expression. The subexpressions are lazily evaluated and
-memoized when accessed by index (e.g: `@index0`).
+CSE optimizes the expression by rewriting it to use a specialized internal
+function `cel.@block`. This function takes a list of duplicate subexpressions
+as its first argument, and a semantically equivalent rewritten expression as
+its second. The subexpressions are lazily evaluated and memoized when accessed
+by index (e.g., `@index0`).
Make the following changes in `Exercise8.java`:
@@ -1375,19 +1375,10 @@ private static final CelOptimizer CEL_OPTIMIZER =
CelOptimizerFactory.standardCelOptimizerBuilder(CEL_COMPILER, CEL_RUNTIME)
.addAstOptimizers(
ConstantFoldingOptimizer.getInstance(),
- SubexpressionOptimizer.newInstance(
- SubexpressionOptimizerOptions.newBuilder().enableCelBlock(true).build()))
+ SubexpressionOptimizer.getInstance())
.build();
```
-As seen here, the usage of `cel.block` must explicitly be enabled as it is
-only supported in CEL-Java as of now. Disabling `cel.block` will instead rewrite
-the AST using cascaded `cel.bind` macros. Prefer using the block format if
-possible as it is a more efficient format for evaluation.
-
-> [!CAUTION]
-> You MUST disable `cel.block` if you are targeting `cel-go` or `cel-cpp` for the runtime until its support has been added in those stacks.
-
Re-run the tests to confirm that they pass.
## Custom AST Validation
diff --git a/codelab/src/main/codelab/Exercise3.java b/codelab/src/main/codelab/Exercise3.java
index 77b57f339..1745920f8 100644
--- a/codelab/src/main/codelab/Exercise3.java
+++ b/codelab/src/main/codelab/Exercise3.java
@@ -27,8 +27,7 @@ final class Exercise3 {
private static final CelCompiler CEL_COMPILER =
CelCompilerFactory.standardCelCompilerBuilder().setResultType(SimpleType.BOOL).build();
- private static final CelRuntime CEL_RUNTIME =
- CelRuntimeFactory.standardCelRuntimeBuilder().build();
+ private static final CelRuntime CEL_RUNTIME = CelRuntimeFactory.plannerRuntimeBuilder().build();
/**
* Compiles the given expression and evaluates it.
diff --git a/codelab/src/main/codelab/Exercise4.java b/codelab/src/main/codelab/Exercise4.java
index df4d3ab1c..402255152 100644
--- a/codelab/src/main/codelab/Exercise4.java
+++ b/codelab/src/main/codelab/Exercise4.java
@@ -63,7 +63,7 @@ CelAbstractSyntaxTree compile(String expression) {
*/
Object eval(CelAbstractSyntaxTree ast, Map parameterValues) {
CelRuntime celRuntime =
- CelRuntimeFactory.standardCelRuntimeBuilder()
+ CelRuntimeFactory.plannerRuntimeBuilder()
// Provide the custom `contains` function implementation here.
.build();
diff --git a/codelab/src/main/codelab/Exercise5.java b/codelab/src/main/codelab/Exercise5.java
index eca2a5df7..00a64e261 100644
--- a/codelab/src/main/codelab/Exercise5.java
+++ b/codelab/src/main/codelab/Exercise5.java
@@ -55,7 +55,7 @@ CelAbstractSyntaxTree compile(String expression) {
* @throws IllegalArgumentException If the compiled expression in AST fails to evaluate.
*/
Object eval(CelAbstractSyntaxTree ast, Map parameterValues) {
- CelRuntime celRuntime = CelRuntimeFactory.standardCelRuntimeBuilder().build();
+ CelRuntime celRuntime = CelRuntimeFactory.plannerRuntimeBuilder().build();
try {
CelRuntime.Program program = celRuntime.createProgram(ast);
diff --git a/codelab/src/main/codelab/Exercise6.java b/codelab/src/main/codelab/Exercise6.java
index 9991fb566..71d8e09f4 100644
--- a/codelab/src/main/codelab/Exercise6.java
+++ b/codelab/src/main/codelab/Exercise6.java
@@ -59,9 +59,7 @@ CelAbstractSyntaxTree compile(String expression) {
/** Evaluates the compiled AST with the user provided parameter values. */
Object eval(CelAbstractSyntaxTree ast, Map parameterValues) {
CelRuntime celRuntime =
- CelRuntimeFactory.standardCelRuntimeBuilder()
- .addMessageTypes(Request.getDescriptor())
- .build();
+ CelRuntimeFactory.plannerRuntimeBuilder().addMessageTypes(Request.getDescriptor()).build();
try {
CelRuntime.Program program = celRuntime.createProgram(ast);
diff --git a/codelab/src/main/codelab/Exercise7.java b/codelab/src/main/codelab/Exercise7.java
index ce2efd88e..2d2fa2c3a 100644
--- a/codelab/src/main/codelab/Exercise7.java
+++ b/codelab/src/main/codelab/Exercise7.java
@@ -58,9 +58,7 @@ CelAbstractSyntaxTree compile(String expression) {
/** Evaluates the compiled AST with the user provided parameter values. */
Object eval(CelAbstractSyntaxTree ast, Map parameterValues) {
CelRuntime celRuntime =
- CelRuntimeFactory.standardCelRuntimeBuilder()
- .addMessageTypes(Request.getDescriptor())
- .build();
+ CelRuntimeFactory.plannerRuntimeBuilder().addMessageTypes(Request.getDescriptor()).build();
try {
CelRuntime.Program program = celRuntime.createProgram(ast);
diff --git a/codelab/src/main/codelab/Exercise8.java b/codelab/src/main/codelab/Exercise8.java
index d38854687..107e95038 100644
--- a/codelab/src/main/codelab/Exercise8.java
+++ b/codelab/src/main/codelab/Exercise8.java
@@ -40,7 +40,7 @@ final class Exercise8 {
.addMessageTypes(AttributeContext.Request.getDescriptor())
.build();
private static final CelRuntime CEL_RUNTIME =
- CelRuntimeFactory.standardCelRuntimeBuilder()
+ CelRuntimeFactory.plannerRuntimeBuilder()
.addMessageTypes(AttributeContext.Request.getDescriptor())
.build();
diff --git a/codelab/src/main/codelab/Exercise9.java b/codelab/src/main/codelab/Exercise9.java
index 85705390c..8129800cb 100644
--- a/codelab/src/main/codelab/Exercise9.java
+++ b/codelab/src/main/codelab/Exercise9.java
@@ -55,7 +55,7 @@ final class Exercise9 {
.addMessageTypes(AttributeContext.Request.getDescriptor())
.build();
private static final CelRuntime CEL_RUNTIME =
- CelRuntimeFactory.standardCelRuntimeBuilder()
+ CelRuntimeFactory.plannerRuntimeBuilder()
.addMessageTypes(AttributeContext.Request.getDescriptor())
.build();
private static final CelValidator CEL_VALIDATOR =
diff --git a/codelab/src/main/codelab/solutions/Exercise1.java b/codelab/src/main/codelab/solutions/Exercise1.java
index 0807a1931..e1b3b2269 100644
--- a/codelab/src/main/codelab/solutions/Exercise1.java
+++ b/codelab/src/main/codelab/solutions/Exercise1.java
@@ -73,7 +73,7 @@ CelAbstractSyntaxTree compile(String expression) {
Object eval(CelAbstractSyntaxTree ast) {
// Construct a CelRuntime instance
// CelRuntime is immutable just like the compiler and can be moved to a static final member.
- CelRuntime celRuntime = CelRuntimeFactory.standardCelRuntimeBuilder().build();
+ CelRuntime celRuntime = CelRuntimeFactory.plannerRuntimeBuilder().build();
try {
// Plan the program
diff --git a/codelab/src/main/codelab/solutions/Exercise2.java b/codelab/src/main/codelab/solutions/Exercise2.java
index 5a1c1e8cc..4525a6f60 100644
--- a/codelab/src/main/codelab/solutions/Exercise2.java
+++ b/codelab/src/main/codelab/solutions/Exercise2.java
@@ -66,7 +66,7 @@ CelAbstractSyntaxTree compile(String expression, String variableName, CelType va
* @throws IllegalArgumentException If the compiled expression in AST fails to evaluate.
*/
Object eval(CelAbstractSyntaxTree ast, Map parameterValues) {
- CelRuntime celRuntime = CelRuntimeFactory.standardCelRuntimeBuilder().build();
+ CelRuntime celRuntime = CelRuntimeFactory.plannerRuntimeBuilder().build();
try {
CelRuntime.Program program = celRuntime.createProgram(ast);
diff --git a/codelab/src/main/codelab/solutions/Exercise3.java b/codelab/src/main/codelab/solutions/Exercise3.java
index 590dfd0df..80c9a8beb 100644
--- a/codelab/src/main/codelab/solutions/Exercise3.java
+++ b/codelab/src/main/codelab/solutions/Exercise3.java
@@ -27,8 +27,7 @@ final class Exercise3 {
private static final CelCompiler CEL_COMPILER =
CelCompilerFactory.standardCelCompilerBuilder().setResultType(SimpleType.BOOL).build();
- private static final CelRuntime CEL_RUNTIME =
- CelRuntimeFactory.standardCelRuntimeBuilder().build();
+ private static final CelRuntime CEL_RUNTIME = CelRuntimeFactory.plannerRuntimeBuilder().build();
/**
* Compiles the given expression and evaluates it.
diff --git a/codelab/src/main/codelab/solutions/Exercise4.java b/codelab/src/main/codelab/solutions/Exercise4.java
index b3cc82a24..129a9d7b5 100644
--- a/codelab/src/main/codelab/solutions/Exercise4.java
+++ b/codelab/src/main/codelab/solutions/Exercise4.java
@@ -81,7 +81,7 @@ CelAbstractSyntaxTree compile(String expression) {
*/
Object eval(CelAbstractSyntaxTree ast, Map parameterValues) {
CelRuntime celRuntime =
- CelRuntimeFactory.standardCelRuntimeBuilder()
+ CelRuntimeFactory.plannerRuntimeBuilder()
.addFunctionBindings(
CelFunctionBinding.from(
"map_contains_key_value",
diff --git a/codelab/src/main/codelab/solutions/Exercise5.java b/codelab/src/main/codelab/solutions/Exercise5.java
index e948adfed..8206efa33 100644
--- a/codelab/src/main/codelab/solutions/Exercise5.java
+++ b/codelab/src/main/codelab/solutions/Exercise5.java
@@ -60,7 +60,7 @@ CelAbstractSyntaxTree compile(String expression) {
* @throws IllegalArgumentException If the compiled expression in AST fails to evaluate.
*/
Object eval(CelAbstractSyntaxTree ast, Map parameterValues) {
- CelRuntime celRuntime = CelRuntimeFactory.standardCelRuntimeBuilder().build();
+ CelRuntime celRuntime = CelRuntimeFactory.plannerRuntimeBuilder().build();
try {
CelRuntime.Program program = celRuntime.createProgram(ast);
diff --git a/codelab/src/main/codelab/solutions/Exercise6.java b/codelab/src/main/codelab/solutions/Exercise6.java
index 9b6c59949..dba84291b 100644
--- a/codelab/src/main/codelab/solutions/Exercise6.java
+++ b/codelab/src/main/codelab/solutions/Exercise6.java
@@ -62,9 +62,7 @@ CelAbstractSyntaxTree compile(String expression) {
/** Evaluates the compiled AST with the user provided parameter values. */
Object eval(CelAbstractSyntaxTree ast, Map parameterValues) {
CelRuntime celRuntime =
- CelRuntimeFactory.standardCelRuntimeBuilder()
- .addMessageTypes(Request.getDescriptor())
- .build();
+ CelRuntimeFactory.plannerRuntimeBuilder().addMessageTypes(Request.getDescriptor()).build();
try {
CelRuntime.Program program = celRuntime.createProgram(ast);
diff --git a/codelab/src/main/codelab/solutions/Exercise7.java b/codelab/src/main/codelab/solutions/Exercise7.java
index e5be29171..e45002f71 100644
--- a/codelab/src/main/codelab/solutions/Exercise7.java
+++ b/codelab/src/main/codelab/solutions/Exercise7.java
@@ -60,9 +60,7 @@ CelAbstractSyntaxTree compile(String expression) {
/** Evaluates the compiled AST with the user provided parameter values. */
Object eval(CelAbstractSyntaxTree ast, Map parameterValues) {
CelRuntime celRuntime =
- CelRuntimeFactory.standardCelRuntimeBuilder()
- .addMessageTypes(Request.getDescriptor())
- .build();
+ CelRuntimeFactory.plannerRuntimeBuilder().addMessageTypes(Request.getDescriptor()).build();
try {
CelRuntime.Program program = celRuntime.createProgram(ast);
diff --git a/codelab/src/main/codelab/solutions/Exercise8.java b/codelab/src/main/codelab/solutions/Exercise8.java
index 161089354..f23bb7aa8 100644
--- a/codelab/src/main/codelab/solutions/Exercise8.java
+++ b/codelab/src/main/codelab/solutions/Exercise8.java
@@ -27,7 +27,6 @@
import dev.cel.optimizer.CelOptimizerFactory;
import dev.cel.optimizer.optimizers.ConstantFoldingOptimizer;
import dev.cel.optimizer.optimizers.SubexpressionOptimizer;
-import dev.cel.optimizer.optimizers.SubexpressionOptimizer.SubexpressionOptimizerOptions;
import dev.cel.runtime.CelEvaluationException;
import dev.cel.runtime.CelRuntime;
import dev.cel.runtime.CelRuntimeFactory;
@@ -52,7 +51,7 @@ final class Exercise8 {
.addMessageTypes(AttributeContext.Request.getDescriptor())
.build();
private static final CelRuntime CEL_RUNTIME =
- CelRuntimeFactory.standardCelRuntimeBuilder()
+ CelRuntimeFactory.plannerRuntimeBuilder()
.addMessageTypes(AttributeContext.Request.getDescriptor())
.build();
@@ -69,9 +68,7 @@ final class Exercise8 {
private static final CelOptimizer CEL_OPTIMIZER =
CelOptimizerFactory.standardCelOptimizerBuilder(CEL_COMPILER, CEL_RUNTIME)
.addAstOptimizers(
- ConstantFoldingOptimizer.getInstance(),
- SubexpressionOptimizer.newInstance(
- SubexpressionOptimizerOptions.newBuilder().enableCelBlock(true).build()))
+ ConstantFoldingOptimizer.getInstance(), SubexpressionOptimizer.getInstance())
.build();
/**
diff --git a/codelab/src/main/codelab/solutions/Exercise9.java b/codelab/src/main/codelab/solutions/Exercise9.java
index 2b45c3539..7ea1c1a52 100644
--- a/codelab/src/main/codelab/solutions/Exercise9.java
+++ b/codelab/src/main/codelab/solutions/Exercise9.java
@@ -62,7 +62,7 @@ final class Exercise9 {
.addMessageTypes(AttributeContext.Request.getDescriptor())
.build();
private static final CelRuntime CEL_RUNTIME =
- CelRuntimeFactory.standardCelRuntimeBuilder()
+ CelRuntimeFactory.plannerRuntimeBuilder()
.addMessageTypes(AttributeContext.Request.getDescriptor())
.build();
private static final CelValidator CEL_VALIDATOR =
diff --git a/common/BUILD.bazel b/common/BUILD.bazel
index 4e0d7485c..9cb0c2f7b 100644
--- a/common/BUILD.bazel
+++ b/common/BUILD.bazel
@@ -8,7 +8,65 @@ package(
java_library(
name = "compiler_common",
- exports = ["//common/src/main/java/dev/cel/common:compiler_common"],
+ deprecation = "Please use the granular targets (e.g. :cel_issue, :cel_validation_result, etc.) instead.",
+ exports = [
+ ":cel_function_decl",
+ ":cel_issue",
+ ":cel_overload_decl",
+ ":cel_validation_exception",
+ ":cel_validation_result",
+ ":cel_var_decl",
+ ],
+)
+
+java_library(
+ name = "cel_function_decl",
+ exports = ["//common/src/main/java/dev/cel/common:cel_function_decl"],
+)
+
+java_library(
+ name = "cel_overload_decl",
+ exports = ["//common/src/main/java/dev/cel/common:cel_overload_decl"],
+)
+
+java_library(
+ name = "cel_var_decl",
+ exports = ["//common/src/main/java/dev/cel/common:cel_var_decl"],
+)
+
+cel_android_library(
+ name = "cel_var_decl_android",
+ exports = ["//common/src/main/java/dev/cel/common:cel_var_decl_android"],
+)
+
+java_library(
+ name = "cel_issue",
+ exports = ["//common/src/main/java/dev/cel/common:cel_issue"],
+)
+
+cel_android_library(
+ name = "cel_issue_android",
+ exports = ["//common/src/main/java/dev/cel/common:cel_issue_android"],
+)
+
+java_library(
+ name = "cel_validation_exception",
+ exports = ["//common/src/main/java/dev/cel/common:cel_validation_exception"],
+)
+
+cel_android_library(
+ name = "cel_validation_exception_android",
+ exports = ["//common/src/main/java/dev/cel/common:cel_validation_exception_android"],
+)
+
+java_library(
+ name = "cel_validation_result",
+ exports = ["//common/src/main/java/dev/cel/common:cel_validation_result"],
+)
+
+cel_android_library(
+ name = "cel_validation_result_android",
+ exports = ["//common/src/main/java/dev/cel/common:cel_validation_result_android"],
)
java_library(
@@ -22,6 +80,11 @@ java_library(
exports = ["//common/src/main/java/dev/cel/common:container"],
)
+cel_android_library(
+ name = "container_android",
+ exports = ["//common/src/main/java/dev/cel/common:container_android"],
+)
+
java_library(
name = "proto_ast",
exports = ["//common/src/main/java/dev/cel/common:proto_ast"],
@@ -69,6 +132,11 @@ java_library(
exports = ["//common/src/main/java/dev/cel/common:source_location"],
)
+cel_android_library(
+ name = "source_location_android",
+ exports = ["//common/src/main/java/dev/cel/common:source_location_android"],
+)
+
java_library(
name = "cel_source",
exports = ["//common/src/main/java/dev/cel/common:cel_source"],
diff --git a/common/ast/BUILD.bazel b/common/ast/BUILD.bazel
index 276db0322..9b7573a7c 100644
--- a/common/ast/BUILD.bazel
+++ b/common/ast/BUILD.bazel
@@ -11,6 +11,18 @@ java_library(
exports = ["//common/src/main/java/dev/cel/common/ast"],
)
+java_library(
+ name = "cel_block",
+ visibility = ["//:internal"],
+ exports = ["//common/src/main/java/dev/cel/common/ast:cel_block"],
+)
+
+cel_android_library(
+ name = "cel_block_android",
+ visibility = ["//:internal"],
+ exports = ["//common/src/main/java/dev/cel/common/ast:cel_block_android"],
+)
+
cel_android_library(
name = "ast_android",
exports = ["//common/src/main/java/dev/cel/common/ast:ast_android"],
@@ -41,6 +53,11 @@ java_library(
exports = ["//common/src/main/java/dev/cel/common/ast:expr_factory"],
)
+cel_android_library(
+ name = "expr_factory_android",
+ exports = ["//common/src/main/java/dev/cel/common/ast:expr_factory_android"],
+)
+
java_library(
name = "mutable_expr",
exports = ["//common/src/main/java/dev/cel/common/ast:mutable_expr"],
diff --git a/common/internal/BUILD.bazel b/common/internal/BUILD.bazel
index 0a07e0d63..7c33e56b9 100644
--- a/common/internal/BUILD.bazel
+++ b/common/internal/BUILD.bazel
@@ -128,11 +128,6 @@ cel_android_library(
exports = ["//common/src/main/java/dev/cel/common/internal:internal_android"],
)
-java_library(
- name = "proto_java_qualified_names",
- exports = ["//common/src/main/java/dev/cel/common/internal:proto_java_qualified_names"],
-)
-
java_library(
name = "proto_time_utils",
exports = ["//common/src/main/java/dev/cel/common/internal:proto_time_utils"],
@@ -152,3 +147,8 @@ cel_android_library(
name = "date_time_helpers_android",
exports = ["//common/src/main/java/dev/cel/common/internal:date_time_helpers_android"],
)
+
+java_library(
+ name = "reflection_util",
+ exports = ["//common/src/main/java/dev/cel/common/internal:reflection_util"],
+)
diff --git a/common/navigation/BUILD.bazel b/common/navigation/BUILD.bazel
index 1dba25b8e..8da2514b8 100644
--- a/common/navigation/BUILD.bazel
+++ b/common/navigation/BUILD.bazel
@@ -1,4 +1,5 @@
load("@rules_java//java:defs.bzl", "java_library")
+load("//:cel_android_rules.bzl", "cel_android_library")
package(
default_applicable_licenses = ["//:license"],
@@ -15,7 +16,22 @@ java_library(
exports = ["//common/src/main/java/dev/cel/common/navigation"],
)
+cel_android_library(
+ name = "navigation_android",
+ exports = ["//common/src/main/java/dev/cel/common/navigation:navigation_android"],
+)
+
java_library(
name = "mutable_navigation",
exports = ["//common/src/main/java/dev/cel/common/navigation:mutable_navigation"],
)
+
+java_library(
+ name = "expr_util",
+ exports = ["//common/src/main/java/dev/cel/common/navigation:expr_util"],
+)
+
+cel_android_library(
+ name = "expr_util_android",
+ exports = ["//common/src/main/java/dev/cel/common/navigation:expr_util_android"],
+)
diff --git a/common/src/main/java/dev/cel/common/BUILD.bazel b/common/src/main/java/dev/cel/common/BUILD.bazel
index 38548744c..73475e623 100644
--- a/common/src/main/java/dev/cel/common/BUILD.bazel
+++ b/common/src/main/java/dev/cel/common/BUILD.bazel
@@ -9,16 +9,6 @@ package(
],
)
-# keep sorted
-COMPILER_COMMON_SOURCES = [
- "CelFunctionDecl.java",
- "CelIssue.java",
- "CelOverloadDecl.java",
- "CelValidationException.java",
- "CelValidationResult.java",
- "CelVarDecl.java",
-]
-
# keep sorted
SOURCE_SOURCES = [
"Source.java",
@@ -67,28 +57,151 @@ java_library(
)
java_library(
- name = "compiler_common",
- srcs = COMPILER_COMMON_SOURCES,
+ name = "cel_function_decl",
+ srcs = ["CelFunctionDecl.java"],
tags = [
],
deps = [
- ":cel_ast",
- ":cel_exception",
- ":cel_source",
- ":source",
- ":source_location",
+ ":cel_overload_decl",
"//:auto_value",
"//common/annotations",
- "//common/internal:safe_string_formatter",
+ "@cel_spec//proto/cel/expr:checked_java_proto",
+ "@maven//:com_google_errorprone_error_prone_annotations",
+ "@maven//:com_google_guava_guava",
+ ],
+)
+
+java_library(
+ name = "cel_overload_decl",
+ srcs = ["CelOverloadDecl.java"],
+ tags = [
+ ],
+ deps = [
+ "//:auto_value",
"//common/types:cel_proto_types",
"//common/types:type_providers",
"@cel_spec//proto/cel/expr:checked_java_proto",
"@maven//:com_google_errorprone_error_prone_annotations",
"@maven//:com_google_guava_guava",
+ ],
+)
+
+java_library(
+ name = "cel_var_decl",
+ srcs = ["CelVarDecl.java"],
+ tags = [
+ ],
+ deps = [
+ "//:auto_value",
+ "//common/types:type_providers",
+ "@maven//:com_google_errorprone_error_prone_annotations",
+ ],
+)
+
+cel_android_library(
+ name = "cel_var_decl_android",
+ srcs = ["CelVarDecl.java"],
+ tags = [
+ ],
+ deps = [
+ "//:auto_value",
+ "//common/types:type_providers_android",
+ "@maven//:com_google_errorprone_error_prone_annotations",
+ ],
+)
+
+java_library(
+ name = "cel_issue",
+ srcs = ["CelIssue.java"],
+ tags = [
+ ],
+ deps = [
+ ":source",
+ ":source_location",
+ "//:auto_value",
+ "//common/internal:safe_string_formatter",
+ "@maven//:com_google_errorprone_error_prone_annotations",
+ "@maven//:com_google_guava_guava",
+ ],
+)
+
+cel_android_library(
+ name = "cel_issue_android",
+ srcs = ["CelIssue.java"],
+ tags = [
+ ],
+ deps = [
+ ":source_android",
+ ":source_location_android",
+ "//:auto_value",
+ "//common/internal:safe_string_formatter",
+ "@maven//:com_google_errorprone_error_prone_annotations",
+ "@maven_android//:com_google_guava_guava",
+ ],
+)
+
+java_library(
+ name = "cel_validation_exception",
+ srcs = ["CelValidationException.java"],
+ tags = [
+ ],
+ deps = [
+ ":cel_exception",
+ ":cel_issue",
+ ":cel_source",
+ "//common/annotations",
+ "@maven//:com_google_guava_guava",
+ ],
+)
+
+cel_android_library(
+ name = "cel_validation_exception_android",
+ srcs = ["CelValidationException.java"],
+ tags = [
+ ],
+ deps = [
+ ":cel_exception",
+ ":cel_issue_android",
+ ":cel_source_android",
+ "//common/annotations",
+ "@maven_android//:com_google_guava_guava",
+ ],
+)
+
+java_library(
+ name = "cel_validation_result",
+ srcs = ["CelValidationResult.java"],
+ tags = [
+ ],
+ deps = [
+ ":cel_ast",
+ ":cel_issue",
+ ":cel_source",
+ ":cel_validation_exception",
+ "//common/annotations",
+ "@maven//:com_google_errorprone_error_prone_annotations",
+ "@maven//:com_google_guava_guava",
"@maven//:org_jspecify_jspecify",
],
)
+cel_android_library(
+ name = "cel_validation_result_android",
+ srcs = ["CelValidationResult.java"],
+ tags = [
+ ],
+ deps = [
+ ":cel_ast_android",
+ ":cel_issue_android",
+ ":cel_source_android",
+ ":cel_validation_exception_android",
+ "//common/annotations",
+ "@maven//:com_google_errorprone_error_prone_annotations",
+ "@maven//:org_jspecify_jspecify",
+ "@maven_android//:com_google_guava_guava",
+ ],
+)
+
java_library(
name = "cel_exception",
srcs = ["CelException.java"],
@@ -345,7 +458,8 @@ cel_android_library(
cel_android_library(
name = "source_location_android",
srcs = ["CelSourceLocation.java"],
- visibility = ["//visibility:private"],
+ tags = [
+ ],
deps = [
"//:auto_value",
"@maven//:com_google_errorprone_error_prone_annotations",
@@ -365,6 +479,18 @@ java_library(
],
)
+cel_android_library(
+ name = "container_android",
+ srcs = ["CelContainer.java"],
+ tags = [
+ ],
+ deps = [
+ "//:auto_value",
+ "@maven//:com_google_errorprone_error_prone_annotations",
+ "@maven_android//:com_google_guava_guava",
+ ],
+)
+
java_library(
name = "operator",
srcs = ["Operator.java"],
diff --git a/common/src/main/java/dev/cel/common/CelAbstractSyntaxTree.java b/common/src/main/java/dev/cel/common/CelAbstractSyntaxTree.java
index 6b3b6a74f..b79c67e79 100644
--- a/common/src/main/java/dev/cel/common/CelAbstractSyntaxTree.java
+++ b/common/src/main/java/dev/cel/common/CelAbstractSyntaxTree.java
@@ -103,6 +103,11 @@ public Optional getType(long exprId) {
return Optional.ofNullable(types().get(exprId));
}
+ public CelType getTypeOrThrow(long exprId) {
+ return getType(exprId)
+ .orElseThrow(() -> new NoSuchElementException("Type not found for expr id: " + exprId));
+ }
+
public ImmutableMap getTypeMap() {
return types();
}
diff --git a/common/src/main/java/dev/cel/common/CelFunctionDecl.java b/common/src/main/java/dev/cel/common/CelFunctionDecl.java
index 12beb53d7..ea10366ff 100644
--- a/common/src/main/java/dev/cel/common/CelFunctionDecl.java
+++ b/common/src/main/java/dev/cel/common/CelFunctionDecl.java
@@ -38,6 +38,12 @@ public abstract class CelFunctionDecl {
/** Required. List of function overloads. Must contain at least one overload. */
public abstract ImmutableSet overloads();
+ /** General interface for defining an extension function overload or standard declaration. */
+ @Immutable
+ public interface Declarer {
+ CelFunctionDecl functionDecl();
+ }
+
/** Builder for configuring the {@link CelFunctionDecl}. */
@AutoValue.Builder
public abstract static class Builder {
diff --git a/common/src/main/java/dev/cel/common/CelOptions.java b/common/src/main/java/dev/cel/common/CelOptions.java
index 9cf9a9caa..c4b868bf2 100644
--- a/common/src/main/java/dev/cel/common/CelOptions.java
+++ b/common/src/main/java/dev/cel/common/CelOptions.java
@@ -17,7 +17,6 @@
import com.google.auto.value.AutoValue;
import com.google.errorprone.annotations.CheckReturnValue;
import com.google.errorprone.annotations.Immutable;
-import dev.cel.common.annotations.Beta;
/**
* Options to configure how the CEL parser, type-checker, and evaluator behave.
@@ -61,6 +60,8 @@ public enum ProtoUnsetFieldOptions {
public abstract int maxParseRecursionDepth();
+ public abstract int maxParseExpressionNodeCount();
+
public abstract boolean populateMacroCalls();
public abstract boolean retainRepeatedUnaryOperators();
@@ -71,6 +72,8 @@ public enum ProtoUnsetFieldOptions {
public abstract boolean enableQuotedIdentifierSyntax();
+ public abstract boolean enablePrattParser();
+
// Type-Checker related options
public abstract boolean enableCompileTimeOverloadResolution();
@@ -119,6 +122,8 @@ public enum ProtoUnsetFieldOptions {
public abstract boolean enableComprehension();
+ public abstract boolean enableTimestampOverflowCheck();
+
public abstract int maxRegexProgramSize();
public abstract Builder toBuilder();
@@ -135,11 +140,13 @@ public static Builder newBuilder() {
.maxExpressionCodePointSize(100_000)
.maxParseErrorRecoveryLimit(30)
.maxParseRecursionDepth(250)
+ .maxParseExpressionNodeCount(1_000_000)
.populateMacroCalls(false)
.retainRepeatedUnaryOperators(false)
.retainUnbalancedLogicalExpressions(false)
.enableHiddenAccumulatorVar(true)
- .enableQuotedIdentifierSyntax(false)
+ .enableQuotedIdentifierSyntax(true)
+ .enablePrattParser(false)
// Type-Checker options
.enableCompileTimeOverloadResolution(false)
.enableHomogeneousLiterals(false)
@@ -164,6 +171,7 @@ public static Builder newBuilder() {
.unwrapWellKnownTypesOnFunctionDispatch(true)
.fromProtoUnsetFieldOption(ProtoUnsetFieldOptions.BIND_DEFAULT)
.enableComprehension(true)
+ .enableTimestampOverflowCheck(true)
.maxRegexProgramSize(-1);
}
@@ -177,6 +185,7 @@ public static Builder current() {
.enableUnsignedComparisonAndArithmeticIsUnsigned(true)
.enableUnsignedLongs(true)
.enableRegexPartialMatch(true)
+ .enableTimestampEpoch(true)
.errorOnDuplicateMapKeys(true)
.evaluateCanonicalTypesToNativeValues(true)
.errorOnIntWrap(true)
@@ -223,6 +232,14 @@ public abstract static class Builder {
/** Limit the amount of recursion within parse expressions. */
public abstract Builder maxParseRecursionDepth(int value);
+ /**
+ * Set a limit on the number of expression nodes in the abstract syntax tree for the expression.
+ * This prevents cases where macro expansion results in an AST that is larger than expected from
+ * the source expression. Once exceeded, the parser will record an error and stop expanding
+ * macros but continue parsing to report other errors.
+ */
+ public abstract Builder maxParseExpressionNodeCount(int value);
+
/** Populate macro_calls map in source_info with macro calls parsed from the expression. */
public abstract Builder populateMacroCalls(boolean value);
@@ -265,6 +282,14 @@ public abstract static class Builder {
*/
public abstract Builder enableQuotedIdentifierSyntax(boolean value);
+ /**
+ * Enables Pratt parser implementation over ANTLR parser.
+ *
+ * The Pratt parser provides improved parsing performance (typically 4x–11x speedup over
+ * ANTLR) and lower memory overhead while producing an equivalent abstract syntax tree.
+ */
+ public abstract Builder enablePrattParser(boolean value);
+
// Type-Checker related options
/**
@@ -292,14 +317,20 @@ public abstract static class Builder {
public abstract Builder enableHomogeneousLiterals(boolean value);
/**
- * Enable the {@code int64_to_timestamp} overload which creates a timestamp from Uxix epoch
+ * Enable the {@code int64_to_timestamp} overload which creates a timestamp from Unix epoch
* seconds.
*
- *
This option will be automatically enabled after a sufficient period of time has elapsed to
- * ensure that all runtimes support the implementation.
+ *
Historically used to opt-in to this feature, this option is now enabled by default across
+ * all runtimes.
*
*
TODO: Remove this feature once it has been auto-enabled.
+ *
+ * @deprecated This option is now enabled by default. If you are passing {@code true}, simply
+ * remove this method call. If you are passing {@code false} to disable this feature, subset
+ * the environment instead using {@code dev.cel.checker.CelStandardDeclarations} and {@code
+ * dev.cel.runtime.CelStandardFunctions}.
*/
+ @Deprecated
public abstract Builder enableTimestampEpoch(boolean value);
/**
@@ -427,13 +458,10 @@ public abstract static class Builder {
public abstract Builder enableUnknownTracking(boolean value);
/**
- * Enables the usage of {@code CelValue} for the runtime. It is a native value representation of
- * CEL that wraps Java native objects, and comes with extended capabilities, such as allowing
- * value constructs not understood by CEL (ex: POJOs).
- *
- *
Warning: This option is experimental.
+ * @deprecated Do not use, this flag will be removed in the future. Use the planner based
+ * runtime instead, which supports CelValue by default.
*/
- @Beta
+ @Deprecated
public abstract Builder enableCelValue(boolean value);
/**
@@ -515,6 +543,15 @@ public abstract static class Builder {
*/
public abstract Builder enableJsonFieldNames(boolean value);
+ /**
+ * Enable or disable validating that duration values resulting from timestamp arithmetic do not
+ * overflow 64-bit nanoseconds. Defaults to enabled.
+ *
+ *
Disabling this option is an out-of-conformance behavior that suppresses nanosecond
+ * overflow validation when subtracting timestamps.
+ */
+ public abstract Builder enableTimestampOverflowCheck(boolean value);
+
public abstract CelOptions build();
}
}
diff --git a/common/src/main/java/dev/cel/common/CelSource.java b/common/src/main/java/dev/cel/common/CelSource.java
index d64049f61..4ea2b7a1e 100644
--- a/common/src/main/java/dev/cel/common/CelSource.java
+++ b/common/src/main/java/dev/cel/common/CelSource.java
@@ -33,6 +33,7 @@
import java.util.List;
import java.util.Map;
import java.util.Optional;
+import org.jspecify.annotations.Nullable;
/** Represents the source content of an expression and related metadata. */
@Immutable
@@ -162,9 +163,13 @@ public static final class Builder {
private final CelCodePointArray codePoints;
private final List lineOffsets;
- private final Map positions;
- private final Map macroCalls;
- private final ImmutableSet.Builder extensions;
+ // Both maps start out immutable and empty, and are only copied into a mutable map once they are
+ // actually modified. This keeps the common case (a source that is either never populated, or
+ // populated in bulk from an already immutable map) allocation free.
+ private Map positions;
+ private Map macroCalls;
+ // Null until the first extension is added; extensions are rare.
+ private ImmutableSet.@Nullable Builder extensions;
private final boolean lineOffsetsAlreadyComputed;
private String description;
@@ -176,9 +181,8 @@ private Builder() {
private Builder(CelCodePointArray codePoints, List lineOffsets) {
this.codePoints = checkNotNull(codePoints);
this.lineOffsets = checkNotNull(lineOffsets);
- this.positions = new HashMap<>();
- this.macroCalls = new HashMap<>();
- this.extensions = ImmutableSet.builder();
+ this.positions = ImmutableMap.of();
+ this.macroCalls = ImmutableMap.of();
this.description = "";
this.lineOffsetsAlreadyComputed = !lineOffsets.isEmpty();
}
@@ -207,39 +211,72 @@ public Builder addAllLineOffsets(Iterable lineOffsets) {
return this;
}
+ /**
+ * Returns a map containing every entry of {@code map} plus every entry of {@code additions}.
+ *
+ * If {@code map} is still the empty immutable placeholder a builder starts with, and {@code
+ * additions} is already immutable, then {@code additions} is adopted as-is and no copy is made.
+ * That is the common case: a source populated in bulk exactly once, which lets {@link #build()}
+ * reuse the argument directly. Otherwise the entries are merged into a mutable copy.
+ */
+ private static Map augmentedMap(Map map, Map additions) {
+ if (map instanceof ImmutableMap && map.isEmpty() && additions instanceof ImmutableMap) {
+ return additions;
+ }
+ Map merged = map instanceof HashMap ? map : new HashMap<>(map);
+ merged.putAll(additions);
+ return merged;
+ }
+
+ private Map mutablePositions() {
+ if (!(positions instanceof HashMap)) {
+ positions = new HashMap<>(positions);
+ }
+ return positions;
+ }
+
@CanIgnoreReturnValue
public Builder addPositionsMap(Map positionsMap) {
checkNotNull(positionsMap);
- this.positions.putAll(positionsMap);
+ positions = augmentedMap(positions, positionsMap);
return this;
}
@CanIgnoreReturnValue
public Builder addPositions(long exprId, int position) {
- this.positions.put(exprId, position);
+ mutablePositions().put(exprId, position);
return this;
}
@CanIgnoreReturnValue
public Builder removePositions(long exprId) {
- this.positions.remove(exprId);
+ if (positions.containsKey(exprId)) {
+ mutablePositions().remove(exprId);
+ }
return this;
}
+ private Map mutableMacroCalls() {
+ if (!(macroCalls instanceof HashMap)) {
+ macroCalls = new HashMap<>(macroCalls);
+ }
+ return macroCalls;
+ }
+
@CanIgnoreReturnValue
public Builder addMacroCalls(long exprId, CelExpr expr) {
- this.macroCalls.put(exprId, expr);
+ mutableMacroCalls().put(exprId, expr);
return this;
}
@CanIgnoreReturnValue
public Builder addAllMacroCalls(Map macroCalls) {
- this.macroCalls.putAll(macroCalls);
+ this.macroCalls = augmentedMap(this.macroCalls, macroCalls);
return this;
}
public ImmutableSet getExtensions() {
- return extensions.build();
+ return extensions == null ? ImmutableSet.of() : extensions.build();
}
/**
@@ -249,6 +286,9 @@ public ImmutableSet getExtensions() {
@CanIgnoreReturnValue
public Builder addAllExtensions(Iterable extends Extension> extensions) {
checkNotNull(extensions);
+ if (this.extensions == null) {
+ this.extensions = ImmutableSet.builder();
+ }
this.extensions.addAll(extensions);
return this;
}
@@ -287,14 +327,16 @@ public Optional getOffsetLocation(int offset) {
return CelSourceHelper.getOffsetLocation(codePoints, offset);
}
+ /** Returns a live, mutable view of the positions recorded so far. */
@CheckReturnValue
public Map getPositionsMap() {
- return this.positions;
+ return mutablePositions();
}
+ /** Returns a live, mutable view of the macro calls recorded so far. */
@CheckReturnValue
public Map getMacroCalls() {
- return macroCalls;
+ return mutableMacroCalls();
}
@CheckReturnValue
@@ -310,7 +352,7 @@ public CelSource build() {
ImmutableList.copyOf(lineOffsets),
ImmutableMap.copyOf(positions),
ImmutableMap.copyOf(macroCalls),
- extensions.build());
+ getExtensions());
}
}
diff --git a/common/src/main/java/dev/cel/common/CelValidationException.java b/common/src/main/java/dev/cel/common/CelValidationException.java
index 18bec2fe6..edbd9a0c0 100644
--- a/common/src/main/java/dev/cel/common/CelValidationException.java
+++ b/common/src/main/java/dev/cel/common/CelValidationException.java
@@ -14,8 +14,8 @@
package dev.cel.common;
-import com.google.common.annotations.VisibleForTesting;
import com.google.common.collect.ImmutableList;
+import dev.cel.common.annotations.Internal;
import java.util.List;
/** Base class for all checked exceptions explicitly thrown by the library during parsing. */
@@ -27,7 +27,7 @@ public final class CelValidationException extends CelException {
private final CelSource source;
private final ImmutableList errors;
- @VisibleForTesting
+ @Internal
public CelValidationException(CelSource source, List errors) {
super(safeJoinErrorMessage(source, errors));
this.source = source;
@@ -49,8 +49,9 @@ private static String safeJoinErrorMessage(CelSource source, List erro
List truncatedErrors = errors.subList(0, MAX_ERRORS_TO_REPORT);
return CelIssue.toDisplayString(truncatedErrors, source)
- + String.format(
- "%n...and %d more errors (truncated)", errors.size() - MAX_ERRORS_TO_REPORT);
+ + "\n...and "
+ + (errors.size() - MAX_ERRORS_TO_REPORT)
+ + " more errors (truncated)";
}
/** Returns the {@link CelSource} that was being validated. */
diff --git a/common/src/main/java/dev/cel/common/CelValidationResult.java b/common/src/main/java/dev/cel/common/CelValidationResult.java
index 61152c493..f1f218336 100644
--- a/common/src/main/java/dev/cel/common/CelValidationResult.java
+++ b/common/src/main/java/dev/cel/common/CelValidationResult.java
@@ -22,6 +22,7 @@
import com.google.errorprone.annotations.Immutable;
import com.google.errorprone.annotations.InlineMe;
import dev.cel.common.annotations.Internal;
+import java.util.Comparator;
import org.jspecify.annotations.Nullable;
/**
@@ -31,6 +32,9 @@
@Immutable
public final class CelValidationResult {
+ private static final Comparator BY_SOURCE_LOCATION =
+ comparing(CelIssue::getSourceLocation);
+
@SuppressWarnings("Immutable")
private final @Nullable Throwable failure;
@@ -64,11 +68,20 @@ private CelValidationResult(
@Nullable Throwable failure) {
this.ast = ast;
this.source = source;
- this.issues = ImmutableList.sortedCopyOf(comparing(CelIssue::getSourceLocation), issues);
- this.hasError = issues.stream().anyMatch(CelValidationResult::issueIsError) || failure != null;
+ this.issues = ImmutableList.sortedCopyOf(BY_SOURCE_LOCATION, issues);
+ this.hasError = failure != null || containsError(issues);
this.failure = failure;
}
+ private static boolean containsError(ImmutableList issues) {
+ for (int i = 0; i < issues.size(); i++) {
+ if (issueIsError(issues.get(i))) {
+ return true;
+ }
+ }
+ return false;
+ }
+
/**
* Returns the validated {@code CelAbstractSyntaxTree} if one exists.
*
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..14cb75dd9 100644
--- a/common/src/main/java/dev/cel/common/ast/BUILD.bazel
+++ b/common/src/main/java/dev/cel/common/ast/BUILD.bazel
@@ -57,6 +57,34 @@ java_library(
],
)
+java_library(
+ name = "cel_block",
+ srcs = ["CelBlock.java"],
+ tags = [
+ ],
+ deps = [
+ ":ast",
+ "//common:cel_ast",
+ "//common/annotations",
+ "//common/navigation",
+ "@maven//:com_google_guava_guava",
+ ],
+)
+
+cel_android_library(
+ name = "cel_block_android",
+ srcs = ["CelBlock.java"],
+ tags = [
+ ],
+ deps = [
+ ":ast_android",
+ "//common:cel_ast_android",
+ "//common/annotations",
+ "//common/navigation:navigation_android",
+ "@maven_android//:com_google_guava_guava",
+ ],
+)
+
java_library(
name = "expr_converter",
srcs = EXPR_CONVERTER_SOURCES,
@@ -128,6 +156,19 @@ java_library(
],
)
+cel_android_library(
+ name = "expr_factory_android",
+ srcs = EXPR_FACTORY_SOURCES,
+ tags = [
+ ],
+ deps = [
+ ":ast_android",
+ "//common/annotations",
+ "//common/values:cel_byte_string",
+ "@maven_android//:com_google_guava_guava",
+ ],
+)
+
java_library(
name = "mutable_expr",
srcs = MUTABLE_EXPR_SOURCES,
diff --git a/common/src/main/java/dev/cel/common/ast/CelBlock.java b/common/src/main/java/dev/cel/common/ast/CelBlock.java
new file mode 100644
index 000000000..12de6d4dd
--- /dev/null
+++ b/common/src/main/java/dev/cel/common/ast/CelBlock.java
@@ -0,0 +1,144 @@
+// 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.ast;
+
+import static com.google.common.collect.ImmutableList.toImmutableList;
+
+import com.google.common.base.Preconditions;
+import com.google.common.collect.ImmutableList;
+import dev.cel.common.CelAbstractSyntaxTree;
+import dev.cel.common.annotations.Internal;
+import dev.cel.common.navigation.CelNavigableExpr;
+import java.util.Optional;
+
+/**
+ * Represents a {@code cel.@block} expression.
+ *
+ * CEL Block is used by the CSE (Common Subexpression Elimination) optimizer to hoist common
+ * subexpressions into an evaluated block.
+ */
+@Internal
+public final class CelBlock {
+ public static final String FUNCTION_NAME = "cel.@block";
+ public static final String INDEX_PREFIX = "@index";
+
+ private final CelExpr blockExpr;
+
+ private CelBlock(CelExpr blockExpr) {
+ this.blockExpr = blockExpr;
+ }
+
+ public ImmutableList indices() {
+ return blockExpr.call().args().get(0).list().elements();
+ }
+
+ public CelExpr result() {
+ return blockExpr.call().args().get(1);
+ }
+
+ public CelExpr expr() {
+ return blockExpr;
+ }
+
+ /**
+ * Extracts a {@link CelBlock} from the given AST.
+ *
+ * Enforces the contract that {@code cel.@block} must only appear exactly once and at the root
+ * of the AST.
+ *
+ * @throws IllegalArgumentException if the block is malformed or its indices are invalid.
+ */
+ public static Optional extract(CelAbstractSyntaxTree ast) {
+ CelNavigableExpr celNavigableExpr = CelNavigableExpr.fromExpr(ast.getExpr());
+
+ ImmutableList allCelBlocks =
+ celNavigableExpr
+ .allNodes()
+ .map(CelNavigableExpr::expr)
+ .filter(expr -> expr.callOrDefault().function().equals(FUNCTION_NAME))
+ .collect(toImmutableList());
+ if (allCelBlocks.isEmpty()) {
+ return Optional.empty();
+ }
+
+ Preconditions.checkArgument(
+ allCelBlocks.size() == 1,
+ "Expected 1 cel.block function to be present but found %s",
+ allCelBlocks.size());
+ Preconditions.checkArgument(
+ celNavigableExpr.expr().equals(allCelBlocks.get(0)),
+ "Expected cel.block to be present at root");
+
+ return Optional.of(fromExpr(allCelBlocks.get(0)));
+ }
+
+ /**
+ * Constructs a {@link CelBlock} from a {@link CelExpr}.
+ *
+ * @throws IllegalArgumentException if the expression is not a valid block.
+ */
+ private static CelBlock fromExpr(CelExpr expr) {
+ Preconditions.checkArgument(
+ expr.exprKind().getKind() == CelExpr.ExprKind.Kind.CALL,
+ "Expected cel.@block to be a call expression");
+ Preconditions.checkArgument(
+ expr.call().function().equals(FUNCTION_NAME), "Expected function to be cel.@block");
+ Preconditions.checkArgument(
+ expr.call().args().size() == 2, "Expected exactly 2 arguments for cel.@block");
+ Preconditions.checkArgument(
+ expr.call().args().get(0).exprKind().getKind() == CelExpr.ExprKind.Kind.LIST,
+ "Expected first argument of cel.@block to be a list");
+
+ CelBlock block = new CelBlock(expr);
+
+ // Assert correctness on block indices used in subexpressions
+ ImmutableList subexprs = block.indices();
+ for (int i = 0; i < subexprs.size(); i++) {
+ verifyBlockIndex(subexprs.get(i), i, expr);
+ }
+
+ // Assert correctness on block indices used in block result
+ CelExpr blockResult = block.result();
+ verifyBlockIndex(blockResult, subexprs.size(), expr);
+ boolean resultHasAtLeastOneBlockIndex =
+ CelNavigableExpr.fromExpr(blockResult)
+ .allNodes()
+ .map(CelNavigableExpr::expr)
+ .anyMatch(e -> e.identOrDefault().name().startsWith(INDEX_PREFIX));
+ Preconditions.checkArgument(
+ resultHasAtLeastOneBlockIndex,
+ "Expected at least one reference of index in cel.block result");
+
+ return block;
+ }
+
+ private static void verifyBlockIndex(CelExpr celExpr, int maxIndexValue, CelExpr rootBlock) {
+ boolean areAllIndicesValid =
+ CelNavigableExpr.fromExpr(celExpr)
+ .allNodes()
+ .map(CelNavigableExpr::expr)
+ .filter(expr -> expr.identOrDefault().name().startsWith(INDEX_PREFIX))
+ .map(CelExpr::ident)
+ .allMatch(
+ blockIdent ->
+ Integer.parseInt(blockIdent.name().substring(INDEX_PREFIX.length()))
+ < maxIndexValue);
+ Preconditions.checkArgument(
+ areAllIndicesValid,
+ "Illegal block index found. The index value must be less than %s. Expr: %s",
+ maxIndexValue,
+ rootBlock);
+ }
+}
diff --git a/common/src/main/java/dev/cel/common/ast/CelExpr.java b/common/src/main/java/dev/cel/common/ast/CelExpr.java
index cac968686..0f238b63d 100644
--- a/common/src/main/java/dev/cel/common/ast/CelExpr.java
+++ b/common/src/main/java/dev/cel/common/ast/CelExpr.java
@@ -20,11 +20,13 @@
import com.google.auto.value.AutoOneOf;
import com.google.auto.value.AutoValue;
import com.google.common.collect.ImmutableList;
+import com.google.common.collect.Iterables;
import com.google.errorprone.annotations.CanIgnoreReturnValue;
import com.google.errorprone.annotations.CheckReturnValue;
import com.google.errorprone.annotations.Immutable;
import java.util.ArrayList;
import java.util.Arrays;
+import java.util.Collections;
import java.util.Optional;
/**
@@ -38,6 +40,16 @@
@SuppressWarnings("unchecked") // Class ensures only the super type is used
public abstract class CelExpr implements Expression {
+ /**
+ * Shared instance of the {@link ExprKind.Kind#NOT_SET} kind. {@link CelNotSet} carries no state,
+ * so a single instance can back every unset expression.
+ */
+ private static final ExprKind NOT_SET_KIND =
+ AutoOneOf_CelExpr_ExprKind.notSet(new AutoValue_CelExpr_CelNotSet());
+
+ /** Shared instance of an expression with an unset kind and a zero id. */
+ private static final CelExpr NOT_SET_EXPR = ofNotSet(0L);
+
@Override
public abstract long id();
@@ -340,9 +352,7 @@ public Builder setComprehension(CelComprehension comprehension) {
public abstract Builder toBuilder();
public static Builder newBuilder() {
- return new AutoValue_CelExpr.Builder()
- .setId(0)
- .setExprKind(AutoOneOf_CelExpr_ExprKind.notSet(new AutoValue_CelExpr_CelNotSet()));
+ return new AutoValue_CelExpr.Builder().setId(0).setExprKind(NOT_SET_KIND);
}
/** Denotes the kind of the expression. An expression can only be of one kind. */
@@ -457,7 +467,7 @@ public abstract static class Builder {
public static Builder newBuilder() {
return new AutoValue_CelExpr_CelSelect.Builder()
.setField("")
- .setOperand(CelExpr.newBuilder().build())
+ .setOperand(NOT_SET_EXPR)
.setTestOnly(false);
}
}
@@ -525,13 +535,14 @@ public Builder clearTarget() {
@CanIgnoreReturnValue
public Builder addArgs(CelExpr... args) {
checkNotNull(args);
- return addArgs(Arrays.asList(args));
+ Collections.addAll(mutableArgs, args);
+ return this;
}
@CanIgnoreReturnValue
public Builder addArgs(Iterable args) {
checkNotNull(args);
- args.forEach(mutableArgs::add);
+ Iterables.addAll(mutableArgs, args);
return this;
}
@@ -604,13 +615,14 @@ public Builder setElement(int index, CelExpr element) {
@CanIgnoreReturnValue
public Builder addElements(CelExpr... elements) {
checkNotNull(elements);
- return addElements(Arrays.asList(elements));
+ Collections.addAll(mutableElements, elements);
+ return this;
}
@CanIgnoreReturnValue
public Builder addElements(Iterable elements) {
checkNotNull(elements);
- elements.forEach(mutableElements::add);
+ Iterables.addAll(mutableElements, elements);
return this;
}
@@ -696,13 +708,14 @@ public Builder setEntry(int index, CelStruct.Entry entry) {
@CanIgnoreReturnValue
public Builder addEntries(CelStruct.Entry... entries) {
checkNotNull(entries);
- return addEntries(Arrays.asList(entries));
+ Collections.addAll(mutableEntries, entries);
+ return this;
}
@CanIgnoreReturnValue
public Builder addEntries(Iterable entries) {
checkNotNull(entries);
- entries.forEach(mutableEntries::add);
+ Iterables.addAll(mutableEntries, entries);
return this;
}
@@ -815,13 +828,14 @@ public Builder setEntry(int index, CelMap.Entry entry) {
@CanIgnoreReturnValue
public Builder addEntries(CelMap.Entry... entries) {
checkNotNull(entries);
- return addEntries(Arrays.asList(entries));
+ Collections.addAll(mutableEntries, entries);
+ return this;
}
@CanIgnoreReturnValue
public Builder addEntries(Iterable entries) {
checkNotNull(entries);
- entries.forEach(mutableEntries::add);
+ Iterables.addAll(mutableEntries, entries);
return this;
}
@@ -963,20 +977,17 @@ public static Builder newBuilder() {
return new AutoValue_CelExpr_CelComprehension.Builder()
.setIterVar("")
.setIterVar2("")
- .setIterRange(CelExpr.newBuilder().build())
+ .setIterRange(NOT_SET_EXPR)
.setAccuVar("")
- .setAccuInit(CelExpr.newBuilder().build())
- .setLoopCondition(CelExpr.newBuilder().build())
- .setLoopStep(CelExpr.newBuilder().build())
- .setResult(CelExpr.newBuilder().build());
+ .setAccuInit(NOT_SET_EXPR)
+ .setLoopCondition(NOT_SET_EXPR)
+ .setLoopStep(NOT_SET_EXPR)
+ .setResult(NOT_SET_EXPR);
}
}
public static CelExpr ofNotSet(long id) {
- return newBuilder()
- .setId(id)
- .setExprKind(AutoOneOf_CelExpr_ExprKind.notSet(new AutoValue_CelExpr_CelNotSet()))
- .build();
+ return newBuilder().setId(id).setExprKind(NOT_SET_KIND).build();
}
public static CelExpr ofConstant(long id, CelConstant celConstant) {
@@ -1007,46 +1018,45 @@ public static CelExpr ofSelect(long id, CelExpr operandExpr, String field, boole
.build();
}
+ /** Creates a global (non receiver-style) call expression. */
+ public static CelExpr ofCall(long id, String function, ImmutableList arguments) {
+ return ofCall(id, Optional.empty(), function, arguments);
+ }
+
public static CelExpr ofCall(
long id, Optional targetExpr, String function, ImmutableList arguments) {
-
- CelCall.Builder celCallBuilder = CelCall.newBuilder().setFunction(function).addArgs(arguments);
- targetExpr.ifPresent(celCallBuilder::setTarget);
- return newBuilder()
- .setId(id)
- .setExprKind(AutoOneOf_CelExpr_ExprKind.call(celCallBuilder.build()))
- .build();
+ // setArgs/autoBuild are used in place of addArgs/build so that the already-immutable argument
+ // list is handed straight to the value class, skipping a copy through the builder's mutable
+ // list. This is on the hot path of every parse.
+ CelCall celCall =
+ CelCall.newBuilder()
+ .setFunction(function)
+ .setTarget(targetExpr)
+ .setArgs(arguments)
+ .autoBuild();
+ return newBuilder().setId(id).setExprKind(AutoOneOf_CelExpr_ExprKind.call(celCall)).build();
}
public static CelExpr ofList(
long id, ImmutableList elements, ImmutableList optionalIndices) {
- return newBuilder()
- .setId(id)
- .setExprKind(
- AutoOneOf_CelExpr_ExprKind.list(
- CelList.newBuilder()
- .addElements(elements)
- .addOptionalIndices(optionalIndices)
- .build()))
- .build();
+ CelList celList =
+ CelList.newBuilder()
+ .setElements(elements)
+ .addOptionalIndices(optionalIndices)
+ .autoBuild();
+ return newBuilder().setId(id).setExprKind(AutoOneOf_CelExpr_ExprKind.list(celList)).build();
}
public static CelExpr ofStruct(
long id, String messageName, ImmutableList entries) {
- return newBuilder()
- .setId(id)
- .setExprKind(
- AutoOneOf_CelExpr_ExprKind.struct(
- CelStruct.newBuilder().setMessageName(messageName).addEntries(entries).build()))
- .build();
+ CelStruct celStruct =
+ CelStruct.newBuilder().setMessageName(messageName).setEntries(entries).autoBuild();
+ return newBuilder().setId(id).setExprKind(AutoOneOf_CelExpr_ExprKind.struct(celStruct)).build();
}
public static CelExpr ofMap(long id, ImmutableList entries) {
- return newBuilder()
- .setId(id)
- .setExprKind(
- AutoOneOf_CelExpr_ExprKind.map(CelMap.newBuilder().addEntries(entries).build()))
- .build();
+ CelMap celMap = CelMap.newBuilder().setEntries(entries).autoBuild();
+ return newBuilder().setId(id).setExprKind(AutoOneOf_CelExpr_ExprKind.map(celMap)).build();
}
public static CelStruct.Entry ofStructEntry(
diff --git a/common/src/main/java/dev/cel/common/exceptions/CelInvalidArgumentException.java b/common/src/main/java/dev/cel/common/exceptions/CelInvalidArgumentException.java
index 41358bb79..6a2b4ab72 100644
--- a/common/src/main/java/dev/cel/common/exceptions/CelInvalidArgumentException.java
+++ b/common/src/main/java/dev/cel/common/exceptions/CelInvalidArgumentException.java
@@ -24,4 +24,8 @@ public final class CelInvalidArgumentException extends CelRuntimeException {
public CelInvalidArgumentException(Throwable cause) {
super(cause, CelErrorCode.INVALID_ARGUMENT);
}
+
+ public CelInvalidArgumentException(String message) {
+ super(message, CelErrorCode.INVALID_ARGUMENT);
+ }
}
diff --git a/common/src/main/java/dev/cel/common/formats/ParserContext.java b/common/src/main/java/dev/cel/common/formats/ParserContext.java
index 0bdfdb299..17eff473f 100644
--- a/common/src/main/java/dev/cel/common/formats/ParserContext.java
+++ b/common/src/main/java/dev/cel/common/formats/ParserContext.java
@@ -42,6 +42,32 @@ public interface ParserContext {
Map getIdToOffsetMap();
- /** NewString creates a new ValueString from the YAML node. */
- ValueString newValueString(T node);
+ /**
+ * @deprecated Use {@link #newSourceString} instead.
+ */
+ @Deprecated
+ default ValueString newValueString(T node) {
+ return newSourceString(node);
+ }
+
+ /**
+ * NewYamlString creates a new ValueString from the YAML node, evaluated according to standard
+ * YAML parsing rules.
+ *
+ * This respects the whitespace folding semantics defined by the node's scalar style (e.g.,
+ * folded string {@code >} versus literal string {@code |}). Use this method for general string
+ * fields such as {@code description}, {@code name}, or {@code id}.
+ */
+ ValueString newYamlString(T node);
+
+ /**
+ * NewRawString creates a new ValueString from the YAML node, preserving formatting for accurate
+ * source mapping.
+ *
+ *
This extracts the verbatim text directly from the source file, preserving raw block
+ * indentation and unmodified newlines. Use this method when the string represents code or a CEL
+ * expression where precise character-level offsets must be maintained for accurate diagnostic
+ * error reporting.
+ */
+ ValueString newSourceString(T node);
}
diff --git a/common/src/main/java/dev/cel/common/formats/YamlHelper.java b/common/src/main/java/dev/cel/common/formats/YamlHelper.java
index e0780b01f..c16126f95 100644
--- a/common/src/main/java/dev/cel/common/formats/YamlHelper.java
+++ b/common/src/main/java/dev/cel/common/formats/YamlHelper.java
@@ -136,7 +136,7 @@ public static boolean newBoolean(ParserContext ctx, Node node) {
}
public static String newString(ParserContext ctx, Node node) {
- return ctx.newValueString(node).value();
+ return ctx.newYamlString(node).value();
}
private YamlHelper() {}
diff --git a/common/src/main/java/dev/cel/common/formats/YamlParserContextImpl.java b/common/src/main/java/dev/cel/common/formats/YamlParserContextImpl.java
index 456872803..9f6077562 100644
--- a/common/src/main/java/dev/cel/common/formats/YamlParserContextImpl.java
+++ b/common/src/main/java/dev/cel/common/formats/YamlParserContextImpl.java
@@ -62,7 +62,18 @@ public Map getIdToOffsetMap() {
}
@Override
- public ValueString newValueString(Node node) {
+ public ValueString newYamlString(Node node) {
+ long id = collectMetadata(node);
+ if (!assertYamlType(this, id, node, YamlNodeType.STRING, YamlNodeType.TEXT)) {
+ return ValueString.of(id, ERROR);
+ }
+
+ ScalarNode scalarNode = (ScalarNode) node;
+ return ValueString.of(id, scalarNode.getValue());
+ }
+
+ @Override
+ public ValueString newSourceString(Node node) {
long id = collectMetadata(node);
if (!assertYamlType(this, id, node, YamlNodeType.STRING, YamlNodeType.TEXT)) {
return ValueString.of(id, ERROR);
diff --git a/common/src/main/java/dev/cel/common/internal/BUILD.bazel b/common/src/main/java/dev/cel/common/internal/BUILD.bazel
index 912b4de4b..58b15b103 100644
--- a/common/src/main/java/dev/cel/common/internal/BUILD.bazel
+++ b/common/src/main/java/dev/cel/common/internal/BUILD.bazel
@@ -153,7 +153,6 @@ java_library(
tags = [
],
deps = [
- ":proto_java_qualified_names",
":reflection_util",
"//common/annotations",
"@maven//:com_google_guava_guava",
@@ -397,22 +396,13 @@ java_library(
)
java_library(
- name = "proto_java_qualified_names",
- srcs = ["ProtoJavaQualifiedNames.java"],
+ name = "reflection_util",
+ srcs = ["ReflectionUtil.java"],
tags = [
],
deps = [
"//common/annotations",
"@maven//:com_google_guava_guava",
- "@maven//:com_google_protobuf_protobuf_java",
- ],
-)
-
-java_library(
- name = "reflection_util",
- srcs = ["ReflectionUtil.java"],
- deps = [
- "//common/annotations",
],
)
diff --git a/common/src/main/java/dev/cel/common/internal/BasicCodePointArray.java b/common/src/main/java/dev/cel/common/internal/BasicCodePointArray.java
index a54fb65d7..482a4884f 100644
--- a/common/src/main/java/dev/cel/common/internal/BasicCodePointArray.java
+++ b/common/src/main/java/dev/cel/common/internal/BasicCodePointArray.java
@@ -58,13 +58,14 @@ public BasicCodePointArray slice(int i, int j) {
}
@Override
- public int get(int index) {
- checkElementIndex(index, size());
- return codePoints()[offset() + index] & 0xffff;
+ public String substring(int i, int j) {
+ checkPositionIndexes(i, j, size());
+ return new String(codePoints(), offset() + i, j - i);
}
@Override
- public final String toString() {
- return new String(codePoints(), offset(), size());
+ public int get(int index) {
+ checkElementIndex(index, size());
+ return codePoints()[offset() + index] & 0xffff;
}
}
diff --git a/common/src/main/java/dev/cel/common/internal/CelCodePointArray.java b/common/src/main/java/dev/cel/common/internal/CelCodePointArray.java
index 1f3124c93..a50ce0eea 100644
--- a/common/src/main/java/dev/cel/common/internal/CelCodePointArray.java
+++ b/common/src/main/java/dev/cel/common/internal/CelCodePointArray.java
@@ -36,6 +36,14 @@ public abstract class CelCodePointArray {
/** Returns a new {@link CelCodePointArray} that is a subview of this between [i, j). */
public abstract CelCodePointArray slice(int i, int j);
+ /**
+ * Returns the code points between [i, j) as a string.
+ *
+ * Equivalent to {@code slice(i, j).toString()}, but does not materialize the intermediate
+ * view. Lexing and parsing call this for every literal and identifier.
+ */
+ public abstract String substring(int i, int j);
+
/** Get the code point at the given index. */
public abstract int get(int index);
@@ -55,7 +63,9 @@ public boolean isEmpty() {
}
@Override
- public abstract String toString();
+ public final String toString() {
+ return substring(0, size());
+ }
public static CelCodePointArray fromString(String text) {
if (isNullOrEmpty(text)) {
diff --git a/common/src/main/java/dev/cel/common/internal/Constants.java b/common/src/main/java/dev/cel/common/internal/Constants.java
index d2c0719ec..49bca7489 100644
--- a/common/src/main/java/dev/cel/common/internal/Constants.java
+++ b/common/src/main/java/dev/cel/common/internal/Constants.java
@@ -207,6 +207,9 @@ private static void decodeString(
continue;
}
skipNewline = false;
+ if (codePoint >= MIN_SURROGATE && codePoint <= MAX_SURROGATE) {
+ throw new ParseException("Invalid unicode code point", seqOffset);
+ }
buffer.appendCodePoint(codePoint);
} else {
// Normalize '\r' and '\r\n' to '\n'.
@@ -231,6 +234,9 @@ private static void decodeString(
// For raw literals, all escapes are valid and those characters come through literally in
// the string.
buffer.appendCodePoint('\\');
+ if (codePoint >= MIN_SURROGATE && codePoint <= MAX_SURROGATE) {
+ throw new ParseException("Invalid unicode code point", seqOffset);
+ }
buffer.appendCodePoint(codePoint);
continue;
}
diff --git a/common/src/main/java/dev/cel/common/internal/DefaultInstanceMessageFactory.java b/common/src/main/java/dev/cel/common/internal/DefaultInstanceMessageFactory.java
index fcb0e7056..163d0273e 100644
--- a/common/src/main/java/dev/cel/common/internal/DefaultInstanceMessageFactory.java
+++ b/common/src/main/java/dev/cel/common/internal/DefaultInstanceMessageFactory.java
@@ -15,6 +15,7 @@
package dev.cel.common.internal;
import com.google.protobuf.Descriptors.Descriptor;
+import com.google.protobuf.GeneratorNames;
import com.google.protobuf.Message;
import com.google.protobuf.MessageLite;
import dev.cel.common.annotations.Internal;
@@ -45,9 +46,7 @@ public static DefaultInstanceMessageFactory getInstance() {
public Optional getPrototype(Descriptor descriptor) {
MessageLite defaultInstance =
DefaultInstanceMessageLiteFactory.getInstance()
- .getPrototype(
- descriptor.getFullName(),
- ProtoJavaQualifiedNames.getFullyQualifiedJavaClassName(descriptor))
+ .getPrototype(descriptor.getFullName(), GeneratorNames.getBytecodeClassName(descriptor))
.orElse(null);
if (defaultInstance == null) {
return Optional.empty();
diff --git a/common/src/main/java/dev/cel/common/internal/DefaultMessageFactory.java b/common/src/main/java/dev/cel/common/internal/DefaultMessageFactory.java
index 4a021cd90..68d05e127 100644
--- a/common/src/main/java/dev/cel/common/internal/DefaultMessageFactory.java
+++ b/common/src/main/java/dev/cel/common/internal/DefaultMessageFactory.java
@@ -52,7 +52,7 @@ public Optional newBuilder(String messageName) {
DefaultInstanceMessageFactory.getInstance().getPrototype(descriptor.get());
if (message.isPresent()) {
- return message.map(Message::toBuilder);
+ return message.map(Message::newBuilderForType);
}
return Optional.of(DynamicMessage.newBuilder(descriptor.get()));
diff --git a/common/src/main/java/dev/cel/common/internal/EmptyCodePointArray.java b/common/src/main/java/dev/cel/common/internal/EmptyCodePointArray.java
index 8bca7bf31..32434b02c 100644
--- a/common/src/main/java/dev/cel/common/internal/EmptyCodePointArray.java
+++ b/common/src/main/java/dev/cel/common/internal/EmptyCodePointArray.java
@@ -14,6 +14,8 @@
package dev.cel.common.internal;
+import static com.google.common.base.Preconditions.checkPositionIndexes;
+
import com.google.common.collect.ImmutableList;
import com.google.errorprone.annotations.DoNotCall;
import com.google.errorprone.annotations.Immutable;
@@ -51,6 +53,12 @@ public int get(int index) {
String.format("index (%s) must not be greater than size (0)", index));
}
+ @Override
+ public String substring(int i, int j) {
+ checkPositionIndexes(i, j, 0);
+ return "";
+ }
+
@Override
public int size() {
return 0;
@@ -60,9 +68,4 @@ public int size() {
public ImmutableList lineOffsets() {
return ImmutableList.of(1);
}
-
- @Override
- public String toString() {
- return "";
- }
}
diff --git a/common/src/main/java/dev/cel/common/internal/Latin1CodePointArray.java b/common/src/main/java/dev/cel/common/internal/Latin1CodePointArray.java
index 42cc0445c..1a35ef87f 100644
--- a/common/src/main/java/dev/cel/common/internal/Latin1CodePointArray.java
+++ b/common/src/main/java/dev/cel/common/internal/Latin1CodePointArray.java
@@ -58,13 +58,14 @@ public Latin1CodePointArray slice(int i, int j) {
}
@Override
- public int get(int index) {
- checkElementIndex(index, size());
- return Byte.toUnsignedInt(codePoints()[offset() + index]);
+ public String substring(int i, int j) {
+ checkPositionIndexes(i, j, size());
+ return new String(codePoints(), offset() + i, j - i, ISO_8859_1);
}
@Override
- public final String toString() {
- return new String(codePoints(), offset(), size(), ISO_8859_1);
+ public int get(int index) {
+ checkElementIndex(index, size());
+ return Byte.toUnsignedInt(codePoints()[offset() + index]);
}
}
diff --git a/common/src/main/java/dev/cel/common/internal/ProtoAdapter.java b/common/src/main/java/dev/cel/common/internal/ProtoAdapter.java
index 962a9d2e9..b1b56afe1 100644
--- a/common/src/main/java/dev/cel/common/internal/ProtoAdapter.java
+++ b/common/src/main/java/dev/cel/common/internal/ProtoAdapter.java
@@ -204,8 +204,27 @@ public Optional adaptFieldToValue(FieldDescriptor fieldDescriptor, Objec
@SuppressWarnings({"unchecked", "rawtypes"})
public Optional adaptValueToFieldType(
FieldDescriptor fieldDescriptor, Object fieldValue) {
- if (isWrapperType(fieldDescriptor) && fieldValue.equals(NullValue.NULL_VALUE)) {
- return Optional.empty();
+ if (fieldValue instanceof NullValue) {
+ // `null` assignment to fields indicate that the field would not be set
+ // in a protobuf message (e.g: Message{msg_field: null} -> Message{})
+ //
+ // We explicitly check below for invalid null assignments, such as repeated
+ // or map fields. (e.g: Message{repeated_field: null} -> Error)
+ if (fieldDescriptor.isMapField()
+ || fieldDescriptor.isRepeated()
+ || fieldDescriptor.getJavaType() != FieldDescriptor.JavaType.MESSAGE
+ || WellKnownProto.JSON_STRUCT_VALUE
+ .typeName()
+ .equals(fieldDescriptor.getMessageType().getFullName())
+ || WellKnownProto.JSON_LIST_VALUE
+ .typeName()
+ .equals(fieldDescriptor.getMessageType().getFullName())) {
+ throw new IllegalArgumentException("Unsupported field type");
+ }
+
+ if (!isFieldAnyOrJson(fieldDescriptor)) {
+ return Optional.empty();
+ }
}
if (fieldDescriptor.isMapField()) {
Descriptor entryDescriptor = fieldDescriptor.getMessageType();
@@ -221,7 +240,11 @@ public Optional adaptValueToFieldType(
getDefaultValueForMaybeMessage(keyDescriptor),
valueDescriptor.getLiteType(),
getDefaultValueForMaybeMessage(valueDescriptor));
+ boolean isValueAnyOrJson = isFieldAnyOrJson(valueDescriptor);
for (Map.Entry entry : ((Map, ?>) fieldValue).entrySet()) {
+ if (!isValueAnyOrJson && entry.getValue() instanceof NullValue) {
+ continue;
+ }
mapEntries.add(
protoMapEntry.toBuilder()
.setKey(keyConverter.backwardConverter().convert(entry.getKey()))
@@ -231,15 +254,54 @@ public Optional adaptValueToFieldType(
return Optional.of(mapEntries);
}
if (fieldDescriptor.isRepeated()) {
+ List> listValue = (List>) fieldValue;
+
+ if (!isFieldAnyOrJson(fieldDescriptor)) {
+ listValue = filterOutNullValues(listValue);
+ }
+
return Optional.of(
- AdaptingTypes.adaptingList(
- (List>) fieldValue, fieldToValueConverter(fieldDescriptor).reverse()));
+ AdaptingTypes.adaptingList(listValue, fieldToValueConverter(fieldDescriptor).reverse()));
}
return Optional.of(
fieldToValueConverter(fieldDescriptor).backwardConverter().convert(fieldValue));
}
+ private static List> filterOutNullValues(List> originalList) {
+ List filteredList = null;
+
+ for (int i = 0; i < originalList.size(); i++) {
+ Object elem = originalList.get(i);
+
+ if (elem instanceof NullValue) {
+ if (filteredList == null) {
+ filteredList = new ArrayList<>(originalList.size() - 1);
+ if (i > 0) {
+ filteredList.addAll(originalList.subList(0, i));
+ }
+ }
+ } else if (filteredList != null) {
+ filteredList.add(elem);
+ }
+ }
+
+ // Return the original list if no nulls were found to avoid unnecessary allocations
+ return filteredList != null ? filteredList : originalList;
+ }
+
+ private static boolean isFieldAnyOrJson(FieldDescriptor fieldDescriptor) {
+ if (!fieldDescriptor.getType().equals(FieldDescriptor.Type.MESSAGE)) {
+ return false;
+ }
+
+ String typeFullName = fieldDescriptor.getMessageType().getFullName();
+
+ return WellKnownProto.getByTypeName(typeFullName)
+ .map(wkp -> wkp.equals(WellKnownProto.ANY_VALUE) || wkp.equals(WellKnownProto.JSON_VALUE))
+ .orElse(false);
+ }
+
@SuppressWarnings("rawtypes")
private BidiConverter fieldToValueConverter(FieldDescriptor fieldDescriptor) {
switch (fieldDescriptor.getType()) {
@@ -263,13 +325,6 @@ private BidiConverter fieldToValueConverter(FieldDescriptor fieldDescriptor) {
value -> BidiConverter.IDENTITY.backwardConverter().convert(maybeUnwrap(value)));
case FLOAT:
return unwrapAndConvert(DOUBLE_CONVERTER);
- case DOUBLE:
- case SFIXED64:
- case SINT64:
- case INT64:
- return BidiConverter.of(
- BidiConverter.IDENTITY.forwardConverter(),
- value -> BidiConverter.IDENTITY.backwardConverter().convert(maybeUnwrap(value)));
case BYTES:
if (celOptions.evaluateCanonicalTypesToNativeValues()) {
return BidiConverter.of(
@@ -280,10 +335,11 @@ private BidiConverter fieldToValueConverter(FieldDescriptor fieldDescriptor) {
return BidiConverter.of(
BidiConverter.IDENTITY.forwardConverter(),
value -> BidiConverter.IDENTITY.backwardConverter().convert(maybeUnwrap(value)));
+ case DOUBLE:
+ case SFIXED64:
+ case SINT64:
+ case INT64:
case STRING:
- return BidiConverter.of(
- BidiConverter.IDENTITY.forwardConverter(),
- value -> BidiConverter.IDENTITY.backwardConverter().convert(maybeUnwrap(value)));
case BOOL:
return BidiConverter.of(
BidiConverter.IDENTITY.forwardConverter(),
@@ -291,10 +347,14 @@ private BidiConverter fieldToValueConverter(FieldDescriptor fieldDescriptor) {
case ENUM:
return BidiConverter.of(
value -> (long) ((EnumValueDescriptor) value).getNumber(),
- number ->
- fieldDescriptor
- .getEnumType()
- .findValueByNumberCreatingIfUnknown(number.intValue()));
+ number -> {
+ if (number > Integer.MAX_VALUE || number < Integer.MIN_VALUE) {
+ throw new IllegalArgumentException("Enum value out of int32 range: " + number);
+ }
+ return fieldDescriptor
+ .getEnumType()
+ .findValueByNumberCreatingIfUnknown(number.intValue());
+ });
case MESSAGE:
return BidiConverter.of(
this::adaptProtoToValue,
@@ -370,14 +430,6 @@ private static String typeName(Descriptor protoType) {
return protoType.getFullName();
}
- private static boolean isWrapperType(FieldDescriptor fieldDescriptor) {
- if (fieldDescriptor.getJavaType() != FieldDescriptor.JavaType.MESSAGE) {
- return false;
- }
- String fieldTypeName = fieldDescriptor.getMessageType().getFullName();
- return WellKnownProto.isWrapperType(fieldTypeName);
- }
-
private static int intCheckedCast(long value) {
try {
return Ints.checkedCast(value);
diff --git a/common/src/main/java/dev/cel/common/internal/ProtoJavaQualifiedNames.java b/common/src/main/java/dev/cel/common/internal/ProtoJavaQualifiedNames.java
deleted file mode 100644
index f27181a50..000000000
--- a/common/src/main/java/dev/cel/common/internal/ProtoJavaQualifiedNames.java
+++ /dev/null
@@ -1,52 +0,0 @@
-// Copyright 2025 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.internal;
-
-import com.google.protobuf.Descriptors.Descriptor;
-import com.google.protobuf.Descriptors.FileDescriptor;
-import com.google.protobuf.GeneratorNames;
-import dev.cel.common.annotations.Internal;
-
-/**
- * Helper class for constructing a fully qualified Java class name from a protobuf descriptor.
- *
- * CEL Library Internals. Do Not Use.
- */
-@Internal
-public final class ProtoJavaQualifiedNames {
- /**
- * Retrieves the full Java class name from the given descriptor
- *
- * @return fully qualified class name.
- *
Example 1: dev.cel.expr.Value
- *
Example 2: com.google.rpc.context.AttributeContext$Resource (Nested classes)
- *
Example 3: com.google.api.expr.cel.internal.testdata$SingleFileProto$SingleFile$Path
- * (Nested class with java multiple files disabled)
- */
- public static String getFullyQualifiedJavaClassName(Descriptor descriptor) {
- return GeneratorNames.getBytecodeClassName(descriptor);
- }
-
- /**
- * Gets the java package name from the descriptor. See
- * https://developers.google.com/protocol-buffers/docs/reference/java-generated#package for rules
- * on package name generation
- */
- public static String getJavaPackageName(FileDescriptor fileDescriptor) {
- return GeneratorNames.getFileJavaPackage(fileDescriptor.toProto());
- }
-
- private ProtoJavaQualifiedNames() {}
-}
diff --git a/common/src/main/java/dev/cel/common/internal/ProtoTimeUtils.java b/common/src/main/java/dev/cel/common/internal/ProtoTimeUtils.java
index 36671842d..124f9dbe1 100644
--- a/common/src/main/java/dev/cel/common/internal/ProtoTimeUtils.java
+++ b/common/src/main/java/dev/cel/common/internal/ProtoTimeUtils.java
@@ -18,7 +18,6 @@
import static com.google.common.math.LongMath.checkedMultiply;
import static com.google.common.math.LongMath.checkedSubtract;
-import com.google.common.annotations.VisibleForTesting;
import com.google.common.base.Strings;
import com.google.errorprone.annotations.CanIgnoreReturnValue;
import com.google.protobuf.Duration;
@@ -50,15 +49,11 @@
public final class ProtoTimeUtils {
// Timestamp for "0001-01-01T00:00:00Z"
- @VisibleForTesting
- static final long TIMESTAMP_SECONDS_MIN = -62135596800L;
+ public static final long TIMESTAMP_SECONDS_MIN = -62135596800L;
// Timestamp for "9999-12-31T23:59:59Z"
- @VisibleForTesting
- static final long TIMESTAMP_SECONDS_MAX = 253402300799L;
- @VisibleForTesting
- static final long DURATION_SECONDS_MIN = -315576000000L;
- @VisibleForTesting
- static final long DURATION_SECONDS_MAX = 315576000000L;
+ public static final long TIMESTAMP_SECONDS_MAX = 253402300799L;
+ public static final long DURATION_SECONDS_MIN = -315576000000L;
+ public static final long DURATION_SECONDS_MAX = 315576000000L;
private static final int MILLIS_PER_SECOND = 1000;
@@ -402,10 +397,21 @@ public static Timestamp subtract(Timestamp ts, Duration dur) {
/** Calculate the difference between two timestamps. */
public static Duration between(Timestamp from, Timestamp to) {
+ return between(from, to, /* validateOverflow= */ true);
+ }
+
+ /** Calculate the difference between two timestamps. */
+ public static Duration between(Timestamp from, Timestamp to, boolean validateOverflow) {
Instant javaFrom = ProtoTimeUtils.toJavaInstant(checkValid(from));
Instant javaTo = ProtoTimeUtils.toJavaInstant(checkValid(to));
java.time.Duration between = java.time.Duration.between(javaFrom, javaTo);
+ if (validateOverflow) {
+ // Call toNanos() to validate 64-bit nanosecond overflow (throws ArithmeticException).
+ // Suppress unused variable warning as the duration object itself is returned.
+ @SuppressWarnings("unused")
+ long unused = between.toNanos();
+ }
return ProtoTimeUtils.toProtoDuration(between);
}
diff --git a/common/src/main/java/dev/cel/common/internal/ReflectionUtil.java b/common/src/main/java/dev/cel/common/internal/ReflectionUtil.java
index e513a446b..97bed650f 100644
--- a/common/src/main/java/dev/cel/common/internal/ReflectionUtil.java
+++ b/common/src/main/java/dev/cel/common/internal/ReflectionUtil.java
@@ -14,9 +14,11 @@
package dev.cel.common.internal;
+import com.google.common.reflect.TypeToken;
import dev.cel.common.annotations.Internal;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
+import java.lang.reflect.Type;
/**
* Utility class for invoking Java reflection.
@@ -48,5 +50,18 @@ public static Object invoke(Method method, Object object, Object... params) {
}
}
+ /** Resolves a generic parameter of a base class from a type token. */
+ public static Type resolveGenericParameter(TypeToken> token, Class> baseClass, int index) {
+ return token.resolveType(baseClass.getTypeParameters()[index]).getType();
+ }
+
+ /**
+ * Extracts the raw Class from a Type. Handles Class, ParameterizedType, and WildcardType (returns
+ * upper bound). Returns Object.class as fallback.
+ */
+ public static Class> getRawType(Type type) {
+ return TypeToken.of(type).getRawType();
+ }
+
private ReflectionUtil() {}
}
diff --git a/common/src/main/java/dev/cel/common/internal/SupplementalCodePointArray.java b/common/src/main/java/dev/cel/common/internal/SupplementalCodePointArray.java
index 0c9214410..f66cbf64b 100644
--- a/common/src/main/java/dev/cel/common/internal/SupplementalCodePointArray.java
+++ b/common/src/main/java/dev/cel/common/internal/SupplementalCodePointArray.java
@@ -59,13 +59,14 @@ public SupplementalCodePointArray slice(int i, int j) {
}
@Override
- public int get(int index) {
- checkElementIndex(index, size());
- return codePoints()[offset() + index];
+ public String substring(int i, int j) {
+ checkPositionIndexes(i, j, size());
+ return new String(codePoints(), offset() + i, j - i);
}
@Override
- public final String toString() {
- return new String(codePoints(), offset(), size());
+ public int get(int index) {
+ checkElementIndex(index, size());
+ return codePoints()[offset() + index];
}
}
diff --git a/common/src/main/java/dev/cel/common/navigation/BUILD.bazel b/common/src/main/java/dev/cel/common/navigation/BUILD.bazel
index b43f3c289..4ae2908bc 100644
--- a/common/src/main/java/dev/cel/common/navigation/BUILD.bazel
+++ b/common/src/main/java/dev/cel/common/navigation/BUILD.bazel
@@ -1,4 +1,5 @@
load("@rules_java//java:defs.bzl", "java_library")
+load("//:cel_android_rules.bzl", "cel_android_library")
package(
default_applicable_licenses = [
@@ -28,6 +29,55 @@ java_library(
],
)
+cel_android_library(
+ name = "common_android",
+ srcs = [
+ "BaseNavigableExpr.java",
+ "CelNavigableExprVisitor.java",
+ "ExprPropertyCalculator.java",
+ "TraversalOrder.java",
+ ],
+ tags = [
+ ],
+ deps = [
+ "//:auto_value",
+ "//common/ast:ast_android",
+ "@maven//:com_google_errorprone_error_prone_annotations",
+ "@maven//:org_jspecify_jspecify",
+ "@maven_android//:com_google_guava_guava",
+ ],
+)
+
+java_library(
+ name = "expr_util",
+ srcs = [
+ "CelNavigableExprUtil.java",
+ ],
+ tags = [
+ ],
+ deps = [
+ ":common",
+ "//common/ast",
+ "@maven//:com_google_errorprone_error_prone_annotations",
+ "@maven//:com_google_guava_guava",
+ ],
+)
+
+cel_android_library(
+ name = "expr_util_android",
+ srcs = [
+ "CelNavigableExprUtil.java",
+ ],
+ tags = [
+ ],
+ deps = [
+ ":common_android",
+ "//common/ast:ast_android",
+ "@maven//:com_google_errorprone_error_prone_annotations",
+ "@maven_android//:com_google_guava_guava",
+ ],
+)
+
java_library(
name = "navigation",
srcs = [
@@ -47,6 +97,25 @@ java_library(
],
)
+cel_android_library(
+ name = "navigation_android",
+ srcs = [
+ "CelNavigableAst.java",
+ "CelNavigableExpr.java",
+ ],
+ tags = [
+ ],
+ deps = [
+ ":common_android",
+ "//:auto_value",
+ "//common:cel_ast_android",
+ "//common/ast:ast_android",
+ "@maven//:com_google_errorprone_error_prone_annotations",
+ "@maven//:org_jspecify_jspecify",
+ "@maven_android//:com_google_guava_guava",
+ ],
+)
+
java_library(
name = "mutable_navigation",
srcs = [
diff --git a/common/src/main/java/dev/cel/common/navigation/BaseNavigableExpr.java b/common/src/main/java/dev/cel/common/navigation/BaseNavigableExpr.java
index 1699b4a96..dabcac3a2 100644
--- a/common/src/main/java/dev/cel/common/navigation/BaseNavigableExpr.java
+++ b/common/src/main/java/dev/cel/common/navigation/BaseNavigableExpr.java
@@ -16,6 +16,7 @@
import com.google.errorprone.annotations.CanIgnoreReturnValue;
import com.google.errorprone.annotations.CheckReturnValue;
+import com.google.errorprone.annotations.DoNotMock;
import dev.cel.common.ast.CelExpr;
import dev.cel.common.ast.CelExpr.ExprKind;
import dev.cel.common.ast.Expression;
@@ -25,9 +26,15 @@
/**
* BaseNavigableExpr represents the base navigable expression value with methods to inspect the
* parent and child expressions.
+ *
+ *
This class is intentionally non-extensible outside of the {@code dev.cel.common.navigation}
+ * package.
*/
+@DoNotMock("Use CelNavigableExpr or CelNavigableMutableExpr")
@SuppressWarnings("unchecked") // Generic types are properly bound to Expression
-abstract class BaseNavigableExpr {
+public abstract class BaseNavigableExpr {
+
+ BaseNavigableExpr() {}
public abstract E expr();
diff --git a/common/src/main/java/dev/cel/common/navigation/CelNavigableExprUtil.java b/common/src/main/java/dev/cel/common/navigation/CelNavigableExprUtil.java
new file mode 100644
index 000000000..c5a19ff9e
--- /dev/null
+++ b/common/src/main/java/dev/cel/common/navigation/CelNavigableExprUtil.java
@@ -0,0 +1,230 @@
+// 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.navigation;
+
+import static com.google.common.base.Preconditions.checkNotNull;
+
+import com.google.common.collect.ImmutableSet;
+import com.google.errorprone.annotations.CheckReturnValue;
+import dev.cel.common.ast.CelExpr.ExprKind.Kind;
+import dev.cel.common.ast.Expression;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.Optional;
+
+/** Utility class for common AST navigation and scoping inspections on {@link BaseNavigableExpr}. */
+@CheckReturnValue
+public final class CelNavigableExprUtil {
+
+ /**
+ * Returns the nearest enclosing comprehension that declares {@code variableName} in scope for
+ * {@code expr}, or {@code Optional.empty()} if none exists.
+ *
+ * A comprehension declares {@code variableName} in scope for {@code expr} if {@code
+ * variableName} matches {@code iterVar}, {@code iterVar2}, or {@code accuVar}, and {@code expr}
+ * resides within the branch where that variable is active:
+ *
+ *
+ * In {@code loopCondition} and {@code loopStep}: {@code iterVar}, {@code iterVar2}, and
+ * {@code accuVar} are in scope.
+ * In {@code result}: only {@code accuVar} is in scope.
+ * In {@code iterRange} and {@code accuInit}: none of the comprehension variables are in
+ * scope.
+ *
+ */
+ @SuppressWarnings("ReferenceEquality") // Disambiguates mutable child branches
+ public static >
+ Optional findDeclaringComprehension(T expr, String variableName) {
+ checkNotNull(expr);
+ checkNotNull(variableName);
+ if (variableName.isEmpty()) {
+ return Optional.empty();
+ }
+ T curr = expr;
+ Optional maybeParent = curr.parent();
+ while (maybeParent.isPresent()) {
+ T parent = maybeParent.get();
+ if (parent.getKind() == Kind.COMPREHENSION) {
+ Expression.Comprehension> comp = parent.expr().comprehension();
+ Expression currExpr = curr.expr();
+
+ if (currExpr != comp.iterRange() && currExpr != comp.accuInit()) {
+ if (currExpr == comp.result()) {
+ if (comp.accuVar().equals(variableName)) {
+ return Optional.of(parent);
+ }
+ } else {
+ if (comp.iterVar().equals(variableName)
+ || comp.iterVar2().equals(variableName)
+ || comp.accuVar().equals(variableName)) {
+ return Optional.of(parent);
+ }
+ }
+ }
+ }
+ curr = parent;
+ maybeParent = parent.parent();
+ }
+ return Optional.empty();
+ }
+
+ /**
+ * Returns a set of all variables declared by enclosing comprehensions that are in scope for
+ * {@code expr}.
+ *
+ * A comprehension variable ({@code iterVar}, {@code iterVar2}, or {@code accuVar}) is in scope
+ * if {@code expr} resides within the branch where that variable is active:
+ *
+ *
+ * In {@code loopCondition} and {@code loopStep}: {@code iterVar}, {@code iterVar2}, and
+ * {@code accuVar} are in scope.
+ * In {@code result}: only {@code accuVar} is in scope.
+ * In {@code iterRange} and {@code accuInit}: none of the comprehension variables are in
+ * scope.
+ *
+ */
+ @SuppressWarnings("ReferenceEquality") // Disambiguates mutable child branches
+ public static ImmutableSet getEnclosingComprehensionVariables(BaseNavigableExpr> expr) {
+ checkNotNull(expr);
+ ImmutableSet.Builder variables = ImmutableSet.builder();
+ BaseNavigableExpr> curr = expr;
+ Optional extends BaseNavigableExpr>> maybeParent = curr.parent();
+ while (maybeParent.isPresent()) {
+ BaseNavigableExpr> parent = maybeParent.get();
+ if (parent.getKind() == Kind.COMPREHENSION) {
+ Expression.Comprehension> comp = parent.expr().comprehension();
+ Expression currExpr = curr.expr();
+
+ if (currExpr != comp.iterRange() && currExpr != comp.accuInit()) {
+ if (currExpr == comp.result()) {
+ variables.add(comp.accuVar());
+ } else {
+ variables.add(comp.iterVar());
+ if (!comp.iterVar2().isEmpty()) {
+ variables.add(comp.iterVar2());
+ }
+ variables.add(comp.accuVar());
+ }
+ }
+ }
+ curr = parent;
+ maybeParent = parent.parent();
+ }
+ return variables.build();
+ }
+
+ /**
+ * Returns true if {@code variableName} is in scope and shadowed by an enclosing comprehension
+ * above {@code expr}.
+ *
+ * A variable is shadowed at {@code expr} if an ancestor comprehension declares it as an
+ * iteration variable ({@code iterVar}, {@code iterVar2}) or accumulator variable ({@code
+ * accuVar}) and {@code expr} resides within a branch where that variable is active:
+ *
+ *
+ * In {@code loopCondition} and {@code loopStep}: {@code iterVar}, {@code iterVar2}, and
+ * {@code accuVar} are in scope.
+ * In {@code result}: only {@code accuVar} is in scope ({@code iterVar} and {@code iterVar2}
+ * have fallen out of scope).
+ * In {@code iterRange} and {@code accuInit}: none of the comprehension variables are in
+ * scope.
+ *
+ *
+ * For example, in the expression:
+ *
+ *
{@code
+ * [1, 2].all(x, x > 0)
+ * }
+ *
+ *
+ * At {@code x} in {@code x > 0}: {@code isVariableShadowed(x, "x")} is {@code true}.
+ * At the list {@code [1, 2]}: {@code isVariableShadowed(list, "x")} is {@code false}.
+ *
+ */
+ public static boolean isVariableShadowed(BaseNavigableExpr> expr, String variableName) {
+ return findDeclaringComprehension(expr, variableName).isPresent();
+ }
+
+ /**
+ * Returns true if any of {@code variableNames} is in scope and shadowed by an enclosing
+ * comprehension above {@code expr}.
+ *
+ * For example, in the nested comprehension expression:
+ *
+ *
{@code
+ * [1, 2].all(x, [3, 4].all(y, x > 0 && y > 0))
+ * }
+ *
+ * At {@code y > 0}, {@code areVariablesShadowed(node, ImmutableSet.of("x", "z"))} is {@code true}
+ * because {@code x} is in scope from the outer comprehension.
+ */
+ public static boolean areVariablesShadowed(
+ BaseNavigableExpr> expr, Collection variableNames) {
+ checkNotNull(expr);
+ checkNotNull(variableNames);
+ for (String varName : variableNames) {
+ if (findDeclaringComprehension(expr, varName).isPresent()) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ /**
+ * Returns true if {@code expr} is an {@code IDENT} node that references a variable declared by an
+ * enclosing comprehension.
+ *
+ * For example, in the expression:
+ *
+ *
{@code
+ * [a].all(x, x > a)
+ * }
+ *
+ *
+ * At identifier {@code x}: {@code isComprehensionVariable(x)} is {@code true}.
+ * At identifier {@code a}: {@code isComprehensionVariable(a)} is {@code false}.
+ *
+ */
+ public static boolean isComprehensionVariable(BaseNavigableExpr> expr) {
+ checkNotNull(expr);
+ return expr.getKind() == Kind.IDENT
+ && areVariablesShadowed(expr, Collections.singleton(expr.expr().ident().name()));
+ }
+
+ /**
+ * Returns true if {@code expr} or any identifier within {@code expr} references a variable
+ * declared by an enclosing comprehension.
+ *
+ * For example, in the expression:
+ *
+ *
{@code
+ * [a].all(x, x > a)
+ * }
+ *
+ *
+ * At the subtree {@code x > a}: {@code hasComprehensionVariable(subtree)} is {@code true}
+ * because {@code x} is a comprehension variable.
+ * At the subtree {@code [a]}: {@code hasComprehensionVariable(iterRange)} is {@code false}.
+ *
+ */
+ public static boolean hasComprehensionVariable(BaseNavigableExpr> expr) {
+ checkNotNull(expr);
+ return expr.allNodes()
+ .filter(node -> node.getKind() == Kind.IDENT)
+ .anyMatch(CelNavigableExprUtil::isComprehensionVariable);
+ }
+
+ private CelNavigableExprUtil() {}
+}
diff --git a/common/src/main/java/dev/cel/common/types/SimpleType.java b/common/src/main/java/dev/cel/common/types/SimpleType.java
index 6c43ab53f..93bd5326d 100644
--- a/common/src/main/java/dev/cel/common/types/SimpleType.java
+++ b/common/src/main/java/dev/cel/common/types/SimpleType.java
@@ -46,7 +46,6 @@ public abstract class SimpleType extends CelType {
public static final ImmutableMap TYPE_MAP =
ImmutableMap.of(
- DYN.name(), DYN,
BOOL.name(), BOOL,
BYTES.name(), BYTES,
DOUBLE.name(), DOUBLE,
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 53ffdda3d..433dcd477 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,16 +71,19 @@ cel_android_library(
deps = [
"//common/values:values_android",
"@maven//:com_google_errorprone_error_prone_annotations",
- "@maven_android//:com_google_guava_guava",
],
)
java_library(
name = "combined_cel_value_provider",
- srcs = ["CombinedCelValueProvider.java"],
+ srcs = [
+ "CombinedCelValueProvider.java",
+ ],
tags = [
],
deps = [
+ ":combined_cel_value_converter",
+ ":values",
"//common/values:cel_value_provider",
"@maven//:com_google_errorprone_error_prone_annotations",
"@maven//:com_google_guava_guava",
@@ -90,16 +92,70 @@ java_library(
cel_android_library(
name = "combined_cel_value_provider_android",
- srcs = ["CombinedCelValueProvider.java"],
+ srcs = [
+ "CombinedCelValueProvider.java",
+ ],
tags = [
],
deps = [
+ ":combined_cel_value_converter_android",
+ ":values_android",
"//common/values:cel_value_provider_android",
"@maven//:com_google_errorprone_error_prone_annotations",
"@maven_android//:com_google_guava_guava",
],
)
+java_library(
+ name = "combined_cel_value_converter",
+ srcs = [
+ "CombinedCelValueConverter.java",
+ ],
+ tags = [
+ ],
+ deps = [
+ ":values",
+ "//common/annotations",
+ "@maven//:com_google_guava_guava",
+ "@maven//:org_jspecify_jspecify",
+ ],
+)
+
+cel_android_library(
+ name = "combined_cel_value_converter_android",
+ srcs = [
+ "CombinedCelValueConverter.java",
+ ],
+ tags = [
+ ],
+ deps = [
+ ":values_android",
+ "//common/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,
@@ -108,6 +164,7 @@ java_library(
deps = [
":cel_byte_string",
":cel_value",
+ ":preadapted_list",
"//:auto_value",
"//common/annotations",
"//common/types",
@@ -118,6 +175,38 @@ java_library(
],
)
+java_library(
+ name = "mutable_map_value",
+ srcs = ["MutableMapValue.java"],
+ tags = [
+ ],
+ deps = [
+ "//common/annotations",
+ "//common/exceptions:attribute_not_found",
+ "//common/types",
+ "//common/types:type_providers",
+ "//common/values",
+ "//common/values:cel_value",
+ "@maven//:com_google_errorprone_error_prone_annotations",
+ ],
+)
+
+cel_android_library(
+ name = "mutable_map_value_android",
+ srcs = ["MutableMapValue.java"],
+ tags = [
+ ],
+ deps = [
+ ":cel_value_android",
+ "//common/annotations",
+ "//common/exceptions:attribute_not_found",
+ "//common/types:type_providers_android",
+ "//common/types:types_android",
+ "//common/values:values_android",
+ "@maven//:com_google_errorprone_error_prone_annotations",
+ ],
+)
+
cel_android_library(
name = "values_android",
srcs = CEL_VALUES_SOURCES,
@@ -126,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",
@@ -154,7 +244,6 @@ java_library(
],
deps = [
":cel_byte_string",
- ":values",
"//common/annotations",
"//common/internal:proto_time_utils",
"//common/internal:well_known_proto",
@@ -189,6 +278,7 @@ java_library(
],
deps = [
":base_proto_cel_value_converter",
+ ":preadapted_list",
":values",
"//:auto_value",
"//common:options",
@@ -201,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",
],
)
@@ -244,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",
],
)
@@ -271,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",
],
@@ -322,6 +408,7 @@ java_library(
],
deps = [
"//common/annotations",
+ "//common/values",
"//common/values:base_proto_cel_value_converter",
"//common/values:cel_value_provider",
"@maven//:com_google_errorprone_error_prone_annotations",
@@ -337,6 +424,7 @@ cel_android_library(
"//common/annotations",
"//common/values:base_proto_cel_value_converter_android",
"//common/values:cel_value_provider_android",
+ "//common/values:values_android",
"@maven//:com_google_errorprone_error_prone_annotations",
],
)
diff --git a/common/src/main/java/dev/cel/common/values/BaseProtoCelValueConverter.java b/common/src/main/java/dev/cel/common/values/BaseProtoCelValueConverter.java
index b05a21e24..9fc218abe 100644
--- a/common/src/main/java/dev/cel/common/values/BaseProtoCelValueConverter.java
+++ b/common/src/main/java/dev/cel/common/values/BaseProtoCelValueConverter.java
@@ -98,6 +98,8 @@ protected Object fromWellKnownProto(MessageLiteOrBuilder message, WellKnownProto
return UnsignedLong.valueOf(((UInt32Value) message).getValue());
case UINT64_VALUE:
return UnsignedLong.fromLongBits(((UInt64Value) message).getValue());
+ case EMPTY:
+ return ImmutableMap.of();
default:
throw new UnsupportedOperationException(
"Unsupported well known proto conversion - " + wellKnownProto);
diff --git a/common/src/main/java/dev/cel/common/values/BaseProtoMessageValueProvider.java b/common/src/main/java/dev/cel/common/values/BaseProtoMessageValueProvider.java
index f42a16179..51bb0a497 100644
--- a/common/src/main/java/dev/cel/common/values/BaseProtoMessageValueProvider.java
+++ b/common/src/main/java/dev/cel/common/values/BaseProtoMessageValueProvider.java
@@ -28,4 +28,9 @@
public abstract class BaseProtoMessageValueProvider implements CelValueProvider {
public abstract BaseProtoCelValueConverter protoCelValueConverter();
+
+ @Override
+ public CelValueConverter celValueConverter() {
+ return protoCelValueConverter();
+ }
}
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 ae0b40ef7..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,9 +20,12 @@
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.Map.Entry;
import java.util.Optional;
+import java.util.RandomAccess;
+import java.util.function.Function;
/**
* {@code CelValueConverter} handles bidirectional conversion between native Java objects to {@link
@@ -37,45 +40,121 @@ public class CelValueConverter {
private static final CelValueConverter DEFAULT_INSTANCE = new CelValueConverter();
+ @SuppressWarnings("Immutable") // Method reference is immutable
+ private final Function maybeUnwrapFunction;
+
+ @SuppressWarnings("Immutable") // Method reference is immutable
+ private final Function toRuntimeValueFunction;
+
public static CelValueConverter getDefaultInstance() {
return DEFAULT_INSTANCE;
}
- /** Adapts a {@link CelValue} to a plain old Java Object. */
- public Object unwrap(CelValue celValue) {
- Preconditions.checkNotNull(celValue);
+ /**
+ * Unwraps the {@code value} into its plain old Java Object representation.
+ *
+ * The value may be a {@link CelValue}, a {@link Collection} or a {@link Map}.
+ */
+ public Object maybeUnwrap(Object value) {
+ if (value instanceof CelValue || value instanceof CelPreAdaptedList) {
+ return value instanceof CelValue ? unwrap((CelValue) value) : value;
+ }
- if (celValue instanceof OptionalValue) {
- OptionalValue optionalValue = (OptionalValue) celValue;
- if (optionalValue.isZeroValue()) {
- return Optional.empty();
+ return mapContainer(value, maybeUnwrapFunction);
+ }
+
+ /**
+ * Maps a container (Collection or Map) by applying the provided mapper function to its elements.
+ * 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