diff --git a/checker/standard_library.cc b/checker/standard_library.cc index 5cb10a116..4cd9e9831 100644 --- a/checker/standard_library.cc +++ b/checker/standard_library.cc @@ -842,17 +842,6 @@ absl::Status AddEnumConstants(TypeCheckerBuilder& builder) { return absl::OkStatus(); } -absl::Status AddComprehensionsV2Functions(TypeCheckerBuilder& builder) { - FunctionDecl map_insert; - map_insert.set_name("@cel.mapInsert"); - CEL_RETURN_IF_ERROR(map_insert.AddOverload( - MakeOverloadDecl("@mapInsert_map_key_value", MapOfAB(), MapOfAB(), - TypeParamA(), TypeParamB()))); - CEL_RETURN_IF_ERROR(map_insert.AddOverload( - MakeOverloadDecl("@mapInsert_map_map", MapOfAB(), MapOfAB(), MapOfAB()))); - return builder.AddFunction(map_insert); -} - absl::Status AddStandardLibraryDecls(TypeCheckerBuilder& builder) { CEL_RETURN_IF_ERROR(AddLogicalOps(builder)); CEL_RETURN_IF_ERROR(AddArithmeticOps(builder)); @@ -865,7 +854,6 @@ absl::Status AddStandardLibraryDecls(TypeCheckerBuilder& builder) { CEL_RETURN_IF_ERROR(AddTimeFunctions(builder)); CEL_RETURN_IF_ERROR(AddTypeConstantVariables(builder)); CEL_RETURN_IF_ERROR(AddEnumConstants(builder)); - CEL_RETURN_IF_ERROR(AddComprehensionsV2Functions(builder)); return absl::OkStatus(); } diff --git a/extensions/BUILD b/extensions/BUILD index e2090a6c7..cf7e571fb 100644 --- a/extensions/BUILD +++ b/extensions/BUILD @@ -207,10 +207,13 @@ cc_library( hdrs = ["bindings_ext.h"], deps = [ "//common:ast", + "//compiler", + "//internal:status_macros", "//parser:macro", "//parser:macro_expr_factory", "//parser:macro_registry", "//parser:options", + "//parser:parser_interface", "@com_google_absl//absl/status", "@com_google_absl//absl/status:statusor", "@com_google_absl//absl/types:optional", @@ -572,40 +575,6 @@ cc_library( ], ) -cc_test( - name = "comprehensions_v2_functions_test", - srcs = ["comprehensions_v2_functions_test.cc"], - deps = [ - ":bindings_ext", - ":comprehensions_v2_functions", - ":comprehensions_v2_macros", - ":strings", - "//common:source", - "//common:value", - "//common:value_testing", - "//extensions/protobuf:runtime_adapter", - "//internal:status_macros", - "//internal:testing", - "//internal:testing_descriptor_pool", - "//parser", - "//parser:macro_registry", - "//parser:options", - "//parser:standard_macros", - "//runtime", - "//runtime:activation", - "//runtime:optional_types", - "//runtime:reference_resolver", - "//runtime:runtime_options", - "//runtime:standard_runtime_builder_factory", - "@com_google_absl//absl/status", - "@com_google_absl//absl/status:status_matchers", - "@com_google_absl//absl/status:statusor", - "@com_google_absl//absl/strings:string_view", - "@com_google_cel_spec//proto/cel/expr:syntax_cc_proto", - "@com_google_protobuf//:protobuf", - ], -) - cc_library( name = "comprehensions_v2_macros", srcs = ["comprehensions_v2_macros.cc"], @@ -613,11 +582,13 @@ cc_library( deps = [ "//common:expr", "//common:operators", + "//compiler", "//internal:status_macros", "//parser:macro", "//parser:macro_expr_factory", "//parser:macro_registry", "//parser:options", + "//parser:parser_interface", "@com_google_absl//absl/base:no_destructor", "@com_google_absl//absl/log:absl_check", "@com_google_absl//absl/status", @@ -627,18 +598,53 @@ cc_library( ], ) -cc_test( - name = "comprehensions_v2_macros_test", - srcs = ["comprehensions_v2_macros_test.cc"], +cc_library( + name = "comprehensions_v2", + srcs = ["comprehensions_v2.cc"], + hdrs = ["comprehensions_v2.h"], deps = [ + ":comprehensions_v2_functions", ":comprehensions_v2_macros", - "//common:source", - "//internal:testing", - "//parser", + "//checker:type_checker_builder", + "//checker/internal:builtins_arena", + "//common:decl", + "//common:type", + "//compiler", + "//internal:status_macros", "//parser:macro_registry", "//parser:options", + "//parser:parser_interface", + "@com_google_absl//absl/base:no_destructor", + "@com_google_absl//absl/status", + ], +) + +cc_test( + name = "comprehensions_v2_test", + srcs = ["comprehensions_v2_test.cc"], + deps = [ + ":bindings_ext", + ":comprehensions_v2", + ":comprehensions_v2_functions", + ":strings", + "//checker:standard_library", + "//checker:validation_result", + "//common:value", + "//common:value_testing", + "//compiler:compiler_factory", + "//compiler:optional", + "//internal:status_macros", + "//internal:testing", + "//internal:testing_descriptor_pool", + "//runtime", + "//runtime:activation", + "//runtime:optional_types", + "//runtime:runtime_options", + "//runtime:standard_runtime_builder_factory", "@com_google_absl//absl/status", "@com_google_absl//absl/status:status_matchers", + "@com_google_absl//absl/status:statusor", + "@com_google_protobuf//:protobuf", ], ) diff --git a/extensions/bindings_ext.cc b/extensions/bindings_ext.cc index 917bba6cc..f097709ca 100644 --- a/extensions/bindings_ext.cc +++ b/extensions/bindings_ext.cc @@ -17,12 +17,16 @@ #include #include +#include "absl/status/status.h" #include "absl/status/statusor.h" #include "absl/types/optional.h" #include "absl/types/span.h" #include "common/ast.h" +#include "compiler/compiler.h" +#include "internal/status_macros.h" #include "parser/macro.h" #include "parser/macro_expr_factory.h" +#include "parser/parser_interface.h" namespace cel::extensions { @@ -36,6 +40,13 @@ bool IsTargetNamespace(const Expr& target) { return target.has_ident_expr() && target.ident_expr().name() == kCelNamespace; } +inline absl::Status ConfigureParser(ParserBuilder& parser_builder) { + for (const Macro& macro : bindings_macros()) { + CEL_RETURN_IF_ERROR(parser_builder.AddMacro(macro)); + } + return absl::OkStatus(); +} + } // namespace std::vector bindings_macros() { @@ -59,4 +70,8 @@ std::vector bindings_macros() { return {*cel_bind}; } +CompilerLibrary BindingsCompilerLibrary() { + return CompilerLibrary("cel.lib.ext.bindings", &ConfigureParser); +} + } // namespace cel::extensions diff --git a/extensions/bindings_ext.h b/extensions/bindings_ext.h index 04eb4da62..a338b24f6 100644 --- a/extensions/bindings_ext.h +++ b/extensions/bindings_ext.h @@ -18,6 +18,7 @@ #include #include "absl/status/status.h" +#include "compiler/compiler.h" #include "parser/macro.h" #include "parser/macro_registry.h" #include "parser/options.h" @@ -33,6 +34,9 @@ inline absl::Status RegisterBindingsMacros(MacroRegistry& registry, return registry.RegisterMacros(bindings_macros()); } +// Declarations for the bindings extension library. +CompilerLibrary BindingsCompilerLibrary(); + } // namespace cel::extensions #endif // THIRD_PARTY_CEL_CPP_EXTENSIONS_BINDINGS_EXT_H_ diff --git a/extensions/comprehensions_v2.cc b/extensions/comprehensions_v2.cc new file mode 100644 index 000000000..122194528 --- /dev/null +++ b/extensions/comprehensions_v2.cc @@ -0,0 +1,68 @@ +// 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. + +#include "extensions/comprehensions_v2.h" + +#include "absl/base/no_destructor.h" +#include "absl/status/status.h" +#include "checker/internal/builtins_arena.h" +#include "checker/type_checker_builder.h" +#include "common/decl.h" +#include "common/type.h" +#include "compiler/compiler.h" +#include "extensions/comprehensions_v2_macros.h" +#include "internal/status_macros.h" +#include "parser/parser_interface.h" + +using ::cel::checker_internal::BuiltinsArena; + +namespace cel::extensions { + +namespace { + +// Arbitrary type parameter name A. +TypeParamType TypeParamA() { return TypeParamType("A"); } + +// Arbitrary type parameter name B. +TypeParamType TypeParamB() { return TypeParamType("B"); } + +Type MapOfAB() { + static absl::NoDestructor kInstance( + MapType(BuiltinsArena(), TypeParamA(), TypeParamB())); + return *kInstance; +} + +absl::Status AddComprehensionsV2Functions(TypeCheckerBuilder& builder) { + FunctionDecl map_insert; + map_insert.set_name("cel.@mapInsert"); + CEL_RETURN_IF_ERROR(map_insert.AddOverload( + MakeOverloadDecl("@mapInsert_map_key_value", MapOfAB(), MapOfAB(), + TypeParamA(), TypeParamB()))); + CEL_RETURN_IF_ERROR(map_insert.AddOverload( + MakeOverloadDecl("@mapInsert_map_map", MapOfAB(), MapOfAB(), MapOfAB()))); + return builder.AddFunction(map_insert); +} + +absl::Status ConfigureParser(ParserBuilder& parser_builder) { + return RegisterComprehensionsV2Macros(parser_builder); +} + +} // namespace + +CompilerLibrary ComprehensionsV2CompilerLibrary() { + return CompilerLibrary("cel.lib.ext.comprev2", &ConfigureParser, + &AddComprehensionsV2Functions); +} + +} // namespace cel::extensions diff --git a/extensions/comprehensions_v2.h b/extensions/comprehensions_v2.h new file mode 100644 index 000000000..75d795572 --- /dev/null +++ b/extensions/comprehensions_v2.h @@ -0,0 +1,35 @@ +// 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. + +#ifndef THIRD_PARTY_CEL_CPP_EXTENSIONS_COMPREHENSIONS_V2_H_ +#define THIRD_PARTY_CEL_CPP_EXTENSIONS_COMPREHENSIONS_V2_H_ + +#include "absl/status/status.h" +#include "compiler/compiler.h" +#include "extensions/comprehensions_v2_functions.h" // IWYU pragma: export +#include "parser/macro_registry.h" +#include "parser/options.h" + +namespace cel::extensions { + +// Registers the macros defined by the comprehension v2 extension. +absl::Status RegisterComprehensionsV2Macros(MacroRegistry& registry, + const ParserOptions& options); + +// Declarations for the comprehensions v2 extension library. +CompilerLibrary ComprehensionsV2CompilerLibrary(); + +} // namespace cel::extensions + +#endif // THIRD_PARTY_CEL_CPP_EXTENSIONS_COMPREHENSIONS_V2_H_ diff --git a/extensions/comprehensions_v2_functions_test.cc b/extensions/comprehensions_v2_functions_test.cc deleted file mode 100644 index 1620239ea..000000000 --- a/extensions/comprehensions_v2_functions_test.cc +++ /dev/null @@ -1,424 +0,0 @@ -// Copyright 2024 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. - -#include "extensions/comprehensions_v2_functions.h" - -#include -#include -#include -#include - -#include "cel/expr/syntax.pb.h" -#include "absl/status/status.h" -#include "absl/status/status_matchers.h" -#include "absl/status/statusor.h" -#include "absl/strings/string_view.h" -#include "common/source.h" -#include "common/value_testing.h" -#include "common/values/list_value_builder.h" -#include "common/values/map_value_builder.h" -#include "extensions/bindings_ext.h" -#include "extensions/comprehensions_v2_macros.h" -#include "extensions/protobuf/runtime_adapter.h" -#include "extensions/strings.h" -#include "internal/status_macros.h" -#include "internal/testing.h" -#include "internal/testing_descriptor_pool.h" -#include "parser/macro_registry.h" -#include "parser/options.h" -#include "parser/parser.h" -#include "parser/standard_macros.h" -#include "runtime/activation.h" -#include "runtime/optional_types.h" -#include "runtime/reference_resolver.h" -#include "runtime/runtime.h" -#include "runtime/runtime_options.h" -#include "runtime/standard_runtime_builder_factory.h" -#include "google/protobuf/arena.h" - -namespace cel::extensions { -namespace { - -using ::absl_testing::IsOk; -using ::absl_testing::IsOkAndHolds; -using ::absl_testing::StatusIs; -using ::cel::test::BoolValueIs; -using ::cel::test::ErrorValueIs; -using ::google::api::expr::parser::EnrichedParse; -using ::testing::TestWithParam; - -struct ComprehensionsV2FunctionsTestCase { - std::string expression; - absl::StatusCode expected_status_code = absl::StatusCode::kOk; - std::string expected_error; -}; - -class ComprehensionsV2FunctionsTest - : public TestWithParam { - protected: - bool enable_optimizations_ = false; - - public: - void SetUp() override { - RuntimeOptions options; - options.enable_qualified_type_identifiers = true; - options.enable_comprehension_list_append = enable_optimizations_; - options.enable_comprehension_mutable_map = enable_optimizations_; - ASSERT_OK_AND_ASSIGN(auto builder, - CreateStandardRuntimeBuilder( - internal::GetTestingDescriptorPool(), options)); - ASSERT_THAT(RegisterStringsFunctions(builder.function_registry(), options), - IsOk()); - ASSERT_THAT( - RegisterComprehensionsV2Functions(builder.function_registry(), options), - IsOk()); - ASSERT_THAT(EnableOptionalTypes(builder), IsOk()); - ASSERT_THAT( - EnableReferenceResolver(builder, ReferenceResolverEnabled::kAlways), - IsOk()); - ASSERT_OK_AND_ASSIGN(runtime_, std::move(builder).Build()); - } - - absl::StatusOr Parse(absl::string_view text) { - CEL_ASSIGN_OR_RETURN(auto source, NewSource(text)); - - ParserOptions options; - options.enable_optional_syntax = true; - - MacroRegistry registry; - CEL_RETURN_IF_ERROR(RegisterStandardMacros(registry, options)); - CEL_RETURN_IF_ERROR(RegisterComprehensionsV2Macros(registry, options)); - CEL_RETURN_IF_ERROR(RegisterBindingsMacros(registry, options)); - - CEL_ASSIGN_OR_RETURN(auto result, - EnrichedParse(*source, registry, options)); - return result.parsed_expr(); - } - - void RunTest(const ComprehensionsV2FunctionsTestCase& test_case) { - ASSERT_OK_AND_ASSIGN(auto ast, Parse(test_case.expression)); - ASSERT_OK_AND_ASSIGN(auto program, - ProtobufRuntimeAdapter::CreateProgram(*runtime_, ast)); - google::protobuf::Arena arena; - Activation activation; - if (test_case.expected_status_code == absl::StatusCode::kOk) { - EXPECT_THAT(program->Evaluate(&arena, activation), - IsOkAndHolds(BoolValueIs(true))) - << test_case.expression; - } else { - EXPECT_THAT( - program->Evaluate(&arena, activation), - IsOkAndHolds(ErrorValueIs(StatusIs(test_case.expected_status_code, - test_case.expected_error)))) - << test_case.expression; - } - } - - protected: - std::unique_ptr runtime_; -}; - -class ComprehensionsV2FunctionsTestWithOptimizations - : public ComprehensionsV2FunctionsTest { - public: - ComprehensionsV2FunctionsTestWithOptimizations() - : ComprehensionsV2FunctionsTest() { - enable_optimizations_ = true; - } -}; - -TEST_P(ComprehensionsV2FunctionsTest, Basic) { RunTest(GetParam()); } - -TEST_P(ComprehensionsV2FunctionsTestWithOptimizations, Optimized) { - RunTest(GetParam()); -} - -std::vector GetTestCases() { - return std::vector({ - // list.all() - {.expression = "[1, 2, 3, 4].all(i, v, i < 5 && v > 0)"}, - {.expression = "[1, 2, 3, 4].all(i, v, i < v)"}, - {.expression = "[1, 2, 3, 4].all(i, v, i > v) == false"}, - { - .expression = - R"cel(cel.bind(listA, [1, 2, 3, 4], cel.bind(listB, [1, 2, 3, 4, 5], listA.all(i, v, listB[?i].hasValue() && listB[i] == v))))cel", - }, - { - .expression = - R"cel(cel.bind(listA, [1, 2, 3, 4, 5, 6], cel.bind(listB, [1, 2, 3, 4, 5], listA.all(i, v, listB[?i].hasValue() && listB[i] == v))) == false)cel", - }, - // list.exists() - { - .expression = - R"cel(cel.bind(l, ['hello', 'world', 'hello!', 'worlds'], l.exists(i, v, v.startsWith('hello') && l[?(i+1)].optMap(next, next.endsWith('world')).orValue(false))))cel", - }, - // list.existsOne() - { - .expression = - R"cel(cel.bind(l, ['hello', 'world', 'hello!', 'worlds'], l.existsOne(i, v, v.startsWith('hello') && l[?(i+1)].optMap(next, next.endsWith('world')).orValue(false))))cel", - }, - { - .expression = - R"cel(cel.bind(l, ['hello', 'goodbye', 'hello!', 'goodbye'], l.existsOne(i, v, v.startsWith('hello') && l[?(i+1)].optMap(next, next == "goodbye").orValue(false))) == false)cel", - }, - // list.transformList() - { - .expression = - R"cel(['Hello', 'world'].transformList(i, v, "[" + string(i) + "]" + v.lowerAscii()) == ["[0]hello", "[1]world"])cel", - }, - { - .expression = - R"cel(['hello', 'world'].transformList(i, v, v.startsWith('greeting'), "[" + string(i) + "]" + v) == [])cel", - }, - { - .expression = - R"cel([1, 2, 3].transformList(indexVar, valueVar, (indexVar * valueVar) + valueVar) == [1, 4, 9])cel", - }, - { - .expression = - R"cel([1, 2, 3].transformList(indexVar, valueVar, indexVar % 2 == 0, (indexVar * valueVar) + valueVar) == [1, 9])cel", - }, - // list.transformMap() - { - .expression = - R"cel(['Hello', 'world'].transformMap(i, v, [v.lowerAscii()]) == {0: ['hello'], 1: ['world']})cel", - }, - { - .expression = - R"cel([1, 2, 3].transformMap(indexVar, valueVar, (indexVar * valueVar) + valueVar) == {0: 1, 1: 4, 2: 9})cel", - }, - { - .expression = - R"cel([1, 2, 3].transformMap(indexVar, valueVar, indexVar % 2 == 0, (indexVar * valueVar) + valueVar) == {0: 1, 2: 9})cel", - }, - // map.all() - { - .expression = - R"cel({'hello': 'world', 'hello!': 'world'}.all(k, v, k.startsWith('hello') && v == 'world'))cel", - }, - { - .expression = - R"cel({'hello': 'world', 'hello!': 'worlds'}.all(k, v, k.startsWith('hello') && v.endsWith('world')) == false)cel", - }, - // map.exists() - { - .expression = - R"cel({'hello': 'world', 'hello!': 'worlds'}.exists(k, v, k.startsWith('hello') && v.endsWith('world')))cel", - }, - // map.existsOne() - { - .expression = - R"cel({'hello': 'world', 'hello!': 'worlds'}.existsOne(k, v, k.startsWith('hello') && v.endsWith('world')))cel", - }, - { - .expression = - R"cel({'hello': 'world', 'hello!': 'wow, world'}.existsOne(k, v, k.startsWith('hello') && v.endsWith('world')) == false)cel", - }, - // map.transformList() - { - .expression = - R"cel({'Hello': 'world'}.transformList(k, v, k.lowerAscii() + "=" + v) == ["hello=world"])cel", - }, - { - .expression = - R"cel({'hello': 'world'}.transformList(k, v, k.startsWith('greeting'), k + "=" + v) == [])cel", - }, - { - .expression = - R"cel(cel.bind(m, {'farewell': 'goodbye', 'greeting': 'hello'}.transformList(k, _, k), m == ['farewell', 'greeting'] || m == ['greeting', 'farewell']))cel", - }, - { - .expression = - R"cel(cel.bind(m, {'greeting': 'hello', 'farewell': 'goodbye'}.transformList(_, v, v), m == ['goodbye', 'hello'] || m == ['hello', 'goodbye']))cel", - }, - // map.transformMap() - { - .expression = - R"cel({'hello': 'world', 'goodbye': 'cruel world'}.transformMap(k, v, k + ", " + v + "!") == {'hello': 'hello, world!', 'goodbye': 'goodbye, cruel world!'})cel", - }, - { - .expression = - R"cel({'hello': 'world', 'goodbye': 'cruel world'}.transformMap(k, v, v.startsWith('world'), k + ", " + v + "!") == {'hello': 'hello, world!'})cel", - }, - // map.transformMapEntry - { - .expression = - R"cel({'hello': 'world', 'greetings': 'tacocat'}.transformMapEntry(k, v, {v: k}) == {'world': 'hello', 'tacocat': 'greetings'})cel", - }, - { - .expression = - R"cel({'hello': 'world', 'same': 'same'}.transformMapEntry(k, v, k != v, {v: k}) == {'world': 'hello'})cel", - }, - { - .expression = - R"cel({'hello': 'world', 'greetings': 'tacocat'}.transformMapEntry(k, v, {}) == {})cel", - }, - { - .expression = - R"cel({'a': 'same', 'c': 'same'}.transformMapEntry(k, v, {v: k}))cel", - .expected_status_code = absl::StatusCode::kAlreadyExists, - .expected_error = "duplicate key in map", - }, - // list.transformMapEntry - { - .expression = - R"cel(['one', 'two'].transformMapEntry(k, v, {k + 1: 'is ' + v}) == {1: 'is one', 2: 'is two'})cel", - }, - }); -}; - -INSTANTIATE_TEST_SUITE_P(ComprehensionsV2FunctionsTest, - ComprehensionsV2FunctionsTest, - ::testing::ValuesIn(GetTestCases())); - -INSTANTIATE_TEST_SUITE_P(ComprehensionsV2FunctionsTest, - ComprehensionsV2FunctionsTestWithOptimizations, - ::testing::ValuesIn(GetTestCases())); - -struct ComprehensionsV2FunctionsTestCaseMutableAccumulator { - std::string expression; - bool enable_optimizations; - int max_recursion_depth = 0; - bool expected_mutable_accumulator; -}; - -using ComprehensionsV2FunctionsTestMutableAccumulator = - ::testing::TestWithParam< - ComprehensionsV2FunctionsTestCaseMutableAccumulator>; - -TEST_P(ComprehensionsV2FunctionsTestMutableAccumulator, MutableAccumulator) { - ASSERT_OK_AND_ASSIGN(auto source, NewSource(GetParam().expression)); - - RuntimeOptions options; - options.enable_comprehension_list_append = GetParam().enable_optimizations; - options.enable_comprehension_mutable_map = GetParam().enable_optimizations; - options.max_recursion_depth = GetParam().max_recursion_depth; - - MacroRegistry registry; - ASSERT_THAT(RegisterComprehensionsV2Macros(registry, ParserOptions()), - IsOk()); - - ASSERT_OK_AND_ASSIGN(auto parsed_expr, - EnrichedParse(*source, registry, ParserOptions())); - - ASSERT_OK_AND_ASSIGN(auto builder, - CreateStandardRuntimeBuilder( - internal::GetTestingDescriptorPool(), options)); - ASSERT_THAT( - RegisterComprehensionsV2Functions(builder.function_registry(), options), - IsOk()); - ASSERT_OK_AND_ASSIGN(auto runtime, std::move(builder).Build()); - - ASSERT_OK_AND_ASSIGN(auto program, ProtobufRuntimeAdapter::CreateProgram( - *runtime, parsed_expr.parsed_expr())); - - google::protobuf::Arena arena; - Activation activation; - - ASSERT_OK_AND_ASSIGN(auto result, program->Evaluate(&arena, activation)); - bool is_mutable_accumulator = common_internal::IsMutableListValue(result) || - common_internal::IsMutableMapValue(result); - EXPECT_EQ(is_mutable_accumulator, GetParam().expected_mutable_accumulator); -} - -INSTANTIATE_TEST_SUITE_P( - ComprehensionsV2FunctionsTest, - ComprehensionsV2FunctionsTestMutableAccumulator, - ::testing::ValuesIn({ - // list.transformList() - { - .expression = R"cel(['Hello', 'world'].transformList(i, v, i))cel", - .enable_optimizations = false, - .expected_mutable_accumulator = false, - }, - { - .expression = R"cel(['Hello', 'world'].transformList(i, v, i))cel", - .enable_optimizations = true, - .expected_mutable_accumulator = true, - }, - // map.transformMap() - { - .expression = R"cel({'hello': 'world'}.transformMap(k, v, k))cel", - .enable_optimizations = false, - .expected_mutable_accumulator = false, - }, - { - .expression = R"cel({'hello': 'world'}.transformMap(k, v, k))cel", - .enable_optimizations = true, - .expected_mutable_accumulator = true, - }, - { - .expression = R"cel({'hello': 'world'}.transformMap(k, v, k))cel", - .enable_optimizations = true, - .max_recursion_depth = -1, - .expected_mutable_accumulator = true, - }, - // list.transformMap() - { - .expression = R"cel(['hello'].transformMap(k, v, k))cel", - .enable_optimizations = false, - .expected_mutable_accumulator = false, - }, - { - .expression = R"cel(['hello'].transformMap(k, v, k))cel", - .enable_optimizations = true, - .expected_mutable_accumulator = true, - }, - { - .expression = R"cel(['hello'].transformMap(k, v, k))cel", - .enable_optimizations = true, - .max_recursion_depth = -1, - .expected_mutable_accumulator = true, - }, - // map.transformMapEntry() - { - .expression = - R"cel({'hello': 'world'}.transformMapEntry(k, v, {v: k}))cel", - .enable_optimizations = false, - .expected_mutable_accumulator = false, - }, - { - .expression = - R"cel({'hello': 'world'}.transformMapEntry(k, v, {v: k}))cel", - .enable_optimizations = true, - .expected_mutable_accumulator = true, - }, - { - .expression = - R"cel({'hello': 'world'}.transformMapEntry(k, v, {v: k}))cel", - .enable_optimizations = true, - .max_recursion_depth = -1, - .expected_mutable_accumulator = true, - }, - // list.transformMapEntry() - { - .expression = R"cel(['hello'].transformMapEntry(k, v, {v: k}))cel", - .enable_optimizations = false, - .expected_mutable_accumulator = false, - }, - { - .expression = R"cel(['hello'].transformMapEntry(k, v, {v: k}))cel", - .enable_optimizations = true, - .expected_mutable_accumulator = true, - }, - { - .expression = R"cel(['hello'].transformMapEntry(k, v, {v: k}))cel", - .enable_optimizations = true, - .max_recursion_depth = -1, - .expected_mutable_accumulator = true, - }, - })); - -} // namespace -} // namespace cel::extensions diff --git a/extensions/comprehensions_v2_macros.cc b/extensions/comprehensions_v2_macros.cc index 2ed4218f7..a8de3a103 100644 --- a/extensions/comprehensions_v2_macros.cc +++ b/extensions/comprehensions_v2_macros.cc @@ -15,6 +15,7 @@ #include "extensions/comprehensions_v2_macros.h" #include +#include #include "absl/base/no_destructor.h" #include "absl/log/absl_check.h" @@ -29,6 +30,7 @@ #include "parser/macro_expr_factory.h" #include "parser/macro_registry.h" #include "parser/options.h" +#include "parser/parser_interface.h" namespace cel::extensions { @@ -529,20 +531,33 @@ const Macro& TransformMapEntry4Macro() { } // namespace +std::vector AllMacros() { + return {AllMacro2(), + ExistsMacro2(), + ExistsOneMacro2(), + TransformList3Macro(), + TransformList4Macro(), + TransformMap3Macro(), + TransformMap4Macro(), + TransformMapEntry3Macro(), + TransformMapEntry4Macro()}; +} + // Registers the macros defined by the comprehension v2 extension. absl::Status RegisterComprehensionsV2Macros(MacroRegistry& registry, const ParserOptions&) { - // TODO(uncreated-issue/85): add CompilerLibrary declaration - - CEL_RETURN_IF_ERROR(registry.RegisterMacro(AllMacro2())); - CEL_RETURN_IF_ERROR(registry.RegisterMacro(ExistsMacro2())); - CEL_RETURN_IF_ERROR(registry.RegisterMacro(ExistsOneMacro2())); - CEL_RETURN_IF_ERROR(registry.RegisterMacro(TransformList3Macro())); - CEL_RETURN_IF_ERROR(registry.RegisterMacro(TransformList4Macro())); - CEL_RETURN_IF_ERROR(registry.RegisterMacro(TransformMap3Macro())); - CEL_RETURN_IF_ERROR(registry.RegisterMacro(TransformMap4Macro())); - CEL_RETURN_IF_ERROR(registry.RegisterMacro(TransformMapEntry3Macro())); - CEL_RETURN_IF_ERROR(registry.RegisterMacro(TransformMapEntry4Macro())); + for (const Macro& macro : AllMacros()) { + CEL_RETURN_IF_ERROR(registry.RegisterMacro(macro)); + } + + return absl::OkStatus(); +} + +absl::Status RegisterComprehensionsV2Macros(ParserBuilder& parser_builder) { + for (const Macro& macro : AllMacros()) { + CEL_RETURN_IF_ERROR(parser_builder.AddMacro(macro)); + } + return absl::OkStatus(); } diff --git a/extensions/comprehensions_v2_macros.h b/extensions/comprehensions_v2_macros.h index 3b2bfd577..fed6e9284 100644 --- a/extensions/comprehensions_v2_macros.h +++ b/extensions/comprehensions_v2_macros.h @@ -16,6 +16,7 @@ #define THIRD_PARTY_CEL_CPP_EXTENSIONS_COMPREHENSIONS_V2_MACROS_H_ #include "absl/status/status.h" +#include "compiler/compiler.h" #include "parser/macro_registry.h" #include "parser/options.h" @@ -25,6 +26,9 @@ namespace cel::extensions { absl::Status RegisterComprehensionsV2Macros(MacroRegistry& registry, const ParserOptions& options); +// Registers the macros defined by the comprehension v2 extension. +absl::Status RegisterComprehensionsV2Macros(ParserBuilder& parser_builder); + } // namespace cel::extensions #endif // THIRD_PARTY_CEL_CPP_EXTENSIONS_COMPREHENSIONS_V2_MACROS_H_ diff --git a/extensions/comprehensions_v2_macros_test.cc b/extensions/comprehensions_v2_macros_test.cc deleted file mode 100644 index 97eef6433..000000000 --- a/extensions/comprehensions_v2_macros_test.cc +++ /dev/null @@ -1,253 +0,0 @@ -// Copyright 2024 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. - -#include "extensions/comprehensions_v2_macros.h" - -#include - -#include "absl/status/status.h" -#include "absl/status/status_matchers.h" -#include "common/source.h" -#include "internal/testing.h" -#include "parser/macro_registry.h" -#include "parser/options.h" -#include "parser/parser.h" - -namespace cel::extensions { -namespace { - -using ::absl_testing::IsOk; -using ::absl_testing::StatusIs; -using ::google::api::expr::parser::EnrichedParse; -using ::testing::HasSubstr; - -struct ComprehensionsV2MacrosTestCase { - std::string expression; - std::string error; -}; - -using ComprehensionsV2MacrosTest = - ::testing::TestWithParam; - -TEST_P(ComprehensionsV2MacrosTest, Basic) { - const auto& test_param = GetParam(); - ASSERT_OK_AND_ASSIGN(auto source, NewSource(test_param.expression)); - - MacroRegistry registry; - ASSERT_THAT(RegisterComprehensionsV2Macros(registry, ParserOptions()), - IsOk()); - - EXPECT_THAT(EnrichedParse(*source, registry), - StatusIs(absl::StatusCode::kInvalidArgument, - HasSubstr(test_param.error))); -} - -INSTANTIATE_TEST_SUITE_P( - ComprehensionsV2MacrosTest, ComprehensionsV2MacrosTest, - ::testing::ValuesIn({ - { - .expression = "[].all(__result__, v, v == 0)", - .error = "variable name cannot be __result__", - }, - { - .expression = "[].all(i, __result__, i == 0)", - .error = "variable name cannot be __result__", - }, - { - .expression = "[].all(e, e, e == e)", - .error = - "second variable must be different from the first variable", - }, - { - .expression = "[].all(foo.bar, e, true)", - .error = "first variable name must be a simple identifier", - }, - { - .expression = "[].all(e, foo.bar, true)", - .error = "second variable name must be a simple identifier", - }, - { - .expression = "[].exists(__result__, v, v == 0)", - .error = "variable name cannot be __result__", - }, - { - .expression = "[].exists(i, __result__, i == 0)", - .error = "variable name cannot be __result__", - }, - { - .expression = "[].exists(e, e, e == e)", - .error = - "second variable must be different from the first variable", - }, - { - .expression = "[].exists(foo.bar, e, true)", - .error = "first variable name must be a simple identifier", - }, - { - .expression = "[].exists(e, foo.bar, true)", - .error = "second variable name must be a simple identifier", - }, - { - .expression = "[].existsOne(__result__, v, v == 0)", - .error = "variable name cannot be __result__", - }, - { - .expression = "[].existsOne(i, __result__, i == 0)", - .error = "variable name cannot be __result__", - }, - { - .expression = "[].existsOne(e, e, e == e)", - .error = - "second variable must be different from the first variable", - }, - { - .expression = "[].existsOne(foo.bar, e, true)", - .error = "first variable name must be a simple identifier", - }, - { - .expression = "[].existsOne(e, foo.bar, true)", - .error = "second variable name must be a simple identifier", - }, - { - .expression = "[].transformList(__result__, v, v)", - .error = "variable name cannot be __result__", - }, - { - .expression = "[].transformList(i, __result__, v)", - .error = "variable name cannot be __result__", - }, - { - .expression = "[].transformList(e, e, e)", - .error = - "second variable must be different from the first variable", - }, - { - .expression = "[].transformList(foo.bar, e, e)", - .error = "first variable name must be a simple identifier", - }, - { - .expression = "[].transformList(e, foo.bar, e)", - .error = "second variable name must be a simple identifier", - }, - { - .expression = "[].transformList(__result__, v, v == 0, v)", - .error = "variable name cannot be __result__", - }, - { - .expression = "[].transformList(i, __result__, i == 0, v)", - .error = "variable name cannot be __result__", - }, - { - .expression = "[].transformList(e, e, e == e, e)", - .error = - "second variable must be different from the first variable", - }, - { - .expression = "[].transformList(foo.bar, e, true, e)", - .error = "first variable name must be a simple identifier", - }, - { - .expression = "[].transformList(e, foo.bar, true, e)", - .error = "second variable name must be a simple identifier", - }, - { - .expression = "{}.transformMap(__result__, v, v)", - .error = "variable name cannot be __result__", - }, - { - .expression = "{}.transformMap(k, __result__, v)", - .error = "variable name cannot be __result__", - }, - { - .expression = "{}.transformMap(e, e, e)", - .error = - "second variable must be different from the first variable", - }, - { - .expression = "{}.transformMap(foo.bar, e, e)", - .error = "first variable name must be a simple identifier", - }, - { - .expression = "{}.transformMap(e, foo.bar, e)", - .error = "second variable name must be a simple identifier", - }, - { - .expression = "{}.transformMap(__result__, v, v == 0, v)", - .error = "variable name cannot be __result__", - }, - { - .expression = "{}.transformMap(k, __result__, k == 0, v)", - .error = "variable name cannot be __result__", - }, - { - .expression = "{}.transformMap(e, e, e == e, e)", - .error = - "second variable must be different from the first variable", - }, - { - .expression = "{}.transformMap(foo.bar, e, true, e)", - .error = "first variable name must be a simple identifier", - }, - { - .expression = "{}.transformMap(e, foo.bar, true, e)", - .error = "second variable name must be a simple identifier", - }, - // transformMapEntry(k, v, expr) - { - .expression = "{}.transformMapEntry(__result__, v, v)", - .error = "variable name cannot be __result__", - }, - { - .expression = "{}.transformMapEntry(k, __result__, v)", - .error = "variable name cannot be __result__", - }, - { - .expression = "{}.transformMapEntry(e, e, e)", - .error = - "second variable must be different from the first variable", - }, - { - .expression = "{}.transformMapEntry(foo.bar, e, e)", - .error = "first variable name must be a simple identifier", - }, - { - .expression = "{}.transformMapEntry(e, foo.bar, e)", - .error = "second variable name must be a simple identifier", - }, - // transformMapEntry(k, v, filter, expr) - { - .expression = "{}.transformMapEntry(__result__, v, v == 0, v)", - .error = "variable name cannot be __result__", - }, - { - .expression = "{}.transformMapEntry(k, __result__, k == 0, v)", - .error = "variable name cannot be __result__", - }, - { - .expression = "{}.transformMapEntry(e, e, e == e, e)", - .error = - "second variable must be different from the first variable", - }, - { - .expression = "{}.transformMapEntry(foo.bar, e, true, e)", - .error = "first variable name must be a simple identifier", - }, - { - .expression = "{}.transformMapEntry(e, foo.bar, true, e)", - .error = "second variable name must be a simple identifier", - }, - })); - -} // namespace -} // namespace cel::extensions diff --git a/extensions/comprehensions_v2_test.cc b/extensions/comprehensions_v2_test.cc new file mode 100644 index 000000000..25645af5c --- /dev/null +++ b/extensions/comprehensions_v2_test.cc @@ -0,0 +1,575 @@ +// 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. + +#include "extensions/comprehensions_v2.h" + +#include +#include +#include +#include + +#include "absl/status/status.h" +#include "absl/status/status_matchers.h" +#include "absl/status/statusor.h" +#include "checker/standard_library.h" +#include "checker/validation_result.h" +#include "common/value_testing.h" +#include "common/values/list_value_builder.h" +#include "common/values/map_value_builder.h" +#include "compiler/compiler_factory.h" +#include "compiler/optional.h" +#include "extensions/bindings_ext.h" +#include "extensions/comprehensions_v2_functions.h" +#include "extensions/strings.h" +#include "internal/status_macros.h" +#include "internal/testing.h" +#include "internal/testing_descriptor_pool.h" +#include "runtime/activation.h" +#include "runtime/optional_types.h" +#include "runtime/runtime.h" +#include "runtime/runtime_options.h" +#include "runtime/standard_runtime_builder_factory.h" +#include "google/protobuf/arena.h" + +namespace cel::extensions { +namespace { + +using ::absl_testing::IsOk; +using ::absl_testing::IsOkAndHolds; +using ::absl_testing::StatusIs; +using ::cel::test::BoolValueIs; +using ::cel::test::ErrorValueIs; +using ::testing::HasSubstr; +using ::testing::TestWithParam; + +absl::StatusOr> CreateProgram( + const std::string& expression, bool enable_mutable_accumulator, + int max_recursion_depth) { + // Configure the compiler + CEL_ASSIGN_OR_RETURN( + auto compiler_builder, + NewCompilerBuilder(internal::GetTestingDescriptorPool())); + CEL_RETURN_IF_ERROR(compiler_builder->AddLibrary(StandardCheckerLibrary())); + CEL_RETURN_IF_ERROR(compiler_builder->AddLibrary(OptionalCompilerLibrary())); + CEL_RETURN_IF_ERROR(compiler_builder->AddLibrary(BindingsCompilerLibrary())); + CEL_RETURN_IF_ERROR(compiler_builder->AddLibrary(StringsCompilerLibrary())); + CEL_RETURN_IF_ERROR(compiler_builder->AddLibrary( + extensions::ComprehensionsV2CompilerLibrary())); + + CEL_ASSIGN_OR_RETURN(auto compiler, std::move(*compiler_builder).Build()); + + // Configure the runtime + cel::RuntimeOptions options; + options.enable_qualified_type_identifiers = true; + options.enable_comprehension_list_append = enable_mutable_accumulator; + options.enable_comprehension_mutable_map = enable_mutable_accumulator; + options.max_recursion_depth = max_recursion_depth; + + CEL_ASSIGN_OR_RETURN(auto runtime_builder, + CreateStandardRuntimeBuilder( + internal::GetTestingDescriptorPool(), options)); + CEL_RETURN_IF_ERROR(EnableOptionalTypes(runtime_builder)); + CEL_RETURN_IF_ERROR( + RegisterStringsFunctions(runtime_builder.function_registry(), options)); + CEL_RETURN_IF_ERROR(RegisterComprehensionsV2Functions( + runtime_builder.function_registry(), options)); + CEL_ASSIGN_OR_RETURN(std::unique_ptr runtime, + std::move(runtime_builder).Build()); + + CEL_ASSIGN_OR_RETURN(ValidationResult result, compiler->Compile(expression)); + if (!result.IsValid()) { + return absl::Status(absl::StatusCode::kInvalidArgument, + result.FormatError()); + } + return runtime->CreateProgram(*result.ReleaseAst()); +} + +struct TestOptions { + bool enable_mutable_accumulator; + int max_recursion_depth; +}; + +struct ComprehensionsV2TestCase { + std::string expression; + absl::StatusCode expected_status_code = absl::StatusCode::kOk; + std::string expected_error; +}; + +class ComprehensionsV2Test + : public TestWithParam> { +}; + +TEST_P(ComprehensionsV2Test, Basic) { + const ComprehensionsV2TestCase& test_case = std::get<0>(GetParam()); + const TestOptions& options = std::get<1>(GetParam()); + + absl::StatusOr> program = + CreateProgram(test_case.expression, options.enable_mutable_accumulator, + options.max_recursion_depth); + + if (!program.ok()) { + EXPECT_THAT(program, StatusIs(absl::StatusCode::kInvalidArgument, + HasSubstr(test_case.expected_error))); + // The error is expected. Nothing more to do in this test case + return; + } + + ASSERT_THAT(program, IsOk()); + + google::protobuf::Arena arena; + Activation activation; + + if (test_case.expected_status_code == absl::StatusCode::kOk) { + EXPECT_THAT(program.value()->Evaluate(&arena, activation), + IsOkAndHolds(BoolValueIs(true))) + << test_case.expression; + } else { + EXPECT_THAT(program.value()->Evaluate(&arena, activation), + IsOkAndHolds(ErrorValueIs(StatusIs( + test_case.expected_status_code, test_case.expected_error)))) + << test_case.expression; + } +} + +INSTANTIATE_TEST_SUITE_P( + ComprehensionsV2Test, ComprehensionsV2Test, + ::testing::Combine( + ::testing::ValuesIn({ + // list.all() + {.expression = "[1, 2, 3, 4].all(i, v, i < 5 && v > 0)"}, + {.expression = "[1, 2, 3, 4].all(i, v, i < v)"}, + {.expression = "[1, 2, 3, 4].all(i, v, i > v) == false"}, + { + .expression = + R"cel(cel.bind(listA, [1, 2, 3, 4], cel.bind(listB, [1, 2, 3, 4, 5], listA.all(i, v, listB[?i].hasValue() && listB[i] == v))))cel", + }, + { + .expression = + R"cel(cel.bind(listA, [1, 2, 3, 4, 5, 6], cel.bind(listB, [1, 2, 3, 4, 5], listA.all(i, v, listB[?i].hasValue() && listB[i] == v))) == false)cel", + }, + { + .expression = "[].all(__result__, v, v == 0)", + .expected_error = "variable name cannot be __result__", + }, + { + .expression = "[].all(__result__, v, v == 0)", + .expected_error = "variable name cannot be __result__", + }, + { + .expression = "[].all(i, __result__, i == 0)", + .expected_error = "variable name cannot be __result__", + }, + { + .expression = "[].all(e, e, e == e)", + .expected_error = + "second variable must be different from the first variable", + }, + { + .expression = "[].all(foo.bar, e, true)", + .expected_error = + "first variable name must be a simple identifier", + }, + { + .expression = "[].all(e, foo.bar, true)", + .expected_error = + "second variable name must be a simple identifier", + }, + + // list.exists() + { + .expression = + R"cel(cel.bind(l, ['hello', 'world', 'hello!', 'worlds'], l.exists(i, v, v.startsWith('hello') && l[?(i+1)].optMap(next, next.endsWith('world')).orValue(false))))cel", + }, + { + .expression = "[].exists(__result__, v, v == 0)", + .expected_error = "variable name cannot be __result__", + }, + { + .expression = "[].exists(i, __result__, i == 0)", + .expected_error = "variable name cannot be __result__", + }, + { + .expression = "[].exists(e, e, e == e)", + .expected_error = + "second variable must be different from the first variable", + }, + { + .expression = "[].exists(foo.bar, e, true)", + .expected_error = + "first variable name must be a simple identifier", + }, + { + .expression = "[].exists(e, foo.bar, true)", + .expected_error = + "second variable name must be a simple identifier", + }, + // list.existsOne() + { + .expression = + R"cel(cel.bind(l, ['hello', 'world', 'hello!', 'worlds'], l.existsOne(i, v, v.startsWith('hello') && l[?(i+1)].optMap(next, next.endsWith('world')).orValue(false))))cel", + }, + { + .expression = + R"cel(cel.bind(l, ['hello', 'goodbye', 'hello!', 'goodbye'], l.existsOne(i, v, v.startsWith('hello') && l[?(i+1)].optMap(next, next == 'goodbye').orValue(false))) == false)cel", + }, + { + .expression = "[].existsOne(__result__, v, v == 0)", + .expected_error = "variable name cannot be __result__", + }, + { + .expression = "[].existsOne(i, __result__, i == 0)", + .expected_error = "variable name cannot be __result__", + }, + { + .expression = "[].existsOne(e, e, e == e)", + .expected_error = + "second variable must be different from the first variable", + }, + { + .expression = "[].existsOne(foo.bar, e, true)", + .expected_error = + "first variable name must be a simple identifier", + }, + { + .expression = "[].existsOne(e, foo.bar, true)", + .expected_error = + "second variable name must be a simple identifier", + }, + // list.transformList() + { + .expression = + R"cel(['Hello', 'world'].transformList(i, v, '[' + string(i) + ']' + v.lowerAscii()) == ['[0]hello', '[1]world'])cel", + }, + { + .expression = + R"cel(['hello', 'world'].transformList(i, v, v.startsWith('greeting'), '[' + string(i) + ']' + v) == [])cel", + }, + { + .expression = + R"cel([1, 2, 3].transformList(indexVar, valueVar, (indexVar * valueVar) + valueVar) == [1, 4, 9])cel", + }, + { + .expression = + R"cel([1, 2, 3].transformList(indexVar, valueVar, indexVar % 2 == 0, (indexVar * valueVar) + valueVar) == [1, 9])cel", + }, + { + .expression = "[].transformList(__result__, v, v)", + .expected_error = "variable name cannot be __result__", + }, + { + .expression = "[].transformList(i, __result__, v)", + .expected_error = "variable name cannot be __result__", + }, + { + .expression = "[].transformList(e, e, e)", + .expected_error = + "second variable must be different from the first variable", + }, + { + .expression = "[].transformList(foo.bar, e, e)", + .expected_error = + "first variable name must be a simple identifier", + }, + { + .expression = "[].transformList(e, foo.bar, e)", + .expected_error = + "second variable name must be a simple identifier", + }, + { + .expression = "[].transformList(__result__, v, v == 0, v)", + .expected_error = "variable name cannot be __result__", + }, + { + .expression = "[].transformList(i, __result__, i == 0, v)", + .expected_error = "variable name cannot be __result__", + }, + { + .expression = "[].transformList(e, e, e == e, e)", + .expected_error = + "second variable must be different from the first variable", + }, + { + .expression = "[].transformList(foo.bar, e, true, e)", + .expected_error = + "first variable name must be a simple identifier", + }, + { + .expression = "[].transformList(e, foo.bar, true, e)", + .expected_error = + "second variable name must be a simple identifier", + }, + // list.transformMap() + { + .expression = + R"cel(['Hello', 'world'].transformMap(i, v, [v.lowerAscii()]) == {0: ['hello'], 1: ['world']})cel", + }, + { + .expression = + R"cel([1, 2, 3].transformMap(indexVar, valueVar, (indexVar * valueVar) + valueVar) == {0: 1, 1: 4, 2: 9})cel", + }, + { + .expression = + R"cel([1, 2, 3].transformMap(indexVar, valueVar, indexVar % 2 == 0, (indexVar * valueVar) + valueVar) == {0: 1, 2: 9})cel", + }, + // map.all() + { + .expression = + R"cel({'hello': 'world', 'hello!': 'world'}.all(k, v, k.startsWith('hello') && v == 'world'))cel", + }, + { + .expression = + R"cel({'hello': 'world', 'hello!': 'worlds'}.all(k, v, k.startsWith('hello') && v.endsWith('world')) == false)cel", + }, + // map.exists() + { + .expression = + R"cel({'hello': 'world', 'hello!': 'worlds'}.exists(k, v, k.startsWith('hello') && v.endsWith('world')))cel", + }, + // map.existsOne() + { + .expression = + R"cel({'hello': 'world', 'hello!': 'worlds'}.existsOne(k, v, k.startsWith('hello') && v.endsWith('world')))cel", + }, + { + .expression = + R"cel({'hello': 'world', 'hello!': 'wow, world'}.existsOne(k, v, k.startsWith('hello') && v.endsWith('world')) == false)cel", + }, + // map.transformList() + { + .expression = + R"cel({'Hello': 'world'}.transformList(k, v, k.lowerAscii() + "=" + v) == ['hello=world'])cel", + }, + { + .expression = + R"cel({'hello': 'world'}.transformList(k, v, k.startsWith('greeting'), k + "=" + v) == [])cel", + }, + { + .expression = + R"cel(cel.bind(m, {'farewell': 'goodbye', 'greeting': 'hello'}.transformList(k, _, k), m == ['farewell', 'greeting'] || m == ['greeting', 'farewell']))cel", + }, + { + .expression = + R"cel(cel.bind(m, {'greeting': 'hello', 'farewell': 'goodbye'}.transformList(_, v, v), m == ['goodbye', 'hello'] || m == ['hello', 'goodbye']))cel", + }, + // map.transformMap() + { + .expression = + R"cel({'hello': 'world', 'goodbye': 'cruel world'}.transformMap(k, v, k + ', ' + v + '!') == {'hello': 'hello, world!', 'goodbye': 'goodbye, cruel world!'})cel", + }, + { + .expression = + R"cel({'hello': 'world', 'goodbye': 'cruel world'}.transformMap(k, v, v.startsWith('world'), k + ", " + v + "!") == {'hello': 'hello, world!'})cel", + }, + { + .expression = "{}.transformMap(__result__, v, v)", + .expected_error = "variable name cannot be __result__", + }, + { + .expression = "{}.transformMap(k, __result__, v)", + .expected_error = "variable name cannot be __result__", + }, + { + .expression = "{}.transformMap(e, e, e)", + .expected_error = + "second variable must be different from the first variable", + }, + { + .expression = "{}.transformMap(foo.bar, e, e)", + .expected_error = + "first variable name must be a simple identifier", + }, + { + .expression = "{}.transformMap(e, foo.bar, e)", + .expected_error = + "second variable name must be a simple identifier", + }, + { + .expression = "{}.transformMap(__result__, v, v == 0, v)", + .expected_error = "variable name cannot be __result__", + }, + { + .expression = "{}.transformMap(k, __result__, k == 0, v)", + .expected_error = "variable name cannot be __result__", + }, + { + .expression = "{}.transformMap(e, e, e == e, e)", + .expected_error = + "second variable must be different from the first variable", + }, + { + .expression = "{}.transformMap(foo.bar, e, true, e)", + .expected_error = + "first variable name must be a simple identifier", + }, + { + .expression = "{}.transformMap(e, foo.bar, true, e)", + .expected_error = + "second variable name must be a simple identifier", + }, + // map.transformMapEntry + { + .expression = + R"cel({'hello': 'world', 'greetings': 'tacocat'}.transformMapEntry(k, v, {v: k}) == {'world': 'hello', 'tacocat': 'greetings'})cel", + }, + { + .expression = + R"cel({'hello': 'world', 'greetings': 'tacocat'}.transformMapEntry(k, v, {}) == {})cel", + }, + { + .expression = + R"cel({'a': 'same', 'c': 'same'}.transformMapEntry(k, v, {v: k}))cel", + .expected_status_code = absl::StatusCode::kAlreadyExists, + .expected_error = "duplicate key in map", + }, + { + .expression = "{}.transformMapEntry(__result__, v, v)", + .expected_error = "variable name cannot be __result__", + }, + { + .expression = "{}.transformMapEntry(k, __result__, v)", + .expected_error = "variable name cannot be __result__", + }, + { + .expression = "{}.transformMapEntry(e, e, e)", + .expected_error = + "second variable must be different from the first variable", + }, + { + .expression = "{}.transformMapEntry(foo.bar, e, e)", + .expected_error = + "first variable name must be a simple identifier", + }, + { + .expression = "{}.transformMapEntry(e, foo.bar, e)", + .expected_error = + "second variable name must be a simple identifier", + }, + // transformMapEntry(k, v, filter, expr) + { + .expression = + R"cel({'hello': 'world', 'same': 'same'}.transformMapEntry(k, v, k != v, {v: k}) == {'world': 'hello'})cel", + }, + { + .expression = "{}.transformMapEntry(__result__, v, v == 0, v)", + .expected_error = "variable name cannot be __result__", + }, + { + .expression = "{}.transformMapEntry(k, __result__, k == 0, v)", + .expected_error = "variable name cannot be __result__", + }, + { + .expression = "{}.transformMapEntry(e, e, e == e, e)", + .expected_error = + "second variable must be different from the first variable", + }, + { + .expression = "{}.transformMapEntry(foo.bar, e, true, e)", + .expected_error = + "first variable name must be a simple identifier", + }, + { + .expression = "{}.transformMapEntry(e, foo.bar, true, e)", + .expected_error = + "second variable name must be a simple identifier", + }, + // list.transformMapEntry + { + .expression = + R"cel(['one', 'two'].transformMapEntry(k, v, {k + 1: 'is ' + v}) == {1: 'is one', 2: 'is two'})cel", + }, + }), + ::testing::ValuesIn({ + { + .enable_mutable_accumulator = true, + .max_recursion_depth = 0, + }, + { + .enable_mutable_accumulator = false, + .max_recursion_depth = 0, + }, + { + .enable_mutable_accumulator = true, + .max_recursion_depth = -1, + }, + { + .enable_mutable_accumulator = false, + .max_recursion_depth = -1, + }, + }))); + +class ComprehensionsV2TestMutableAccumulator + : public TestWithParam> { +}; + +TEST_P(ComprehensionsV2TestMutableAccumulator, MutableAccumulator) { + const ComprehensionsV2TestCase& test_case = std::get<0>(GetParam()); + const TestOptions& options = std::get<1>(GetParam()); + + ASSERT_OK_AND_ASSIGN( + std::unique_ptr program, + CreateProgram(test_case.expression, options.enable_mutable_accumulator, + options.max_recursion_depth)); + + google::protobuf::Arena arena; + Activation activation; + ASSERT_OK_AND_ASSIGN(auto result, program->Evaluate(&arena, activation)); + bool is_mutable_accumulator = common_internal::IsMutableListValue(result) || + common_internal::IsMutableMapValue(result); + EXPECT_EQ(is_mutable_accumulator, options.enable_mutable_accumulator); +} + +INSTANTIATE_TEST_SUITE_P( + ComprehensionsV2Test, ComprehensionsV2TestMutableAccumulator, + ::testing::Combine( + ::testing::ValuesIn({ + {.expression = + R"cel(['Hello', 'world'].transformList(i, v, i))cel"}, + { + .expression = + R"cel({'hello': 'world'}.transformMap(k, v, k + v))cel", + }, + { + .expression = + R"cel(['hello', 'world'].transformMap(k, v, v))cel", + }, + { + .expression = + R"cel({'hello': 'world'}.transformMapEntry(k, v, {v: k}))cel", + }, + { + .expression = + R"cel(['hello', 'world'].transformMapEntry(k, v, {v: k}))cel", + }, + }), + ::testing::ValuesIn({ + { + .enable_mutable_accumulator = true, + .max_recursion_depth = 0, + }, + { + .enable_mutable_accumulator = false, + .max_recursion_depth = 0, + }, + { + .enable_mutable_accumulator = true, + .max_recursion_depth = -1, + }, + { + .enable_mutable_accumulator = false, + .max_recursion_depth = -1, + }, + }))); + +} // namespace +} // namespace cel::extensions