diff --git a/bundle/src/test/java/dev/cel/bundle/CelEnvironmentExporterTest.java b/bundle/src/test/java/dev/cel/bundle/CelEnvironmentExporterTest.java index b9a81a662..d6608a9d4 100644 --- a/bundle/src/test/java/dev/cel/bundle/CelEnvironmentExporterTest.java +++ b/bundle/src/test/java/dev/cel/bundle/CelEnvironmentExporterTest.java @@ -110,7 +110,7 @@ public void standardLibrarySubset_favorExclusion() throws Exception { FunctionSelector.create("matches", ImmutableSet.of()), FunctionSelector.create( "timestamp", ImmutableSet.of("string_to_timestamp")))) - .setExcludedMacros(ImmutableSet.of("map", "filter")) + .setExcludedMacros(ImmutableSet.of("map", "existsOne", "filter")) .build()); } diff --git a/common/src/main/java/dev/cel/common/Operator.java b/common/src/main/java/dev/cel/common/Operator.java index 0c059d989..c49c78267 100644 --- a/common/src/main/java/dev/cel/common/Operator.java +++ b/common/src/main/java/dev/cel/common/Operator.java @@ -45,7 +45,9 @@ public enum Operator { HAS("has"), ALL("all"), EXISTS("exists"), + @Deprecated // Prefer EXISTS_ONE_NEW. EXISTS_ONE("exists_one"), + EXISTS_ONE_NEW("existsOne"), MAP("map"), FILTER("filter"), NOT_STRICTLY_FALSE("@not_strictly_false"), @@ -109,6 +111,7 @@ public static Optional find(String text) { .put(EQUALS.getFunction(), EQUALS) .put(EXISTS.getFunction(), EXISTS) .put(EXISTS_ONE.getFunction(), EXISTS_ONE) + .put(EXISTS_ONE_NEW.getFunction(), EXISTS_ONE_NEW) .put(FILTER.getFunction(), FILTER) .put(GREATER.getFunction(), GREATER) .put(GREATER_EQUALS.getFunction(), GREATER_EQUALS) diff --git a/conformance/src/test/java/dev/cel/conformance/BUILD.bazel b/conformance/src/test/java/dev/cel/conformance/BUILD.bazel index 7283e2977..248bc7fe2 100644 --- a/conformance/src/test/java/dev/cel/conformance/BUILD.bazel +++ b/conformance/src/test/java/dev/cel/conformance/BUILD.bazel @@ -22,6 +22,7 @@ java_library( "//common:options", "//common/types:cel_proto_types", "//compiler", + "//compiler:compiler_builder", "//extensions", "//extensions:optional_library", "//parser:macro", @@ -49,6 +50,7 @@ java_library( tags = ["conformance_maven"], deps = MAVEN_JAR_DEPS + [ "//:java_truth", + "//compiler:compiler_builder", "//testing:expr_value_utils", "@cel_spec//proto/cel/expr:expr_java_proto", "@cel_spec//proto/cel/expr/conformance/proto2:test_all_types_java_proto", @@ -75,6 +77,7 @@ _ALL_TESTS = [ "@cel_spec//tests/simple:testdata/lists.textproto", "@cel_spec//tests/simple:testdata/logic.textproto", "@cel_spec//tests/simple:testdata/macros.textproto", + "@cel_spec//tests/simple:testdata/macros2.textproto", "@cel_spec//tests/simple:testdata/math_ext.textproto", "@cel_spec//tests/simple:testdata/namespace.textproto", "@cel_spec//tests/simple:testdata/optionals.textproto", diff --git a/conformance/src/test/java/dev/cel/conformance/ConformanceTest.java b/conformance/src/test/java/dev/cel/conformance/ConformanceTest.java index bc5ecfd12..dcd226fa5 100644 --- a/conformance/src/test/java/dev/cel/conformance/ConformanceTest.java +++ b/conformance/src/test/java/dev/cel/conformance/ConformanceTest.java @@ -35,6 +35,7 @@ import dev.cel.common.CelValidationResult; import dev.cel.common.types.CelProtoTypes; import dev.cel.compiler.CelCompilerFactory; +import dev.cel.compiler.CelCompilerLibrary; import dev.cel.expr.conformance.test.SimpleTest; import dev.cel.extensions.CelExtensions; import dev.cel.extensions.CelOptionalLibrary; @@ -45,6 +46,7 @@ import dev.cel.runtime.CelRuntime; import dev.cel.runtime.CelRuntime.Program; import dev.cel.runtime.CelRuntimeFactory; +import dev.cel.runtime.CelRuntimeLibrary; import java.util.Map; import org.junit.runners.model.Statement; @@ -61,31 +63,37 @@ public final class ConformanceTest extends Statement { .enableQuotedIdentifierSyntax(true) .build(); + private static final ImmutableList CANONICAL_COMPILER_EXTENSIONS = + ImmutableList.of( + CelExtensions.bindings(), + CelExtensions.comprehensions(), + CelExtensions.encoders(OPTIONS), + CelExtensions.math(OPTIONS), + CelExtensions.protos(), + CelExtensions.sets(OPTIONS), + CelExtensions.strings(), + CelOptionalLibrary.INSTANCE); + + private static final ImmutableList CANONICAL_RUNTIME_EXTENSIONS = + ImmutableList.of( + CelExtensions.comprehensions(), + CelExtensions.encoders(OPTIONS), + CelExtensions.math(OPTIONS), + CelExtensions.sets(OPTIONS), + CelExtensions.strings(), + CelOptionalLibrary.INSTANCE); + private static final CelParser PARSER_WITH_MACROS = CelParserFactory.standardCelParserBuilder() .setOptions(OPTIONS) - .addLibraries( - CelExtensions.bindings(), - CelExtensions.encoders(OPTIONS), - CelExtensions.math(OPTIONS), - CelExtensions.protos(), - CelExtensions.sets(OPTIONS), - CelExtensions.strings(), - CelOptionalLibrary.INSTANCE) + .addLibraries(CANONICAL_COMPILER_EXTENSIONS) .setStandardMacros(CelStandardMacro.STANDARD_MACROS) .build(); private static final CelParser PARSER_WITHOUT_MACROS = CelParserFactory.standardCelParserBuilder() .setOptions(OPTIONS) - .addLibraries( - CelExtensions.bindings(), - CelExtensions.encoders(OPTIONS), - CelExtensions.math(OPTIONS), - CelExtensions.protos(), - CelExtensions.sets(OPTIONS), - CelExtensions.strings(), - CelOptionalLibrary.INSTANCE) + .addLibraries(CANONICAL_COMPILER_EXTENSIONS) .setStandardMacros() .build(); @@ -104,13 +112,7 @@ private static CelChecker getChecker(SimpleTest test) throws Exception { .setContainer(CelContainer.ofName(test.getContainer())) .addDeclarations(decls.build()) .addFileTypes(dev.cel.expr.conformance.proto2.TestAllTypesExtensions.getDescriptor()) - .addLibraries( - CelExtensions.bindings(), - CelExtensions.encoders(OPTIONS), - CelExtensions.math(OPTIONS), - CelExtensions.sets(OPTIONS), - CelExtensions.strings(), - CelOptionalLibrary.INSTANCE) + .addLibraries(CANONICAL_COMPILER_EXTENSIONS) .addMessageTypes(dev.cel.expr.conformance.proto2.TestAllTypes.getDescriptor()) .addMessageTypes(dev.cel.expr.conformance.proto3.TestAllTypes.getDescriptor()) .build(); @@ -119,12 +121,7 @@ private static CelChecker getChecker(SimpleTest test) throws Exception { private static final CelRuntime RUNTIME = CelRuntimeFactory.standardCelRuntimeBuilder() .setOptions(OPTIONS) - .addLibraries( - CelExtensions.encoders(OPTIONS), - CelExtensions.math(OPTIONS), - CelExtensions.sets(OPTIONS), - CelExtensions.strings(), - CelOptionalLibrary.INSTANCE) + .addLibraries(CANONICAL_RUNTIME_EXTENSIONS) .setExtensionRegistry(DEFAULT_EXTENSION_REGISTRY) .addMessageTypes(dev.cel.expr.conformance.proto2.TestAllTypes.getDescriptor()) .addMessageTypes(dev.cel.expr.conformance.proto3.TestAllTypes.getDescriptor()) diff --git a/extensions/src/main/java/dev/cel/extensions/CelComprehensionsExtensions.java b/extensions/src/main/java/dev/cel/extensions/CelComprehensionsExtensions.java index ddea801b6..462e1d008 100644 --- a/extensions/src/main/java/dev/cel/extensions/CelComprehensionsExtensions.java +++ b/extensions/src/main/java/dev/cel/extensions/CelComprehensionsExtensions.java @@ -159,6 +159,10 @@ public ImmutableSet macros() { Operator.EXISTS_ONE.getFunction(), 3, CelComprehensionsExtensions::expandExistsOneMacro), + CelMacro.newReceiverMacro( + Operator.EXISTS_ONE_NEW.getFunction(), + 3, + CelComprehensionsExtensions::expandExistsOneMacro), CelMacro.newReceiverMacro( "transformList", 3, CelComprehensionsExtensions::transformListMacro), CelMacro.newReceiverMacro( diff --git a/parser/src/main/java/dev/cel/parser/CelStandardMacro.java b/parser/src/main/java/dev/cel/parser/CelStandardMacro.java index a90acce72..20d30bc17 100644 --- a/parser/src/main/java/dev/cel/parser/CelStandardMacro.java +++ b/parser/src/main/java/dev/cel/parser/CelStandardMacro.java @@ -54,6 +54,14 @@ public enum CelStandardMacro { CelMacro.newReceiverMacro( Operator.EXISTS_ONE.getFunction(), 2, CelStandardMacro::expandExistsOneMacro)), + /** + * Boolean comprehension which asserts that a predicate holds true for exactly one element in the + * input range. + */ + EXISTS_ONE_NEW( + CelMacro.newReceiverMacro( + Operator.EXISTS_ONE_NEW.getFunction(), 2, CelStandardMacro::expandExistsOneMacro)), + /** * Comprehension which applies a transform to each element in the input range and produces a list * of equivalent size as output. diff --git a/parser/src/test/java/dev/cel/parser/CelParserImplTest.java b/parser/src/test/java/dev/cel/parser/CelParserImplTest.java index f12fef292..1e7b44fab 100644 --- a/parser/src/test/java/dev/cel/parser/CelParserImplTest.java +++ b/parser/src/test/java/dev/cel/parser/CelParserImplTest.java @@ -17,6 +17,7 @@ import static com.google.common.truth.Truth.assertThat; import static org.junit.Assert.assertThrows; +import com.google.common.collect.ImmutableSet; import com.google.testing.junit.testparameterinjector.TestParameter; import com.google.testing.junit.testparameterinjector.TestParameterInjector; import com.google.testing.junit.testparameterinjector.TestParameters; @@ -260,10 +261,17 @@ public void parse_exprUnderMaxRecursionLimit_doesNotThrow( @TestParameters("{expression: 'A.all(a?b, c)'}") @TestParameters("{expression: 'A.exists(a?b, c)'}") @TestParameters("{expression: 'A.exists_one(a?b, c)'}") + @TestParameters("{expression: 'A.existsOne(a?b, c)'}") @TestParameters("{expression: 'A.filter(a?b, c)'}") public void parse_macroArgumentContainsSyntaxError_throws(String expression) { CelParser parser = - CelParserImpl.newBuilder().setStandardMacros(CelStandardMacro.STANDARD_MACROS).build(); + CelParserImpl.newBuilder() + .setStandardMacros( + ImmutableSet.builder() + .addAll(CelStandardMacro.STANDARD_MACROS) + .add(CelStandardMacro.EXISTS_ONE_NEW) + .build()) + .build(); CelValidationResult parseResult = parser.parse(expression); diff --git a/parser/src/test/java/dev/cel/parser/CelParserParameterizedTest.java b/parser/src/test/java/dev/cel/parser/CelParserParameterizedTest.java index 0e4490aa6..58b45ddab 100644 --- a/parser/src/test/java/dev/cel/parser/CelParserParameterizedTest.java +++ b/parser/src/test/java/dev/cel/parser/CelParserParameterizedTest.java @@ -26,6 +26,7 @@ import com.google.auto.value.AutoValue; import com.google.common.base.Ascii; import com.google.common.base.Joiner; +import com.google.common.collect.ImmutableSet; import com.google.errorprone.annotations.Immutable; import com.google.protobuf.Descriptors.Descriptor; import com.google.protobuf.Descriptors.EnumDescriptor; @@ -57,7 +58,11 @@ public final class CelParserParameterizedTest extends BaselineTestCase { private static final CelParser PARSER = CelParserFactory.standardCelParserBuilder() - .setStandardMacros(CelStandardMacro.STANDARD_MACROS) + .setStandardMacros( + ImmutableSet.builder() + .addAll(CelStandardMacro.STANDARD_MACROS) + .add(CelStandardMacro.EXISTS_ONE_NEW) + .build()) .addLibraries(CelOptionalLibrary.INSTANCE) .addMacros( CelMacro.newGlobalVarArgMacro("noop_macro", (a, b, c) -> Optional.empty()), @@ -162,6 +167,7 @@ public void parser() { runTest(PARSER, "aaa.bbb(ccc)"); runTest(PARSER, "has(m.f)"); runTest(PARSER, "m.exists_one(v, f)"); + runTest(PARSER, "m.existsOne(v, f)"); runTest(PARSER, "m.map(v, f)"); runTest(PARSER, "m.map(v, p, f)"); runTest(PARSER, "m.filter(v, p)"); diff --git a/parser/src/test/java/dev/cel/parser/CelStandardMacroTest.java b/parser/src/test/java/dev/cel/parser/CelStandardMacroTest.java index 80538930a..7ab96180c 100644 --- a/parser/src/test/java/dev/cel/parser/CelStandardMacroTest.java +++ b/parser/src/test/java/dev/cel/parser/CelStandardMacroTest.java @@ -32,6 +32,8 @@ public void getFunction() { assertThat(CelStandardMacro.EXISTS.getFunction()).isEqualTo(Operator.EXISTS.getFunction()); assertThat(CelStandardMacro.EXISTS_ONE.getFunction()) .isEqualTo(Operator.EXISTS_ONE.getFunction()); + assertThat(CelStandardMacro.EXISTS_ONE_NEW.getFunction()) + .isEqualTo(Operator.EXISTS_ONE_NEW.getFunction()); assertThat(CelStandardMacro.FILTER.getFunction()).isEqualTo(Operator.FILTER.getFunction()); assertThat(CelStandardMacro.MAP.getFunction()).isEqualTo(Operator.MAP.getFunction()); assertThat(CelStandardMacro.MAP_FILTER.getFunction()).isEqualTo(Operator.MAP.getFunction()); @@ -90,6 +92,21 @@ public void testExistsOne() { .isEqualTo(CelStandardMacro.EXISTS_ONE.getDefinition().getKey().hashCode()); } + @Test + public void testExistsOneNew() { + assertThat(CelStandardMacro.EXISTS_ONE_NEW.getFunction()) + .isEqualTo(Operator.EXISTS_ONE_NEW.getFunction()); + assertThat(CelStandardMacro.EXISTS_ONE_NEW.getDefinition().getArgumentCount()).isEqualTo(2); + assertThat(CelStandardMacro.EXISTS_ONE_NEW.getDefinition().isReceiverStyle()).isTrue(); + assertThat(CelStandardMacro.EXISTS_ONE_NEW.getDefinition().getKey()) + .isEqualTo("existsOne:2:true"); + assertThat(CelStandardMacro.EXISTS_ONE_NEW.getDefinition().isVariadic()).isFalse(); + assertThat(CelStandardMacro.EXISTS_ONE_NEW.getDefinition().toString()) + .isEqualTo(CelStandardMacro.EXISTS_ONE_NEW.getDefinition().getKey()); + assertThat(CelStandardMacro.EXISTS_ONE_NEW.getDefinition().hashCode()) + .isEqualTo(CelStandardMacro.EXISTS_ONE_NEW.getDefinition().getKey().hashCode()); + } + @Test public void testMap2() { assertThat(CelStandardMacro.MAP.getFunction()).isEqualTo(Operator.MAP.getFunction()); diff --git a/parser/src/test/resources/parser.baseline b/parser/src/test/resources/parser.baseline index 9b509f61e..37b8ef3cc 100644 --- a/parser/src/test/resources/parser.baseline +++ b/parser/src/test/resources/parser.baseline @@ -784,6 +784,63 @@ M: m^#1:Expr.Ident#.exists_one( f^#4:Expr.Ident# )^#0:Expr.Call# +I: m.existsOne(v, f) +=====> +P: __comprehension__( + // Variable + v, + // Target + m^#1:Expr.Ident#, + // Accumulator + @result, + // Init + 0^#5:int64#, + // LoopCondition + true^#6:bool#, + // LoopStep + _?_:_( + f^#4:Expr.Ident#, + _+_( + @result^#7:Expr.Ident#, + 1^#8:int64# + )^#9:Expr.Call#, + @result^#10:Expr.Ident# + )^#11:Expr.Call#, + // Result + _==_( + @result^#12:Expr.Ident#, + 1^#13:int64# + )^#14:Expr.Call#)^#15:Expr.Comprehension# +L: __comprehension__( + // Variable + v, + // Target + m^#1[1,0]#, + // Accumulator + @result, + // Init + 0^#5[1,11]#, + // LoopCondition + true^#6[1,11]#, + // LoopStep + _?_:_( + f^#4[1,15]#, + _+_( + @result^#7[1,11]#, + 1^#8[1,11]# + )^#9[1,11]#, + @result^#10[1,11]# + )^#11[1,11]#, + // Result + _==_( + @result^#12[1,11]#, + 1^#13[1,11]# + )^#14[1,11]#)^#15[1,11]# +M: m^#1:Expr.Ident#.existsOne( + v^#3:Expr.Ident#, + f^#4:Expr.Ident# +)^#0:Expr.Call# + I: m.map(v, f) =====> P: __comprehension__(