diff --git a/conformance/BUILD b/conformance/BUILD index d53fd819c..64c20ea64 100644 --- a/conformance/BUILD +++ b/conformance/BUILD @@ -53,6 +53,8 @@ cc_library( "//extensions:math_ext_macros", "//extensions:proto_ext", "//extensions:select_optimization", + "//extensions:set_membership_optimization", + "//extensions:sets_functions", "//extensions:strings", "//extensions/protobuf:enum_adapter", "//internal:status_macros", @@ -162,6 +164,7 @@ _ALL_TESTS = [ "@com_google_cel_spec//tests/simple:testdata/unknowns.textproto", "@com_google_cel_spec//tests/simple:testdata/wrappers.textproto", "@com_google_cel_spec//tests/simple:testdata/block_ext.textproto", + "@com_google_cel_spec//tests/simple:testdata/sets_ext.textproto", "@com_google_cel_spec//tests/simple:testdata/type_deduction.textproto", ] @@ -286,8 +289,8 @@ gen_conformance_tests( name = "conformance_legacy_select_opt", checked = True, data = _ALL_TESTS, + extended_optimizations = True, modern = False, - select_opt = True, skip_tests = _TESTS_TO_SKIP_LEGACY, ) @@ -295,8 +298,8 @@ gen_conformance_tests( name = "conformance_select_opt", checked = True, data = _ALL_TESTS, + extended_optimizations = True, modern = True, - select_opt = True, skip_tests = _TESTS_TO_SKIP_MODERN, ) diff --git a/conformance/run.bzl b/conformance/run.bzl index 9c4b315e5..c56db3ae3 100644 --- a/conformance/run.bzl +++ b/conformance/run.bzl @@ -56,14 +56,14 @@ def _conformance_test_name(name, pratt, optimize): ], ) -def _conformance_test_args(modern, optimize, select_opt, skip_check, dashboard, enable_variadic_logical_operators, pratt): +def _conformance_test_args(modern, optimize, extended_optimizations, skip_check, dashboard, enable_variadic_logical_operators, pratt): args = [] if modern: args.append("--modern") if optimize: args.append("--opt") - if select_opt: - args.append("--select_optimization") + if extended_optimizations: + args.append("--extended_optimizations") if skip_check: args.append("--skip_check") else: @@ -78,10 +78,10 @@ def _conformance_test_args(modern, optimize, select_opt, skip_check, dashboard, args.append("--noenable_pratt_parser") return args -def _conformance_test(name, data, modern, optimize, select_opt, skip_check, skip_tests, tags, dashboard, enable_variadic_logical_operators, pratt): +def _conformance_test(name, data, modern, optimize, extended_optimizations, skip_check, skip_tests, tags, dashboard, enable_variadic_logical_operators, pratt): cc_test( name = _conformance_test_name(name, pratt, optimize), - args = _conformance_test_args(modern, optimize, select_opt, skip_check, dashboard, enable_variadic_logical_operators, pratt) + ["$(rlocationpath {})".format(test) for test in data], + args = _conformance_test_args(modern, optimize, extended_optimizations, skip_check, dashboard, enable_variadic_logical_operators, pratt) + ["$(rlocationpath {})".format(test) for test in data], env = select( { "@platforms//os:windows": {"CEL_SKIP_TESTS": ",".join(skip_tests + _TESTS_TO_SKIP_WINDOWS)}, @@ -93,7 +93,7 @@ def _conformance_test(name, data, modern, optimize, select_opt, skip_check, skip tags = tags, ) -def gen_conformance_tests(name, data, modern = False, checked = False, select_opt = False, dashboard = False, skip_tests = [], tags = [], enable_variadic_logical_operators = False): +def gen_conformance_tests(name, data, modern = False, checked = False, extended_optimizations = False, dashboard = False, skip_tests = [], tags = [], enable_variadic_logical_operators = False): """Generates conformance tests. Args: @@ -101,7 +101,7 @@ def gen_conformance_tests(name, data, modern = False, checked = False, select_op data: textproto targets describing conformance tests modern: run using modern APIs checked: whether to apply type checking - select_opt: enable select optimization + extended_optimizations: enable extended optimizations (select and set membership) dashboard: enable dashboard mode skip_tests: tests to skip in the format of the cel-spec test runner. See documentation in github.com/google/cel-spec/tests/simple/simple_test.go @@ -119,7 +119,7 @@ def gen_conformance_tests(name, data, modern = False, checked = False, select_op data, modern = modern, optimize = optimize, - select_opt = select_opt, + extended_optimizations = extended_optimizations, skip_check = skip_check, skip_tests = _expand_tests_to_skip(skip_tests), tags = tags, diff --git a/conformance/run.cc b/conformance/run.cc index 72eeeb4dd..67cd7c694 100644 --- a/conformance/run.cc +++ b/conformance/run.cc @@ -62,7 +62,8 @@ ABSL_FLAG( ABSL_FLAG(std::vector, skip_tests, {}, "Tests to skip"); ABSL_FLAG(bool, dashboard, false, "Dashboard mode, ignore test failures"); ABSL_FLAG(bool, skip_check, true, "Skip type checking the expressions"); -ABSL_FLAG(bool, select_optimization, false, "Enable select optimization."); +ABSL_FLAG(bool, extended_optimizations, false, + "Enable extended optimizations."); ABSL_FLAG(bool, enable_variadic_logical_operators, false, "Enable parsing logical AND & OR operators as a single flat variadic " "call."); @@ -261,7 +262,7 @@ NewConformanceServiceFromFlags() { cel_conformance::ConformanceServiceOptions{ .optimize = absl::GetFlag(FLAGS_opt), .modern = absl::GetFlag(FLAGS_modern), - .select_optimization = absl::GetFlag(FLAGS_select_optimization), + .extended_optimizations = absl::GetFlag(FLAGS_extended_optimizations), .enable_variadic_logical_operators = absl::GetFlag(FLAGS_enable_variadic_logical_operators), .enable_pratt_parser = absl::GetFlag(FLAGS_enable_pratt_parser), diff --git a/conformance/service.cc b/conformance/service.cc index c826d4572..b82e2d02f 100644 --- a/conformance/service.cc +++ b/conformance/service.cc @@ -67,6 +67,8 @@ #include "extensions/proto_ext.h" #include "extensions/protobuf/enum_adapter.h" #include "extensions/select_optimization.h" +#include "extensions/set_membership_optimization.h" +#include "extensions/sets_functions.h" #include "extensions/strings.h" #include "internal/status_macros.h" #include "parser/macro_registry.h" @@ -196,6 +198,8 @@ absl::Status CheckImpl(google::protobuf::Arena* arena, builder->AddLibrary(cel::extensions::ComprehensionsV2CheckerLibrary())); CEL_RETURN_IF_ERROR( builder->AddLibrary(cel::extensions::ListsCheckerLibrary())); + CEL_RETURN_IF_ERROR( + builder->AddLibrary(cel::extensions::SetsCheckerLibrary())); } for (const auto& decl : request.type_env()) { @@ -245,7 +249,7 @@ absl::Status CheckImpl(google::protobuf::Arena* arena, class LegacyConformanceServiceImpl : public ConformanceServiceInterface { public: static absl::StatusOr> Create( - bool optimize, bool recursive, bool select_optimization, + bool optimize, bool recursive, bool extended_optimizations, bool enable_variadic_logical_operators, bool enable_pratt_parser) { static auto* constant_arena = new Arena(); @@ -293,8 +297,8 @@ class LegacyConformanceServiceImpl : public ConformanceServiceInterface { options.enable_typed_field_access = true; } - if (select_optimization) { - std::cerr << "Enabling select optimizations" << std::endl; + if (extended_optimizations) { + std::cerr << "Enabling extended optimizations" << std::endl; options.enable_select_optimization = true; } @@ -323,6 +327,8 @@ class LegacyConformanceServiceImpl : public ConformanceServiceInterface { builder->GetRegistry(), options)); CEL_RETURN_IF_ERROR(cel::extensions::RegisterMathExtensionFunctions( builder->GetRegistry(), options)); + CEL_RETURN_IF_ERROR(cel::extensions::RegisterSetsFunctions( + builder->GetRegistry(), options)); return absl::WrapUnique(new LegacyConformanceServiceImpl( std::move(builder), enable_variadic_logical_operators, @@ -446,7 +452,7 @@ class LegacyConformanceServiceImpl : public ConformanceServiceInterface { class ModernConformanceServiceImpl : public ConformanceServiceInterface { public: static absl::StatusOr> Create( - bool optimize, bool recursive, bool select_optimization, + bool optimize, bool recursive, bool extended_optimizations, bool enable_variadic_logical_operators, bool enable_pratt_parser) { google::protobuf::LinkMessageReflection< cel::expr::conformance::proto3::TestAllTypes>(); @@ -490,7 +496,7 @@ class ModernConformanceServiceImpl : public ConformanceServiceInterface { } return absl::WrapUnique(new ModernConformanceServiceImpl( - options, optimize, select_optimization, + options, optimize, extended_optimizations, enable_variadic_logical_operators, enable_pratt_parser)); } @@ -512,8 +518,10 @@ class ModernConformanceServiceImpl : public ConformanceServiceInterface { } CEL_RETURN_IF_ERROR(cel::EnableReferenceResolver( builder, cel::ReferenceResolverEnabled::kAlways)); - if (enable_select_optimization_) { + if (enable_extended_optimizations_) { CEL_RETURN_IF_ERROR(cel::extensions::EnableSelectOptimization(builder)); + CEL_RETURN_IF_ERROR( + cel::extensions::EnableSetMembershipOptimization(builder)); } auto& type_registry = builder.type_registry(); @@ -542,6 +550,8 @@ class ModernConformanceServiceImpl : public ConformanceServiceInterface { builder.function_registry(), options)); CEL_RETURN_IF_ERROR(cel::extensions::RegisterListsFunctions( builder.function_registry(), options)); + CEL_RETURN_IF_ERROR(cel::extensions::RegisterSetsFunctions( + builder.function_registry(), options)); return std::move(builder).Build(); } @@ -641,12 +651,12 @@ class ModernConformanceServiceImpl : public ConformanceServiceInterface { private: ModernConformanceServiceImpl(const RuntimeOptions& options, bool enable_optimizations, - bool enable_select_optimization, + bool enable_extended_optimizations, bool enable_variadic_logical_operators, bool enable_pratt_parser) : options_(options), enable_optimizations_(enable_optimizations), - enable_select_optimization_(enable_select_optimization), + enable_extended_optimizations_(enable_extended_optimizations), enable_variadic_logical_operators_(enable_variadic_logical_operators), enable_pratt_parser_(enable_pratt_parser) {} @@ -678,7 +688,7 @@ class ModernConformanceServiceImpl : public ConformanceServiceInterface { RuntimeOptions options_; bool enable_optimizations_; - bool enable_select_optimization_; + bool enable_extended_optimizations_; bool enable_variadic_logical_operators_; bool enable_pratt_parser_; }; @@ -693,11 +703,11 @@ absl::StatusOr> NewConformanceService(const ConformanceServiceOptions& options) { if (options.modern) { return google::api::expr::runtime::ModernConformanceServiceImpl::Create( - options.optimize, options.recursive, options.select_optimization, + options.optimize, options.recursive, options.extended_optimizations, options.enable_variadic_logical_operators, options.enable_pratt_parser); } else { return google::api::expr::runtime::LegacyConformanceServiceImpl::Create( - options.optimize, options.recursive, options.select_optimization, + options.optimize, options.recursive, options.extended_optimizations, options.enable_variadic_logical_operators, options.enable_pratt_parser); } } diff --git a/conformance/service.h b/conformance/service.h index 4cf2aec73..1a3c28c3d 100644 --- a/conformance/service.h +++ b/conformance/service.h @@ -45,7 +45,7 @@ struct ConformanceServiceOptions { bool modern; bool arena; bool recursive; - bool select_optimization; + bool extended_optimizations; bool enable_variadic_logical_operators = false; bool enable_pratt_parser = true; }; diff --git a/eval/compiler/flat_expr_builder_extensions.cc b/eval/compiler/flat_expr_builder_extensions.cc index 3f308125a..f38b9c9e2 100644 --- a/eval/compiler/flat_expr_builder_extensions.cc +++ b/eval/compiler/flat_expr_builder_extensions.cc @@ -303,8 +303,15 @@ Subexpression* absl_nullable ProgramBuilder::MakeSubexpression( return it->second.get(); } -bool PlannerContext::IsSubplanInspectable(const cel::Expr& node) const { - return program_builder_.GetSubexpression(&node) != nullptr; +bool PlannerContext::NodeHasInspectablePlan(const cel::Expr& node) const { + const Subexpression* subexpression = program_builder_.GetSubexpression(&node); + if (subexpression == nullptr) { + return false; + } + if (subexpression->IsFlattened()) { + return !subexpression->flattened_elements().empty(); + } + return !subexpression->elements().empty(); } ExecutionPathView PlannerContext::GetSubplan(const cel::Expr& node) { diff --git a/eval/compiler/flat_expr_builder_extensions.h b/eval/compiler/flat_expr_builder_extensions.h index d6d3abb17..2a888a8e2 100644 --- a/eval/compiler/flat_expr_builder_extensions.h +++ b/eval/compiler/flat_expr_builder_extensions.h @@ -308,7 +308,7 @@ class PlannerContext { // Returns true if the subplan is inspectable. // // If false, the node is not mapped to a subexpression in the program builder. - bool IsSubplanInspectable(const cel::Expr& node) const; + bool NodeHasInspectablePlan(const cel::Expr& node) const; // Return a view to the current subplan representing node. // diff --git a/eval/compiler/flat_expr_builder_extensions_test.cc b/eval/compiler/flat_expr_builder_extensions_test.cc index 7d9426205..1add6b2b4 100644 --- a/eval/compiler/flat_expr_builder_extensions_test.cc +++ b/eval/compiler/flat_expr_builder_extensions_test.cc @@ -144,7 +144,7 @@ TEST_F(PlannerContextTest, GetPlan) { UniquePtrHolds(step_ptrs.a))); Expr d; - EXPECT_FALSE(context.IsSubplanInspectable(d)); + EXPECT_FALSE(context.NodeHasInspectablePlan(d)); EXPECT_THAT(context.GetSubplan(d), IsEmpty()); } @@ -193,8 +193,8 @@ TEST_F(PlannerContextTest, ExtractPlan) { type_registry_.GetComposedTypeProvider(), issue_collector_, program_builder, arena); - EXPECT_TRUE(context.IsSubplanInspectable(a)); - EXPECT_TRUE(context.IsSubplanInspectable(b)); + EXPECT_TRUE(context.NodeHasInspectablePlan(a)); + EXPECT_TRUE(context.NodeHasInspectablePlan(b)); ASSERT_OK_AND_ASSIGN(ExecutionPath extracted, context.ExtractSubplan(b)); @@ -233,7 +233,7 @@ TEST_F(PlannerContextTest, ReplacePlanUpdatesParent) { type_registry_.GetComposedTypeProvider(), issue_collector_, program_builder, arena); - EXPECT_TRUE(context.IsSubplanInspectable(a)); + EXPECT_TRUE(context.NodeHasInspectablePlan(a)); ASSERT_THAT(context.ReplaceSubplan(c, {}), IsOk()); diff --git a/eval/eval/BUILD b/eval/eval/BUILD index 602c1580b..ac2d0ded3 100644 --- a/eval/eval/BUILD +++ b/eval/eval/BUILD @@ -35,6 +35,7 @@ package_group( cc_library( name = "evaluator_core", srcs = [ + "bitset_steps.cc", "comprehension_step.cc", "create_list_step.cc", "equality_steps.cc", @@ -45,6 +46,7 @@ cc_library( "logic_step.cc", ], hdrs = [ + "bitset_steps.h", "comprehension_step.h", "create_list_step.h", "equality_steps.h", @@ -356,6 +358,40 @@ cc_library( ], ) +cc_library( + name = "bitset_steps", + deps = [ + ":evaluator_core", + ], +) + +cc_test( + name = "bitset_steps_test", + srcs = [ + "bitset_steps_test.cc", + ], + deps = [ + ":attribute_trail", + ":evaluator_core", + ":expression_step_logic", + "//base:attributes", + "//common:value", + "//common:value_kind", + "//common:value_testing", + "//internal:testing", + "//internal:testing_descriptor_pool", + "//internal:testing_message_factory", + "//runtime:activation", + "//runtime:runtime_options", + "//runtime/internal:runtime_type_provider", + "@com_google_absl//absl/base:nullability", + "@com_google_absl//absl/log:absl_check", + "@com_google_absl//absl/status", + "@com_google_absl//absl/status:status_matchers", + "@com_google_protobuf//:protobuf", + ], +) + cc_test( name = "equality_steps_test", srcs = [ diff --git a/eval/eval/bitset_steps.cc b/eval/eval/bitset_steps.cc new file mode 100644 index 000000000..0d2868ce7 --- /dev/null +++ b/eval/eval/bitset_steps.cc @@ -0,0 +1,233 @@ +// 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. + +#include "eval/eval/bitset_steps.h" + +#include +#include +#include + +#include "absl/base/optimization.h" +#include "absl/status/status.h" +#include "absl/status/statusor.h" +#include "base/builtins.h" +#include "common/value.h" +#include "common/value_kind.h" +#include "eval/eval/attribute_trail.h" +#include "eval/eval/attribute_utility.h" +#include "eval/eval/evaluator_core.h" +#include "internal/number.h" +#include "internal/status_macros.h" +#include "runtime/internal/errors.h" + +namespace google::api::expr::runtime { + +namespace { + +using ::cel::BoolValue; +using ::cel::ListValue; +using ::cel::Value; +using ::cel::ValueKind; +using ::cel::internal::Number; + +enum class BitSetOp { + kAndIsNonZero, + kAndEqual, + kEqual, +}; + +} // namespace + +uint64_t ExtractBit(const Value& value) { + switch (value.kind()) { + case ValueKind::kInt: { + int64_t v = value.GetInt().NativeValue(); + if (v >= 0 && v < 64) { + return uint64_t{1} << v; + } + return 0; + } + case ValueKind::kUint: { + uint64_t v = value.GetUint().NativeValue(); + if (v < 64) { + return uint64_t{1} << v; + } + return 0; + } + case ValueKind::kDouble: { + Number number = Number::FromDouble(value.GetDouble().NativeValue()); + if (number.LosslessConvertibleToUint()) { + uint64_t v = number.AsUint(); + if (v < 64) { + return uint64_t{1} << v; + } + } + return 0; + } + default: + return 0; + } +} + +namespace { + +absl::StatusOr EvaluateBitAndIsNonZero(ExecutionFrameBase& frame, + const ListValue& list, + uint64_t bitmask) { + if (bitmask == 0) { + return BoolValue(false); + } + bool intersects = false; + CEL_RETURN_IF_ERROR(list.ForEach( + [&](size_t /*index*/, const Value& element) -> absl::StatusOr { + uint64_t bit = ExtractBit(element); + if ((bit & bitmask) != 0) { + intersects = true; + return false; + } + return true; + }, + frame.descriptor_pool(), frame.message_factory(), frame.arena())); + return BoolValue(intersects); +} + +absl::StatusOr EvaluateBitAndEqual(ExecutionFrameBase& frame, + const ListValue& list, + uint64_t bitmask) { + if (bitmask == 0) { + return BoolValue(true); + } + uint64_t remaining = bitmask; + CEL_RETURN_IF_ERROR(list.ForEach( + [&](size_t /*index*/, const Value& element) -> absl::StatusOr { + remaining &= ~ExtractBit(element); + return remaining != 0; + }, + frame.descriptor_pool(), frame.message_factory(), frame.arena())); + return BoolValue(remaining == 0); +} + +absl::StatusOr EvaluateBitEqual(ExecutionFrameBase& frame, + const ListValue& list, + uint64_t bitmask) { + uint64_t list_mask = 0; + bool has_unexpected = false; + CEL_RETURN_IF_ERROR(list.ForEach( + [&](size_t /*index*/, const Value& element) -> absl::StatusOr { + uint64_t bit = ExtractBit(element); + if ((bit & bitmask) == 0) { + has_unexpected = true; + return false; + } + list_mask |= bit; + return true; + }, + frame.descriptor_pool(), frame.message_factory(), frame.arena())); + return BoolValue(!has_unexpected && list_mask == bitmask); +} + +absl::StatusOr EvaluateBitSetOp(ExecutionFrameBase& frame, + const Value& arg, + const AttributeTrail& attr, + uint64_t bitmask, BitSetOp op) { + if (arg.IsError()) { + return arg; + } + + if (frame.unknown_processing_enabled()) { + AttributeUtility::Accumulator accu = + frame.attribute_utility().CreateAccumulator(); + accu.MaybeAdd(arg, attr); + if (!accu.IsEmpty()) { + return std::move(accu).Build(frame.arena()); + } + } + + if (!arg.IsList()) { + return cel::ErrorValue::From( + cel::runtime_internal::CreateNoMatchingOverloadError(cel::builtin::kIn), + frame.arena()); + } + + const ListValue& list = arg.GetList(); + + switch (op) { + case BitSetOp::kAndIsNonZero: + return EvaluateBitAndIsNonZero(frame, list, bitmask); + case BitSetOp::kAndEqual: + return EvaluateBitAndEqual(frame, list, bitmask); + case BitSetOp::kEqual: + return EvaluateBitEqual(frame, list, bitmask); + } + ABSL_UNREACHABLE(); +} + +void EvaluateBitSetStep(uint64_t bitmask, BitSetOp op, ExecutionFrame& frame) { + if (!frame.value_stack().HasEnough(1)) { + frame.Abort( + absl::Status(absl::StatusCode::kInternal, "Value stack underflow")); + return; + } + const Value& arg = frame.value_stack().Peek(); + const AttributeTrail& attr = frame.value_stack().PeekAttribute(); + + absl::StatusOr result = + EvaluateBitSetOp(frame, arg, attr, bitmask, op); + if (!result.ok()) { + frame.Abort(std::move(result).status()); + return; + } + frame.value_stack().PopAndPush(*std::move(result)); +} + +} // namespace + +void EvaluateBitIsSetStep(uint64_t bitmask, ExecutionFrame& frame) { + if (!frame.value_stack().HasEnough(1)) { + frame.Abort( + absl::Status(absl::StatusCode::kInternal, "Value stack underflow")); + return; + } + const Value& arg = frame.value_stack().Peek(); + if (arg.IsError()) { + return; + } + + if (frame.unknown_processing_enabled()) { + AttributeUtility::Accumulator accu = + frame.attribute_utility().CreateAccumulator(); + accu.MaybeAdd(arg, frame.value_stack().PeekAttribute()); + if (!accu.IsEmpty()) { + frame.value_stack().PopAndPush(std::move(accu).Build(frame.arena())); + return; + } + } + + uint64_t bit = ExtractBit(arg); + frame.value_stack().PopAndPush(BoolValue((bit & bitmask) != 0)); +} + +void EvaluateBitAndIsNonZeroStep(uint64_t bitmask, ExecutionFrame& frame) { + EvaluateBitSetStep(bitmask, BitSetOp::kAndIsNonZero, frame); +} + +void EvaluateBitAndEqualStep(uint64_t bitmask, ExecutionFrame& frame) { + EvaluateBitSetStep(bitmask, BitSetOp::kAndEqual, frame); +} + +void EvaluateBitEqualStep(uint64_t bitmask, ExecutionFrame& frame) { + EvaluateBitSetStep(bitmask, BitSetOp::kEqual, frame); +} + +} // namespace google::api::expr::runtime diff --git a/eval/eval/bitset_steps.h b/eval/eval/bitset_steps.h new file mode 100644 index 000000000..553ca42b5 --- /dev/null +++ b/eval/eval/bitset_steps.h @@ -0,0 +1,43 @@ +// 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. + +#ifndef THIRD_PARTY_CEL_CPP_EVAL_EVAL_BITSET_STEPS_H_ +#define THIRD_PARTY_CEL_CPP_EVAL_EVAL_BITSET_STEPS_H_ + +#include + +#include "common/value.h" + +namespace google::api::expr::runtime { + +class ExecutionFrame; + +// Extracts a single-bit bitmask (`1ULL << v`) for a `cel::Value` equal to an +// integer `v` in `[0, 63]` under heterogeneous equality. +// +// Return values: +// - `1ULL << v` (non-zero) if `value` is `int`, `uint`, or `double` with an +// exact integer value `v` in `[0, 63]`. +// - `0` otherwise (if `value` is outside `[0, 63]`, is not an exact integer, +// or is not a numeric value). +uint64_t ExtractBit(const cel::Value& value); + +void EvaluateBitIsSetStep(uint64_t bitmask, ExecutionFrame& frame); +void EvaluateBitAndIsNonZeroStep(uint64_t bitmask, ExecutionFrame& frame); +void EvaluateBitAndEqualStep(uint64_t bitmask, ExecutionFrame& frame); +void EvaluateBitEqualStep(uint64_t bitmask, ExecutionFrame& frame); + +} // namespace google::api::expr::runtime + +#endif // THIRD_PARTY_CEL_CPP_EVAL_EVAL_BITSET_STEPS_H_ diff --git a/eval/eval/bitset_steps_test.cc b/eval/eval/bitset_steps_test.cc new file mode 100644 index 000000000..8ba13bfe3 --- /dev/null +++ b/eval/eval/bitset_steps_test.cc @@ -0,0 +1,453 @@ +// 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. + +#include "eval/eval/bitset_steps.h" + +#include +#include +#include +#include +#include +#include + +#include "absl/base/nullability.h" +#include "absl/log/absl_check.h" +#include "absl/status/status.h" +#include "absl/status/status_matchers.h" +#include "base/attribute.h" +#include "common/value.h" +#include "common/value_kind.h" +#include "common/value_testing.h" +#include "eval/eval/attribute_trail.h" +#include "eval/eval/evaluator_core.h" +#include "eval/eval/expression_step_logic.h" +#include "internal/testing.h" +#include "internal/testing_descriptor_pool.h" +#include "internal/testing_message_factory.h" +#include "runtime/activation.h" +#include "runtime/internal/runtime_type_provider.h" +#include "runtime/runtime_options.h" +#include "google/protobuf/arena.h" + +namespace google::api::expr::runtime { +namespace { + +using ::absl_testing::IsOk; +using ::cel::Attribute; +using ::cel::DoubleValue; +using ::cel::ErrorValue; +using ::cel::IntValue; +using ::cel::ListValueBuilderPtr; +using ::cel::StringValue; +using ::cel::UintValue; +using ::cel::UnknownValue; +using ::cel::Value; +using ::cel::ValueKind; +using ::cel::test::BoolValueIs; +using ::cel::test::ErrorValueIs; +using ::cel::test::ValueKindIs; +using ::testing::HasSubstr; + +class ValueStep : public ExpressionStepLogic { + public: + ValueStep(Value value, Attribute attr) + : ExpressionStepLogic(), + value_(std::move(value)), + attr_(std::move(attr)) {} + explicit ValueStep(Value value) + : ExpressionStepLogic(), value_(std::move(value)), attr_() {} + + void Evaluate(ExecutionFrame* frame) const override { + frame->value_stack().Push(value_, attr_); + } + + private: + Value value_; + AttributeTrail attr_; +}; + +Value MakeList(google::protobuf::Arena* absl_nonnull arena, + const std::vector& elements) { + ListValueBuilderPtr builder = cel::NewListValueBuilder(arena); + for (const Value& elem : elements) { + ABSL_CHECK_OK(builder->Add(elem)); + } + return std::move(*builder).Build(); +} + +enum class OutputType { kBoolTrue, kBoolFalse, kError, kUnknown }; + +struct BitIsSetTestCase { + Value input; + uint64_t bitmask; + OutputType expected_result; +}; + +class BitIsSetTest : public ::testing::TestWithParam {}; + +TEST_P(BitIsSetTest, Evaluate) { + const BitIsSetTestCase& test_case = GetParam(); + cel::Activation activation; + google::protobuf::Arena arena; + cel::RuntimeOptions opts; + opts.unknown_processing = cel::UnknownProcessingOptions::kAttributeOnly; + cel::runtime_internal::RuntimeTypeProvider type_provider( + cel::internal::GetTestingDescriptorPool()); + + FlatExpressionEvaluatorState state( + /*value_stack_size=*/5, + /*comprehension_slot_count=*/0, /*iterator_stack_size=*/0, type_provider, + cel::internal::GetTestingDescriptorPool(), + cel::internal::GetTestingMessageFactory(), &arena); + + ExecutionPath steps; + steps.push_back(ExpressionStep::MakeGenericStep( + std::make_unique(test_case.input))); + steps.push_back(ExpressionStep::MakeBitIsSetStep(test_case.bitmask)); + + ExecutionFrame frame(steps, activation, opts, state); + + ASSERT_OK_AND_ASSIGN(Value result, frame.Evaluate()); + + switch (test_case.expected_result) { + case OutputType::kBoolTrue: + EXPECT_THAT(result, BoolValueIs(true)); + break; + case OutputType::kBoolFalse: + EXPECT_THAT(result, BoolValueIs(false)); + break; + case OutputType::kError: + EXPECT_THAT(result, ValueKindIs(ValueKind::kError)); + break; + case OutputType::kUnknown: + EXPECT_THAT(result, ValueKindIs(ValueKind::kUnknown)); + break; + } +} + +INSTANTIATE_TEST_SUITE_P( + BitIsSetTest, BitIsSetTest, + testing::Values( + // IntValue boundaries and basics + BitIsSetTestCase{IntValue(0), 0b101, OutputType::kBoolTrue}, + BitIsSetTestCase{IntValue(1), 0b101, OutputType::kBoolFalse}, + BitIsSetTestCase{IntValue(2), 0b101, OutputType::kBoolTrue}, + BitIsSetTestCase{IntValue(63), uint64_t{1} << 63, + OutputType::kBoolTrue}, + BitIsSetTestCase{IntValue(62), uint64_t{1} << 63, + OutputType::kBoolFalse}, + BitIsSetTestCase{IntValue(-1), ~uint64_t{0}, OutputType::kBoolFalse}, + BitIsSetTestCase{IntValue(std::numeric_limits::min()), + ~uint64_t{0}, OutputType::kBoolFalse}, + BitIsSetTestCase{IntValue(64), ~uint64_t{0}, OutputType::kBoolFalse}, + BitIsSetTestCase{IntValue(std::numeric_limits::max()), + ~uint64_t{0}, OutputType::kBoolFalse}, + BitIsSetTestCase{IntValue(0), 0, OutputType::kBoolFalse}, + // UintValue boundaries and basics + BitIsSetTestCase{UintValue(0), 0b101, OutputType::kBoolTrue}, + BitIsSetTestCase{UintValue(1), 0b101, OutputType::kBoolFalse}, + BitIsSetTestCase{UintValue(63), uint64_t{1} << 63, + OutputType::kBoolTrue}, + BitIsSetTestCase{UintValue(64), ~uint64_t{0}, OutputType::kBoolFalse}, + BitIsSetTestCase{UintValue(std::numeric_limits::max()), + ~uint64_t{0}, OutputType::kBoolFalse}, + // DoubleValue (heterogeneous equality is always enabled for bitset + // steps) + BitIsSetTestCase{DoubleValue(0.0), 0b101, OutputType::kBoolTrue}, + BitIsSetTestCase{DoubleValue(2.0), 0b101, OutputType::kBoolTrue}, + BitIsSetTestCase{DoubleValue(1.0), 0b101, OutputType::kBoolFalse}, + BitIsSetTestCase{DoubleValue(63.0), uint64_t{1} << 63, + OutputType::kBoolTrue}, + BitIsSetTestCase{DoubleValue(64.0), ~uint64_t{0}, + OutputType::kBoolFalse}, + BitIsSetTestCase{DoubleValue(-1.0), ~uint64_t{0}, + OutputType::kBoolFalse}, + BitIsSetTestCase{DoubleValue(2.5), ~uint64_t{0}, + OutputType::kBoolFalse}, + BitIsSetTestCase{DoubleValue(NAN), ~uint64_t{0}, + OutputType::kBoolFalse}, + BitIsSetTestCase{DoubleValue(INFINITY), ~uint64_t{0}, + OutputType::kBoolFalse}, + // Non-numeric and Unknown + BitIsSetTestCase{StringValue(), 0b101, OutputType::kBoolFalse}, + BitIsSetTestCase{UnknownValue(), 0b101, OutputType::kUnknown})); + +TEST(BitIsSetStandaloneTest, PropagatesErrorAndPartialUnknown) { + cel::Activation activation; + google::protobuf::Arena arena; + cel::RuntimeOptions opts; + opts.unknown_processing = cel::UnknownProcessingOptions::kAttributeOnly; + cel::runtime_internal::RuntimeTypeProvider type_provider( + cel::internal::GetTestingDescriptorPool()); + + FlatExpressionEvaluatorState state( + /*value_stack_size=*/5, + /*comprehension_slot_count=*/0, /*iterator_stack_size=*/0, type_provider, + cel::internal::GetTestingDescriptorPool(), + cel::internal::GetTestingMessageFactory(), &arena); + + ASSERT_THAT(activation.SetUnknownPatterns({cel::AttributePattern( + "foo", {cel::AttributeQualifierPattern::OfString("bar")})}), + IsOk()); + + { + ExecutionPath steps; + steps.push_back(ExpressionStep::MakeGenericStep( + std::make_unique(IntValue(0), cel::Attribute("foo")))); + steps.push_back(ExpressionStep::MakeBitIsSetStep(0b1)); + ExecutionFrame frame(steps, activation, opts, state); + ASSERT_OK_AND_ASSIGN(Value result, frame.Evaluate()); + EXPECT_THAT(result, ValueKindIs(ValueKind::kUnknown)); + } + + { + ExecutionPath steps; + steps.push_back(ExpressionStep::MakeGenericStep(std::make_unique( + ErrorValue::From(absl::CancelledError("custom_err"), &arena), + cel::Attribute("foo")))); + steps.push_back(ExpressionStep::MakeBitIsSetStep(0b1)); + ExecutionFrame frame(steps, activation, opts, state); + ASSERT_OK_AND_ASSIGN(Value result, frame.Evaluate()); + EXPECT_THAT(result, ErrorValueIs(absl_testing::StatusIs( + absl::StatusCode::kCancelled, "custom_err"))); + } +} + +enum class BitSetStepKind { kAndIsNonZero, kAndEqual, kEqual }; + +ExpressionStep MakeBitSetStepForKind(BitSetStepKind kind, uint64_t bitmask) { + switch (kind) { + case BitSetStepKind::kAndIsNonZero: + return ExpressionStep::MakeBitAndIsNonZeroStep(bitmask); + case BitSetStepKind::kAndEqual: + return ExpressionStep::MakeBitAndEqualStep(bitmask); + case BitSetStepKind::kEqual: + return ExpressionStep::MakeBitEqualStep(bitmask); + } +} + +struct BitSetTestCase { + BitSetStepKind kind; + std::vector list_elements; + uint64_t bitmask; + OutputType expected_result; +}; + +class BitSetTest : public ::testing::TestWithParam {}; + +TEST_P(BitSetTest, Evaluate) { + const BitSetTestCase& test_case = GetParam(); + cel::Activation activation; + google::protobuf::Arena arena; + cel::RuntimeOptions opts; + opts.unknown_processing = cel::UnknownProcessingOptions::kAttributeOnly; + cel::runtime_internal::RuntimeTypeProvider type_provider( + cel::internal::GetTestingDescriptorPool()); + + FlatExpressionEvaluatorState state( + /*value_stack_size=*/5, + /*comprehension_slot_count=*/0, /*iterator_stack_size=*/0, type_provider, + cel::internal::GetTestingDescriptorPool(), + cel::internal::GetTestingMessageFactory(), &arena); + + ExecutionPath steps; + steps.push_back(ExpressionStep::MakeGenericStep( + std::make_unique(MakeList(&arena, test_case.list_elements)))); + steps.push_back(MakeBitSetStepForKind(test_case.kind, test_case.bitmask)); + + ExecutionFrame frame(steps, activation, opts, state); + + ASSERT_OK_AND_ASSIGN(Value result, frame.Evaluate()); + + switch (test_case.expected_result) { + case OutputType::kBoolTrue: + EXPECT_THAT(result, BoolValueIs(true)); + break; + case OutputType::kBoolFalse: + EXPECT_THAT(result, BoolValueIs(false)); + break; + case OutputType::kError: + EXPECT_THAT(result, ValueKindIs(ValueKind::kError)); + break; + case OutputType::kUnknown: + EXPECT_THAT(result, ValueKindIs(ValueKind::kUnknown)); + break; + } +} + +INSTANTIATE_TEST_SUITE_P( + BitSetTest, BitSetTest, + testing::Values( + // kBitAndIsNonZero (Any / overlap) + BitSetTestCase{ + BitSetStepKind::kAndIsNonZero, {}, 0b101, OutputType::kBoolFalse}, + BitSetTestCase{BitSetStepKind::kAndIsNonZero, + {IntValue(1), IntValue(3)}, + 0b101, + OutputType::kBoolFalse}, + BitSetTestCase{BitSetStepKind::kAndIsNonZero, + {IntValue(1), UintValue(2)}, + 0b101, + OutputType::kBoolTrue}, + BitSetTestCase{BitSetStepKind::kAndIsNonZero, + {UintValue(63)}, + uint64_t{1} << 63, + OutputType::kBoolTrue}, + BitSetTestCase{BitSetStepKind::kAndIsNonZero, + {IntValue(-5), IntValue(100), DoubleValue(0.0)}, + 0b101, + OutputType::kBoolTrue}, + // kBitAndEqual (All / contains all bits of mask) + BitSetTestCase{BitSetStepKind::kAndEqual, {}, 0, OutputType::kBoolTrue}, + BitSetTestCase{ + BitSetStepKind::kAndEqual, {}, 0b101, OutputType::kBoolFalse}, + BitSetTestCase{BitSetStepKind::kAndEqual, + {IntValue(0)}, + 0b101, + OutputType::kBoolFalse}, + BitSetTestCase{BitSetStepKind::kAndEqual, + {IntValue(0), UintValue(2)}, + 0b101, + OutputType::kBoolTrue}, + BitSetTestCase{BitSetStepKind::kAndEqual, + {IntValue(0), DoubleValue(2.0), IntValue(5), + IntValue(100), IntValue(-1)}, + 0b101, + OutputType::kBoolTrue}, + // kBitEqual (Equivalent / exact set match) + BitSetTestCase{BitSetStepKind::kEqual, {}, 0, OutputType::kBoolTrue}, + BitSetTestCase{ + BitSetStepKind::kEqual, {}, 0b101, OutputType::kBoolFalse}, + BitSetTestCase{BitSetStepKind::kEqual, + {IntValue(0)}, + 0b101, + OutputType::kBoolFalse}, + BitSetTestCase{BitSetStepKind::kEqual, + {IntValue(0), UintValue(2)}, + 0b101, + OutputType::kBoolTrue}, + BitSetTestCase{BitSetStepKind::kEqual, + {IntValue(0), UintValue(2), DoubleValue(0.0)}, + 0b101, + OutputType::kBoolTrue}, + BitSetTestCase{BitSetStepKind::kEqual, + {IntValue(0), IntValue(1), UintValue(2)}, + 0b101, + OutputType::kBoolFalse}, + BitSetTestCase{BitSetStepKind::kEqual, + {IntValue(0), UintValue(2), IntValue(-1)}, + 0b101, + OutputType::kBoolFalse}, + BitSetTestCase{BitSetStepKind::kEqual, + {IntValue(0), UintValue(2), IntValue(64)}, + 0b101, + OutputType::kBoolFalse}, + BitSetTestCase{BitSetStepKind::kEqual, + {IntValue(0), UintValue(2), UintValue(64)}, + 0b101, + OutputType::kBoolFalse}, + BitSetTestCase{BitSetStepKind::kEqual, + {IntValue(0), UintValue(2), DoubleValue(1.5)}, + 0b101, + OutputType::kBoolFalse}, + BitSetTestCase{BitSetStepKind::kEqual, + {IntValue(0), UintValue(2), DoubleValue(NAN)}, + 0b101, + OutputType::kBoolFalse}, + // Non-numeric element inside list (heterogeneous equality treats as not + // equal to any integer in [0, 63]) + BitSetTestCase{BitSetStepKind::kAndIsNonZero, + {StringValue()}, + 0b101, + OutputType::kBoolFalse}, + BitSetTestCase{BitSetStepKind::kAndIsNonZero, + {StringValue(), IntValue(2)}, + 0b101, + OutputType::kBoolTrue}, + BitSetTestCase{BitSetStepKind::kAndEqual, + {IntValue(0), IntValue(2), StringValue()}, + 0b101, + OutputType::kBoolTrue}, + BitSetTestCase{BitSetStepKind::kAndEqual, + {IntValue(0), StringValue()}, + 0b101, + OutputType::kBoolFalse}, + BitSetTestCase{BitSetStepKind::kEqual, + {IntValue(0), IntValue(2), StringValue()}, + 0b101, + OutputType::kBoolFalse})); + +TEST(BitSetStandaloneTest, HandlesNonListAndErrorsAndUnknowns) { + cel::Activation activation; + google::protobuf::Arena arena; + cel::RuntimeOptions opts; + opts.unknown_processing = cel::UnknownProcessingOptions::kAttributeOnly; + cel::runtime_internal::RuntimeTypeProvider type_provider( + cel::internal::GetTestingDescriptorPool()); + + FlatExpressionEvaluatorState state( + /*value_stack_size=*/5, + /*comprehension_slot_count=*/0, /*iterator_stack_size=*/0, type_provider, + cel::internal::GetTestingDescriptorPool(), + cel::internal::GetTestingMessageFactory(), &arena); + + ASSERT_THAT(activation.SetUnknownPatterns({cel::AttributePattern( + "foo", {cel::AttributeQualifierPattern::OfString("bar")})}), + IsOk()); + + for (BitSetStepKind kind : + {BitSetStepKind::kAndIsNonZero, BitSetStepKind::kAndEqual, + BitSetStepKind::kEqual}) { + // Non-list top-of-stack operand returns NoMatchingOverloadError. + { + ExecutionPath steps; + steps.push_back(ExpressionStep::MakeGenericStep( + std::make_unique(IntValue(1)))); + steps.push_back(MakeBitSetStepForKind(kind, 0b1)); + ExecutionFrame frame(steps, activation, opts, state); + ASSERT_OK_AND_ASSIGN(Value result, frame.Evaluate()); + EXPECT_THAT(result, ErrorValueIs(absl_testing::StatusIs( + absl::StatusCode::kUnknown, + HasSubstr("No matching overload")))); + } + + // Partial unknown attribute on list returns UnknownValue. + { + ExecutionPath steps; + steps.push_back( + ExpressionStep::MakeGenericStep(std::make_unique( + MakeList(&arena, {IntValue(0)}), cel::Attribute("foo")))); + steps.push_back(MakeBitSetStepForKind(kind, 0b1)); + ExecutionFrame frame(steps, activation, opts, state); + ASSERT_OK_AND_ASSIGN(Value result, frame.Evaluate()); + EXPECT_THAT(result, ValueKindIs(ValueKind::kUnknown)); + } + + // Top-level ErrorValue on stack is propagated. + { + ExecutionPath steps; + steps.push_back( + ExpressionStep::MakeGenericStep(std::make_unique( + ErrorValue::From(absl::AbortedError("top_err"), &arena)))); + steps.push_back(MakeBitSetStepForKind(kind, 0b1)); + ExecutionFrame frame(steps, activation, opts, state); + ASSERT_OK_AND_ASSIGN(Value result, frame.Evaluate()); + EXPECT_THAT(result, ErrorValueIs(absl_testing::StatusIs( + absl::StatusCode::kAborted, "top_err"))); + } + } +} + +} // namespace +} // namespace google::api::expr::runtime diff --git a/eval/eval/evaluator_core.cc b/eval/eval/evaluator_core.cc index 7058c75ed..146ff67c6 100644 --- a/eval/eval/evaluator_core.cc +++ b/eval/eval/evaluator_core.cc @@ -415,6 +415,10 @@ std::optional ExpressionStep::StackDelta() const { case ExpressionStepKind::kBooleanAndJump: case ExpressionStepKind::kTernaryJump: case ExpressionStepKind::kFixedJump: + case ExpressionStepKind::kBitIsSet: + case ExpressionStepKind::kBitAndIsNonZero: + case ExpressionStepKind::kBitAndEqual: + case ExpressionStepKind::kBitEqual: return 0; case ExpressionStepKind::kBooleanOr: case ExpressionStepKind::kBooleanAnd: diff --git a/eval/eval/evaluator_core.h b/eval/eval/evaluator_core.h index fea6a4650..782632356 100644 --- a/eval/eval/evaluator_core.h +++ b/eval/eval/evaluator_core.h @@ -39,6 +39,7 @@ #include "common/value_kind.h" #include "common/values/list_value_builder.h" #include "eval/eval/attribute_utility.h" +#include "eval/eval/bitset_steps.h" #include "eval/eval/comprehension_slots.h" #include "eval/eval/comprehension_step.h" #include "eval/eval/create_list_step.h" @@ -118,6 +119,12 @@ enum class ExpressionStepKind : uint16_t { // Create list step. kCreateList = 33, kCreateSmallList = 34, + // Specialized bit operations implementing set checks. + // Each step contains a constant uint64_t bitmask. + kBitIsSet = 35, // In (member) (1 << arg) & mask != 0 + kBitAndIsNonZero = 36, // Any (intersect) & mask != 0 + kBitAndEqual = 37, // All (contains) & mask == mask + kBitEqual = 38, // Equivalent == mask }; struct BoolJumpStepInfo { @@ -374,6 +381,31 @@ class ExpressionStep { return step; } + static ExpressionStep MakeBitIsSetStep(uint64_t bitmask, int64_t id = -1) { + ExpressionStep step(ExpressionStepKind::kBitIsSet, id); + step.u_.bitmask = bitmask; + return step; + } + + static ExpressionStep MakeBitAndIsNonZeroStep(uint64_t bitmask, + int64_t id = -1) { + ExpressionStep step(ExpressionStepKind::kBitAndIsNonZero, id); + step.u_.bitmask = bitmask; + return step; + } + + static ExpressionStep MakeBitAndEqualStep(uint64_t bitmask, int64_t id = -1) { + ExpressionStep step(ExpressionStepKind::kBitAndEqual, id); + step.u_.bitmask = bitmask; + return step; + } + + static ExpressionStep MakeBitEqualStep(uint64_t bitmask, int64_t id = -1) { + ExpressionStep step(ExpressionStepKind::kBitEqual, id); + step.u_.bitmask = bitmask; + return step; + } + private: static ABSL_ATTRIBUTE_ALWAYS_INLINE inline void EvaluateReadSlotStep( size_t slot_index, ExecutionFrame& frame); @@ -448,6 +480,7 @@ class ExpressionStep { IdentStepInfo ident_step; ListStepInfo* create_list_step; SmallListStepInfo create_small_list_step; + uint64_t bitmask; Data() : empty(nullptr) {} ~Data() {} @@ -1093,6 +1126,10 @@ inline ExpressionStep::~ExpressionStep() { case ExpressionStepKind::kNewMutableList: case ExpressionStepKind::kMutableListAppend: case ExpressionStepKind::kCreateSmallList: + case ExpressionStepKind::kBitIsSet: + case ExpressionStepKind::kBitAndIsNonZero: + case ExpressionStepKind::kBitAndEqual: + case ExpressionStepKind::kBitEqual: break; default: ABSL_UNREACHABLE(); @@ -1291,6 +1328,18 @@ inline void ExpressionStep::Evaluate(ExecutionFrame& frame) const { case ExpressionStepKind::kCreateSmallList: EvaluateSmallListStep(u_.create_small_list_step, frame); break; + case ExpressionStepKind::kBitIsSet: + EvaluateBitIsSetStep(u_.bitmask, frame); + break; + case ExpressionStepKind::kBitAndIsNonZero: + EvaluateBitAndIsNonZeroStep(u_.bitmask, frame); + break; + case ExpressionStepKind::kBitAndEqual: + EvaluateBitAndEqualStep(u_.bitmask, frame); + break; + case ExpressionStepKind::kBitEqual: + EvaluateBitEqualStep(u_.bitmask, frame); + break; case ExpressionStepKind::kMovedFrom: frame.Abort(absl::InternalError( "ExpressionStep::Evaluate called on moved-from step")); diff --git a/extensions/BUILD b/extensions/BUILD index 93b08e6da..3a2fd682e 100644 --- a/extensions/BUILD +++ b/extensions/BUILD @@ -521,11 +521,14 @@ cc_test( name = "sets_functions_test", srcs = ["sets_functions_test.cc"], deps = [ + ":set_membership_optimization", ":sets_functions", "//checker:standard_library", "//checker:validation_result", "//common:ast_proto", "//common:minimal_descriptor_pool", + "//common:value", + "//compiler", "//compiler:compiler_factory", "//eval/public:activation", "//eval/public:builtin_func_registrar", @@ -535,7 +538,11 @@ cc_test( "//eval/public:cel_options", "//eval/public:cel_value", "//internal:testing", + "//runtime", + "//runtime:activation", + "//runtime:runtime_builder", "//runtime:runtime_options", + "//runtime:standard_runtime_builder_factory", "@com_google_absl//absl/status:status_matchers", "@com_google_cel_spec//proto/cel/expr:syntax_cc_proto", "@com_google_protobuf//:protobuf", @@ -547,25 +554,35 @@ cc_test( srcs = ["sets_functions_benchmark_test.cc"], tags = ["benchmark"], deps = [ + ":set_membership_optimization", ":sets_functions", + "//checker:type_checker_builder", + "//checker:validation_result", + "//common:ast", + "//common:decl", + "//common:minimal_descriptor_pool", + "//common:type", "//common:value", - "//eval/internal:interop", - "//eval/public:activation", - "//eval/public:builtin_func_registrar", - "//eval/public:cel_expr_builder_factory", - "//eval/public:cel_expression", - "//eval/public:cel_options", + "//compiler", + "//compiler:compiler_factory", + "//compiler:standard_library", "//eval/public:cel_value", "//eval/public/containers:container_backed_list_impl", "//internal:benchmark", "//internal:status_macros", "//internal:testing", - "//parser", + "//runtime", + "//runtime:activation", + "//runtime:constant_folding", + "//runtime:reference_resolver", + "//runtime:runtime_builder", "//runtime:runtime_options", + "//runtime:standard_runtime_builder_factory", "@com_google_absl//absl/base:nullability", + "@com_google_absl//absl/status", + "@com_google_absl//absl/status:status_matchers", "@com_google_absl//absl/status:statusor", "@com_google_absl//absl/strings", - "@com_google_cel_spec//proto/cel/expr:syntax_cc_proto", "@com_google_protobuf//:protobuf", ], ) @@ -854,3 +871,75 @@ cc_test( "@com_google_protobuf//:protobuf", ], ) + +cc_library( + name = "set_membership_optimization", + srcs = ["set_membership_optimization.cc"], + hdrs = ["set_membership_optimization.h"], + deps = [ + "//base:builtins", + "//common:ast", + "//common:constant", + "//common:expr", + "//common:native_type", + "//common:type", + "//common:value", + "//eval/compiler:flat_expr_builder", + "//eval/compiler:flat_expr_builder_extensions", + "//eval/eval:evaluator_core", + "//internal:casts", + "//internal:status_macros", + "//runtime", + "//runtime:runtime_builder", + "//runtime/internal:runtime_friend_access", + "//runtime/internal:runtime_impl", + "@com_google_absl//absl/status", + "@com_google_absl//absl/status:statusor", + "@com_google_absl//absl/strings:string_view", + ], +) + +cc_test( + name = "set_membership_optimization_test", + srcs = ["set_membership_optimization_test.cc"], + deps = [ + ":set_membership_optimization", + ":sets_functions", + "//base:attributes", + "//checker:type_checker_builder", + "//checker:validation_result", + "//common:ast", + "//common:decl", + "//common:type", + "//common:value", + "//common:value_kind", + "//common:value_testing", + "//compiler", + "//compiler:compiler_factory", + "//compiler:standard_library", + "//eval/compiler:constant_folding", + "//eval/compiler:flat_expr_builder", + "//eval/compiler:qualified_reference_resolver", + "//eval/eval:evaluator_core", + "//internal:status_macros", + "//internal:testing", + "//internal:testing_descriptor_pool", + "//runtime", + "//runtime:activation", + "//runtime:constant_folding", + "//runtime:reference_resolver", + "//runtime:runtime_builder", + "//runtime:runtime_options", + "//runtime:standard_functions", + "//runtime:standard_runtime_builder_factory", + "//runtime/internal:runtime_env", + "//runtime/internal:runtime_env_testing", + "@com_google_absl//absl/log:absl_check", + "@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/conformance/proto2:test_all_types_cc_proto", + "@com_google_protobuf//:protobuf", + ], +) diff --git a/extensions/set_membership_optimization.cc b/extensions/set_membership_optimization.cc new file mode 100644 index 000000000..44a0e593d --- /dev/null +++ b/extensions/set_membership_optimization.cc @@ -0,0 +1,349 @@ +// 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. + +#include "extensions/set_membership_optimization.h" + +#include +#include +#include +#include +#include + +#include "absl/status/status.h" +#include "absl/status/statusor.h" +#include "absl/strings/string_view.h" +#include "base/builtins.h" +#include "common/ast.h" +#include "common/constant.h" +#include "common/expr.h" +#include "common/native_type.h" +#include "common/type.h" +#include "common/value.h" +#include "eval/compiler/flat_expr_builder.h" +#include "eval/compiler/flat_expr_builder_extensions.h" +#include "eval/eval/bitset_steps.h" +#include "eval/eval/evaluator_core.h" +#include "internal/casts.h" +#include "internal/status_macros.h" +#include "runtime/internal/runtime_friend_access.h" +#include "runtime/internal/runtime_impl.h" +#include "runtime/runtime.h" +#include "runtime/runtime_builder.h" + +namespace cel::extensions { +namespace { + +using ::google::api::expr::runtime::ExecutionPath; +using ::google::api::expr::runtime::ExecutionPathView; +using ::google::api::expr::runtime::ExpressionStep; +using ::google::api::expr::runtime::ExtractBit; +using ::google::api::expr::runtime::FlatExprBuilder; +using ::google::api::expr::runtime::GetIfConstant; +using ::google::api::expr::runtime::PlannerContext; +using ::google::api::expr::runtime::ProgramBuilder; +using ::google::api::expr::runtime::ProgramOptimizer; +using ::google::api::expr::runtime::ProgramOptimizerFactory; + +constexpr absl::string_view kSetsContains = "sets.contains"; +constexpr absl::string_view kSetsIntersects = "sets.intersects"; +constexpr absl::string_view kSetsEquivalent = "sets.equivalent"; + +bool IsIntOrUint(const Type& type) { return type.IsInt() || type.IsUint(); } + +bool IsIntOrUintType(const PlannerContext& context, int64_t expr_id) { + std::optional type = context.GetResolvedType(expr_id); + if (!type.has_value()) { + return false; + } + return IsIntOrUint(*type); +} + +bool IsIntOrUintListType(const PlannerContext& context, int64_t expr_id) { + std::optional type = context.GetResolvedType(expr_id); + if (!type.has_value() || !type->IsList()) { + return false; + } + return IsIntOrUint(type->GetList().GetElement()); +} + +bool ShouldOptimizeEmptyList(const PlannerContext& context, + const Expr& list_arg, const Expr& other_arg, + bool is_in_op) { + return IsIntOrUintListType(context, list_arg.id()) || + (is_in_op ? IsIntOrUintType(context, other_arg.id()) + : IsIntOrUintListType(context, other_arg.id())); +} + +std::optional TryExtractBitmaskFromListExpr(const ListExpr& list_expr, + bool allow_empty_list) { + if (list_expr.elements().empty()) { + return allow_empty_list ? std::optional(0) : std::nullopt; + } + + std::optional expected_kind; + uint64_t bitmask = 0; + + for (const ListExprElement& element : list_expr.elements()) { + if (element.optional() || !element.has_expr() || + !element.expr().has_const_expr()) { + return std::nullopt; + } + const Constant& constant = element.expr().const_expr(); + ConstantKindCase kind = constant.kind_case(); + if (kind != ConstantKindCase::kInt && kind != ConstantKindCase::kUint) { + return std::nullopt; + } + if (!expected_kind.has_value()) { + expected_kind = kind; + } else if (*expected_kind != kind) { + return std::nullopt; + } + + if (kind == ConstantKindCase::kInt) { + int64_t v = constant.int_value(); + if (v < 0 || v >= 64) { + return std::nullopt; + } + bitmask |= (uint64_t{1} << v); + } else { + uint64_t v = constant.uint_value(); + if (v >= 64) { + return std::nullopt; + } + bitmask |= (uint64_t{1} << v); + } + } + + return bitmask; +} + +std::optional TryExtractBitmaskFromFoldedList( + PlannerContext& context, const ListValue& list_value, + bool allow_empty_list) { + absl::StatusOr size = list_value.Size(); + if (!size.ok()) { + return std::nullopt; + } + if (*size == 0) { + return allow_empty_list ? std::optional(0) : std::nullopt; + } + + uint64_t bitmask = 0; + bool valid = true; + + absl::Status status = list_value.ForEach( + [&](size_t /*index*/, const Value& element) -> absl::StatusOr { + uint64_t bit = ExtractBit(element); + if (bit == 0) { + valid = false; + return false; + } + bitmask |= bit; + return true; + }, + context.descriptor_pool(), context.MutableMessageFactory(), + context.MutableArena()); + + if (!status.ok() || !valid) { + return std::nullopt; + } + return bitmask; +} + +std::optional TryExtractBitmask(PlannerContext& context, + const Expr& list_arg, + bool allow_empty_list) { + ExecutionPathView subplan = context.GetSubplan(list_arg); + if (subplan.empty()) { + return std::nullopt; + } + if (subplan.size() == 1) { + Value constant_value; + if (GetIfConstant(subplan[0], constant_value) && constant_value.IsList()) { + return TryExtractBitmaskFromFoldedList(context, constant_value.GetList(), + allow_empty_list); + } + } + + if (list_arg.has_list_expr()) { + return TryExtractBitmaskFromListExpr(list_arg.list_expr(), + allow_empty_list); + } + + return std::nullopt; +} + +bool IsInFunction(absl::string_view fn) { + return fn == cel::builtin::kIn || fn == cel::builtin::kInDeprecated || + fn == cel::builtin::kInFunction; +} + +bool IsTargetFunction(absl::string_view fn) { + return IsInFunction(fn) || fn == kSetsContains || fn == kSetsIntersects || + fn == kSetsEquivalent; +} + +class SetMembershipOptimizer : public ProgramOptimizer { + public: + explicit SetMembershipOptimizer( + const SetMembershipOptimizationOptions& options) + : options_(options) {} + + absl::Status OnPreVisit(PlannerContext& context, const Expr& node) override { + return absl::OkStatus(); + } + + absl::Status OnPostVisit(PlannerContext& context, const Expr& node) override { + if (!node.has_call_expr()) { + return absl::OkStatus(); + } + + const CallExpr& call = node.call_expr(); + if (call.has_target() || call.args().size() != 2) { + return absl::OkStatus(); + } + + absl::string_view fn = call.function(); + if (!IsTargetFunction(fn)) { + return absl::OkStatus(); + } + + const Expr& arg0 = call.args()[0]; + const Expr& arg1 = call.args()[1]; + if (!context.NodeHasInspectablePlan(arg0) || + !context.NodeHasInspectablePlan(arg1)) { + return absl::OkStatus(); + } + + if (IsInFunction(fn)) { + bool allow_empty_list = + ShouldOptimizeEmptyList(context, arg1, arg0, /*is_in_op=*/true); + std::optional bitmask = + TryExtractBitmask(context, arg1, allow_empty_list); + if (!bitmask.has_value()) { + return absl::OkStatus(); + } + return ReplaceWithBitsetStep( + context, node, arg0, + ExpressionStep::MakeBitIsSetStep(*bitmask, node.id())); + } + + if (fn == kSetsContains) { + bool allow_empty_list = + ShouldOptimizeEmptyList(context, arg1, arg0, /*is_in_op=*/false); + std::optional bitmask = + TryExtractBitmask(context, arg1, allow_empty_list); + if (!bitmask.has_value()) { + return absl::OkStatus(); + } + return ReplaceWithBitsetStep( + context, node, arg0, + ExpressionStep::MakeBitAndEqualStep(*bitmask, node.id())); + } + + if (fn == kSetsIntersects) { + bool allow_empty_list = + ShouldOptimizeEmptyList(context, arg1, arg0, /*is_in_op=*/false); + std::optional bitmask = + TryExtractBitmask(context, arg1, allow_empty_list); + if (bitmask.has_value()) { + return ReplaceWithBitsetStep( + context, node, arg0, + ExpressionStep::MakeBitAndIsNonZeroStep(*bitmask, node.id())); + } + bitmask = TryExtractBitmask(context, arg0, allow_empty_list); + if (bitmask.has_value()) { + return ReplaceWithBitsetStep( + context, node, arg1, + ExpressionStep::MakeBitAndIsNonZeroStep(*bitmask, node.id())); + } + return absl::OkStatus(); + } + + if (fn == kSetsEquivalent) { + bool allow_empty_list = + ShouldOptimizeEmptyList(context, arg1, arg0, /*is_in_op=*/false); + std::optional bitmask = + TryExtractBitmask(context, arg1, allow_empty_list); + if (bitmask.has_value()) { + return ReplaceWithBitsetStep( + context, node, arg0, + ExpressionStep::MakeBitEqualStep(*bitmask, node.id())); + } + bitmask = TryExtractBitmask(context, arg0, allow_empty_list); + if (bitmask.has_value()) { + return ReplaceWithBitsetStep( + context, node, arg1, + ExpressionStep::MakeBitEqualStep(*bitmask, node.id())); + } + return absl::OkStatus(); + } + + return absl::OkStatus(); + } + + private: + static absl::Status ReplaceWithBitsetStep(PlannerContext& context, + const Expr& call_node, + const Expr& var_arg, + ExpressionStep bitset_step) { + CEL_ASSIGN_OR_RETURN(ExecutionPath path, context.ExtractSubplan(var_arg)); + path.push_back(std::move(bitset_step)); + return context.ReplaceSubplan(call_node, std::move(path)); + } + + [[maybe_unused]] SetMembershipOptimizationOptions options_; +}; + +FlatExprBuilder* GetFlatExprBuilder(RuntimeBuilder& builder) { + Runtime& runtime = + runtime_internal::RuntimeFriendAccess::GetMutableRuntime(builder); + if (runtime_internal::RuntimeFriendAccess::RuntimeTypeId(runtime) == + NativeTypeId::For()) { + runtime_internal::RuntimeImpl& runtime_impl = + cel::internal::down_cast(runtime); + return &runtime_impl.expr_builder(); + } + return nullptr; +} + +} // namespace + +ProgramOptimizerFactory CreateSetMembershipOptimizationProgramOptimizer( + const SetMembershipOptimizationOptions& options) { + return [options]( + PlannerContext& context, + const Ast&) -> absl::StatusOr> { + if (!context.options().enable_heterogeneous_equality) { + return nullptr; + } + context.set_resolve_types(); + return std::make_unique(options); + }; +} + +absl::Status EnableSetMembershipOptimization( + cel::RuntimeBuilder& builder, + const SetMembershipOptimizationOptions& options) { + FlatExprBuilder* flat_expr_builder = GetFlatExprBuilder(builder); + if (flat_expr_builder == nullptr) { + return absl::InvalidArgumentError( + "SetMembershipOptimization requires default runtime implementation"); + } + flat_expr_builder->AddProgramOptimizer( + CreateSetMembershipOptimizationProgramOptimizer(options)); + return absl::OkStatus(); +} + +} // namespace cel::extensions diff --git a/extensions/set_membership_optimization.h b/extensions/set_membership_optimization.h new file mode 100644 index 000000000..718b641e5 --- /dev/null +++ b/extensions/set_membership_optimization.h @@ -0,0 +1,63 @@ +// 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. + +#ifndef THIRD_PARTY_CEL_CPP_EXTENSIONS_SET_MEMBERSHIP_OPTIMIZATION_H_ +#define THIRD_PARTY_CEL_CPP_EXTENSIONS_SET_MEMBERSHIP_OPTIMIZATION_H_ + +#include "absl/status/status.h" +#include "eval/compiler/flat_expr_builder_extensions.h" +#include "runtime/runtime_builder.h" + +namespace cel::extensions { + +// Options for set membership optimization. +// TODO(b/572083203): Add options for non-bitset constant list optimizations. +struct SetMembershipOptimizationOptions {}; + +// Enable set membership optimization on the given RuntimeBuilder. +// +// This should only be called *once* on a given runtime builder. +// +// This optimization assumes that `enable_heterogeneous_equality` is enabled. +// +// When enabled, the runtime will attempt to optimize branches that appear +// to implement a set membership tests against a constant list. For small +// integer lists, this is implemented via a precomputed bitset. list is +// emphasized to cover the common case of testing protobuf enums against a +// constant list. +// +// Examples: +// foo in [EnumA, EnumB, EnumC] +// sets.contains(foos, [EnumA, EnumB, EnumC]) // foos has all +// sets.intersects(foos, [EnumA, EnumB, EnumC]) // foos has any +// sets.equivalent(foos, [EnumA, EnumB, EnumC]) +// +// Should be called after `EnableConstantFolding` if enabled. +absl::Status EnableSetMembershipOptimization( + cel::RuntimeBuilder& builder, + const SetMembershipOptimizationOptions& options = {}); + +// =============================================================== +// Implementation details -- CEL users should not depend on these. +// Exposed here for enabling on Legacy APIs. They expose internal details +// which are not guaranteed to be stable. +// =============================================================== + +google::api::expr::runtime::ProgramOptimizerFactory +CreateSetMembershipOptimizationProgramOptimizer( + const SetMembershipOptimizationOptions& options = {}); + +} // namespace cel::extensions + +#endif // THIRD_PARTY_CEL_CPP_EXTENSIONS_SET_MEMBERSHIP_OPTIMIZATION_H_ diff --git a/extensions/set_membership_optimization_test.cc b/extensions/set_membership_optimization_test.cc new file mode 100644 index 000000000..9c17e9ab7 --- /dev/null +++ b/extensions/set_membership_optimization_test.cc @@ -0,0 +1,393 @@ +// 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. + +#include "extensions/set_membership_optimization.h" + +#include +#include +#include +#include +#include + +#include "absl/log/absl_check.h" +#include "absl/status/status.h" +#include "absl/status/status_matchers.h" +#include "absl/status/statusor.h" +#include "absl/strings/string_view.h" +#include "base/attribute.h" +#include "checker/type_checker_builder.h" +#include "checker/validation_result.h" +#include "common/ast.h" +#include "common/decl.h" +#include "common/type.h" +#include "common/value.h" +#include "common/value_kind.h" +#include "common/value_testing.h" +#include "compiler/compiler.h" +#include "compiler/compiler_factory.h" +#include "compiler/standard_library.h" +#include "eval/compiler/constant_folding.h" +#include "eval/compiler/flat_expr_builder.h" +#include "eval/compiler/qualified_reference_resolver.h" +#include "eval/eval/evaluator_core.h" +#include "extensions/sets_functions.h" +#include "internal/status_macros.h" +#include "internal/testing.h" +#include "internal/testing_descriptor_pool.h" +#include "runtime/activation.h" +#include "runtime/constant_folding.h" +#include "runtime/internal/runtime_env.h" +#include "runtime/internal/runtime_env_testing.h" +#include "runtime/reference_resolver.h" +#include "runtime/runtime.h" +#include "runtime/runtime_builder.h" +#include "runtime/runtime_options.h" +#include "runtime/standard_functions.h" +#include "runtime/standard_runtime_builder_factory.h" +#include "cel/expr/conformance/proto2/test_all_types.pb.h" +#include "google/protobuf/arena.h" +#include "google/protobuf/descriptor.h" + +namespace cel::extensions { +namespace { + +using ::absl_testing::IsOk; +using ::cel::expr::conformance::proto2::TestAllTypes; +using ::cel::runtime_internal::NewTestingRuntimeEnv; +using ::cel::test::BoolValueIs; +using ::cel::test::ValueKindIs; +using ::google::api::expr::runtime::FlatExprBuilder; +using ::google::api::expr::runtime::FlatExpression; +using ::google::api::expr::runtime::NewReferenceResolverExtension; +using ::google::api::expr::runtime::ReferenceResolverOption; +using ::testing::SizeIs; + +absl::StatusOr> CreateTestCompiler() { + CEL_ASSIGN_OR_RETURN( + std::unique_ptr builder, + NewCompilerBuilder(cel::internal::GetTestingDescriptorPool())); + CEL_RETURN_IF_ERROR(builder->AddLibrary(StandardCompilerLibrary())); + CEL_RETURN_IF_ERROR(builder->AddLibrary(SetsCompilerLibrary())); + + TypeCheckerBuilder& checker = builder->GetCheckerBuilder(); + checker.set_container("cel.expr.conformance.proto2"); + CEL_RETURN_IF_ERROR(checker.AddContextDeclaration( + "cel.expr.conformance.proto2.TestAllTypes")); + CEL_RETURN_IF_ERROR(checker.AddVariable(MakeVariableDecl("x", IntType()))); + CEL_RETURN_IF_ERROR(checker.AddVariable(MakeVariableDecl("u", UintType()))); + CEL_RETURN_IF_ERROR(checker.AddVariable(MakeVariableDecl("d", DynType()))); + CEL_RETURN_IF_ERROR(checker.AddVariable( + MakeVariableDecl("int_list", ListType(checker.arena(), IntType())))); + CEL_RETURN_IF_ERROR(checker.AddVariable( + MakeVariableDecl("uint_list", ListType(checker.arena(), UintType())))); + CEL_RETURN_IF_ERROR(checker.AddVariable( + MakeVariableDecl("dyn_list", ListType(checker.arena(), DynType())))); + CEL_RETURN_IF_ERROR(checker.AddVariable( + MakeVariableDecl("str_list", ListType(checker.arena(), StringType())))); + + return builder->Build(); +} + +absl::StatusOr> CompileTestExpr(absl::string_view expr) { + static const Compiler* const kCompiler = []() { + absl::StatusOr> compiler = CreateTestCompiler(); + ABSL_CHECK_OK(compiler.status()); + return compiler->release(); + }(); + + CEL_ASSIGN_OR_RETURN(ValidationResult result, kCompiler->Compile(expr)); + if (!result.IsValid()) { + return absl::InvalidArgumentError(result.FormatError()); + } + return result.ReleaseAst(); +} + +absl::StatusOr> BuildTestRuntime( + bool enable_constant_folding, UnknownProcessingOptions unknown_processing = + UnknownProcessingOptions::kDisabled) { + RuntimeOptions options; + options.container = "cel.expr.conformance.proto2"; + options.enable_heterogeneous_equality = true; + options.unknown_processing = unknown_processing; + + CEL_ASSIGN_OR_RETURN(RuntimeBuilder builder, + CreateStandardRuntimeBuilder( + cel::internal::GetTestingDescriptorPool(), options)); + CEL_RETURN_IF_ERROR( + RegisterSetsFunctions(builder.function_registry(), options)); + CEL_RETURN_IF_ERROR( + EnableReferenceResolver(builder, ReferenceResolverEnabled::kAlways)); + if (enable_constant_folding) { + CEL_RETURN_IF_ERROR(EnableConstantFolding(builder)); + } + CEL_RETURN_IF_ERROR(EnableSetMembershipOptimization(builder)); + return std::move(builder).Build(); +} + +Value MakeIntList(google::protobuf::Arena* arena, const std::vector& values) { + ListValueBuilderPtr builder = NewListValueBuilder(arena); + for (int64_t v : values) { + ABSL_CHECK_OK(builder->Add(IntValue(v))); + } + return std::move(*builder).Build(); +} + +Value MakeUintList(google::protobuf::Arena* arena, const std::vector& values) { + ListValueBuilderPtr builder = NewListValueBuilder(arena); + for (uint64_t v : values) { + ABSL_CHECK_OK(builder->Add(UintValue(v))); + } + return std::move(*builder).Build(); +} + +Value MakeValueList(google::protobuf::Arena* arena, const std::vector& values) { + ListValueBuilderPtr builder = NewListValueBuilder(arena); + for (const Value& v : values) { + ABSL_CHECK_OK(builder->Add(v)); + } + return std::move(*builder).Build(); +} + +TEST(SetMembershipOptimizationPlanTest, EmitsCompactBitsetPlan) { + RuntimeOptions options; + options.container = "cel.expr.conformance.proto2"; + options.enable_heterogeneous_equality = true; + std::shared_ptr env = + NewTestingRuntimeEnv(); + ASSERT_THAT(RegisterSetsFunctions(env->function_registry, options), IsOk()); + FlatExprBuilder builder(std::move(env), options); + builder.set_container(options.container); + builder.AddAstTransform( + NewReferenceResolverExtension(ReferenceResolverOption::kAlways)); + builder.AddProgramOptimizer( + CreateSetMembershipOptimizationProgramOptimizer()); + + for (absl::string_view expr_str : { + "x in [1, 2, 3, 4]", + "sets.contains(int_list, [1, 2, 3, 4])", + "sets.intersects(int_list, [1, 2, 3, 4])", + "sets.intersects([1, 2, 3, 4], int_list)", + "sets.equivalent(int_list, [1, 2, 3, 4])", + "sets.equivalent([1, 2, 3, 4], int_list)", + }) { + ASSERT_OK_AND_ASSIGN(std::unique_ptr ast, CompileTestExpr(expr_str)); + ASSERT_OK_AND_ASSIGN(FlatExpression plan, + builder.CreateExpressionImpl(std::move(ast), nullptr)); + // Optimized plan consists of just 2 steps: reading the variable and + // evaluating the bitset step. + EXPECT_THAT(plan.path(), SizeIs(2)) << expr_str; + } +} + +TEST(SetMembershipOptimizationPlanTest, + EmitsCompactBitsetPlanWithConstantFoldedList) { + RuntimeOptions options; + options.container = "cel.expr.conformance.proto2"; + options.enable_heterogeneous_equality = true; + std::shared_ptr env = + NewTestingRuntimeEnv(); + ASSERT_THAT(RegisterStandardFunctions(env->function_registry, options), + IsOk()); + ASSERT_THAT(RegisterSetsFunctions(env->function_registry, options), IsOk()); + FlatExprBuilder builder(std::move(env), options); + builder.set_container(options.container); + builder.AddAstTransform( + NewReferenceResolverExtension(ReferenceResolverOption::kAlways)); + builder.AddProgramOptimizer( + cel::runtime_internal::CreateConstantFoldingOptimizer()); + builder.AddProgramOptimizer( + CreateSetMembershipOptimizationProgramOptimizer()); + + for (absl::string_view expr_str : { + "x in ([1, 2] + [3, 4])", + "sets.contains(int_list, [1, 2] + [3, 4])", + "sets.intersects(int_list, [1, 2] + [3, 4])", + "sets.equivalent([1, 2] + [3, 4], int_list)", + }) { + ASSERT_OK_AND_ASSIGN(std::unique_ptr ast, CompileTestExpr(expr_str)); + ASSERT_OK_AND_ASSIGN(FlatExpression plan, + builder.CreateExpressionImpl(std::move(ast), nullptr)); + // Constant folding folds `[1, 2] + [3, 4]` into a single constant ListValue + // step, which SetMembershipOptimization then replaces with a 2-step plan. + EXPECT_THAT(plan.path(), SizeIs(2)) << expr_str; + } +} + +struct EvalTestCase { + std::string expr; + bool expected; +}; + +class SetMembershipOptimizationEvalTest + : public ::testing::TestWithParam {}; + +TEST_P(SetMembershipOptimizationEvalTest, EvaluatesCorrectly) { + const bool enable_constant_folding = GetParam(); + ASSERT_OK_AND_ASSIGN(std::unique_ptr runtime, + BuildTestRuntime(enable_constant_folding)); + + google::protobuf::Arena arena; + Activation activation; + activation.InsertOrAssignValue("x", IntValue(2)); + activation.InsertOrAssignValue("u", UintValue(63)); + activation.InsertOrAssignValue("d", DoubleValue(2.0)); + activation.InsertOrAssignValue("int_list", MakeIntList(&arena, {0, 2, 63})); + activation.InsertOrAssignValue("uint_list", MakeUintList(&arena, {0, 2})); + activation.InsertOrAssignValue( + "dyn_list", MakeValueList(&arena, {IntValue(0), DoubleValue(2.0)})); + activation.InsertOrAssignValue( + "str_list", MakeValueList(&arena, {StringValue::From("a", &arena), + StringValue::From("b", &arena)})); + + const std::vector test_cases = { + // @in with int, uint, dyn(double), and protobuf enums + {"x in [0, 2, 63]", true}, + {"x in [0, 1, 63]", false}, + {"u in [0u, 2u, 63u]", true}, + {"u in [0u, 2u]", false}, + {"d in [0, 2, 63]", true}, + {"d in [0, 1, 63]", false}, + {"dyn(2.5) in [0, 2, 63]", false}, + {"-1 in [0, 2, 63]", false}, + {"64 in [0, 2, 63]", false}, + {"x in [TestAllTypes.NestedEnum.FOO, TestAllTypes.NestedEnum.BAR, " + "TestAllTypes.NestedEnum.BAZ]", + true}, + {"x in [TestAllTypes.NestedEnum.FOO, TestAllTypes.NestedEnum.BAR]", + false}, + + // sets.contains + {"sets.contains(int_list, [0, 2])", true}, + {"sets.contains(int_list, [0, 2, 63])", true}, + {"sets.contains(int_list, [0, 1, 2])", false}, + {"sets.contains(uint_list, [0u, 2u])", true}, + {"sets.contains(uint_list, [0u, 1u])", false}, + {"sets.contains(dyn_list, [0, 2])", true}, + {"sets.contains(int_list, [TestAllTypes.NestedEnum.FOO, " + "TestAllTypes.NestedEnum.BAZ])", + true}, + // Non-optimizable 1st arg constant list still evaluates via standard fn + {"sets.contains([0, 2, 63], int_list)", true}, + {"sets.contains([0, 2], int_list)", false}, + + // sets.intersects (both argument orders) + {"sets.intersects(int_list, [1, 2])", true}, + {"sets.intersects([1, 2], int_list)", true}, + {"sets.intersects(int_list, [1, 3])", false}, + {"sets.intersects([1, 3], int_list)", false}, + {"sets.intersects(uint_list, [2u, 5u])", true}, + {"sets.intersects([2u, 5u], uint_list)", true}, + {"sets.intersects(dyn_list, [2, 5])", true}, + + // sets.equivalent (both argument orders) + {"sets.equivalent(int_list, [0, 2, 63])", true}, + {"sets.equivalent([0, 2, 63], int_list)", true}, + {"sets.equivalent(int_list, [0, 2, 2, 63])", true}, + {"sets.equivalent(int_list, [0, 2])", false}, + {"sets.equivalent([0, 2], int_list)", false}, + {"sets.equivalent(int_list, [0, 1, 2, 63])", false}, + {"sets.equivalent(dyn_list, [0, 2])", true}, + {"sets.equivalent([0, 2], dyn_list)", true}, + {"sets.equivalent([0, 2, 64], [0, 2])", false}, + {"sets.equivalent([0, 2, -1], [0, 2])", false}, + {"sets.equivalent([dyn(0), dyn(2), dyn(1.5)], [0, 2])", false}, + + // Empty constant lists (typed as int / uint) + {"x in []", false}, + {"u in []", false}, + {"sets.contains(int_list, [])", true}, + {"sets.intersects(int_list, [])", false}, + {"sets.intersects([], int_list)", false}, + {"sets.equivalent(int_list, [])", false}, + {"sets.equivalent([], int_list)", false}, + {"sets.equivalent( int_list.filter(i, i < 0), [])", true}, + + // Untyped empty list with non-numeric dyn variable (not optimized to + // bitset, so returns false rather than overload error) + {"'hello' in []", false}, + + // Fallback when constant list has out-of-range values, mixed types, or + // strings + {"x in [-1, 2, 100]", true}, + {"100 in [-1, 2, 100]", true}, + {"-1 in [-1, 2, 100]", true}, + {"x in [1, 64]", false}, + {"d in [dyn(1), dyn(2u), dyn(3.5)]", true}, + {"'a' in ['a', 'b', 'c', 'd']", true}, + {"sets.contains(str_list, ['a', 'b'])", true}, + {"sets.intersects(str_list, ['b', 'c'])", true}, + {"sets.equivalent(str_list, ['b', 'a'])", true}, + + // Heterogeneous non-numeric dynamic elements with constant int sets + {"dyn('1') in [0, 1, 2]", false}, + {"sets.contains([dyn('1'), dyn(1)], [1])", true}, + {"sets.intersects([1], [dyn('1'), dyn(2)])", false}, + {"sets.equivalent([1], [dyn('1'), dyn(1)])", false}, + + // Constant-folded list expression when constant folding is enabled + {"x in ([0, 1] + [2, 3])", true}, + {"sets.contains(int_list, [0] + [2])", true}, + }; + + for (const EvalTestCase& tc : test_cases) { + ASSERT_OK_AND_ASSIGN(std::unique_ptr ast, CompileTestExpr(tc.expr)); + ASSERT_OK_AND_ASSIGN(std::unique_ptr program, + runtime->CreateProgram(std::move(ast))); + ASSERT_OK_AND_ASSIGN(Value result, program->Evaluate(&arena, activation)); + EXPECT_THAT(result, BoolValueIs(tc.expected)) << "Expression: " << tc.expr; + } +} + +INSTANTIATE_TEST_SUITE_P(SetMembershipOptimizationEvalTest, + SetMembershipOptimizationEvalTest, testing::Bool()); + +TEST(SetMembershipOptimizationUnknownAndErrorTest, + PropagatesUnknownsAndErrors) { + ASSERT_OK_AND_ASSIGN( + std::unique_ptr runtime, + BuildTestRuntime(/*enable_constant_folding=*/true, + UnknownProcessingOptions::kAttributeAndFunction)); + + google::protobuf::Arena arena; + Activation activation; + ASSERT_THAT(activation.SetUnknownPatterns({AttributePattern("x", {}), + AttributePattern("int_list", {})}), + IsOk()); + + for (absl::string_view expr_str : { + "x in [1, 2, 3]", + "sets.contains(int_list, [1, 2])", + "sets.intersects(int_list, [1, 2])", + "sets.equivalent(int_list, [1, 2])", + }) { + ASSERT_OK_AND_ASSIGN(std::unique_ptr ast, CompileTestExpr(expr_str)); + ASSERT_OK_AND_ASSIGN(std::unique_ptr program, + runtime->CreateProgram(std::move(ast))); + ASSERT_OK_AND_ASSIGN(Value result, program->Evaluate(&arena, activation)); + EXPECT_THAT(result, ValueKindIs(ValueKind::kUnknown)) << expr_str; + } + + // Runtime error in operand propagates as ErrorValue. + { + ASSERT_OK_AND_ASSIGN(std::unique_ptr ast, + CompileTestExpr("(1 / 0) in [1, 2, 3]")); + ASSERT_OK_AND_ASSIGN(std::unique_ptr program, + runtime->CreateProgram(std::move(ast))); + ASSERT_OK_AND_ASSIGN(Value result, program->Evaluate(&arena, activation)); + EXPECT_THAT(result, ValueKindIs(ValueKind::kError)); + } +} + +} // namespace +} // namespace cel::extensions diff --git a/extensions/sets_functions_benchmark_test.cc b/extensions/sets_functions_benchmark_test.cc index 0b51f1464..493a46d2b 100644 --- a/extensions/sets_functions_benchmark_test.cc +++ b/extensions/sets_functions_benchmark_test.cc @@ -17,52 +17,65 @@ #include #include -#include "cel/expr/syntax.pb.h" #include "absl/base/nullability.h" +#include "absl/status/status.h" +#include "absl/status/status_matchers.h" #include "absl/status/statusor.h" #include "absl/strings/str_cat.h" #include "absl/strings/str_join.h" #include "absl/strings/str_replace.h" +#include "checker/type_checker_builder.h" +#include "checker/validation_result.h" +#include "common/ast.h" +#include "common/decl.h" +#include "common/legacy_value.h" +#include "common/minimal_descriptor_pool.h" +#include "common/type.h" #include "common/value.h" -#include "eval/internal/interop.h" -#include "eval/public/activation.h" -#include "eval/public/builtin_func_registrar.h" -#include "eval/public/cel_expr_builder_factory.h" -#include "eval/public/cel_expression.h" -#include "eval/public/cel_options.h" +#include "compiler/compiler.h" +#include "compiler/compiler_factory.h" +#include "compiler/standard_library.h" #include "eval/public/cel_value.h" #include "eval/public/containers/container_backed_list_impl.h" +#include "extensions/set_membership_optimization.h" #include "extensions/sets_functions.h" #include "internal/benchmark.h" #include "internal/status_macros.h" #include "internal/testing.h" -#include "parser/parser.h" +#include "runtime/activation.h" +#include "runtime/constant_folding.h" +#include "runtime/reference_resolver.h" +#include "runtime/runtime.h" +#include "runtime/runtime_builder.h" #include "runtime/runtime_options.h" +#include "runtime/standard_runtime_builder_factory.h" #include "google/protobuf/arena.h" namespace cel::extensions { namespace { -using ::cel::Value; -using ::cel::expr::ParsedExpr; -using ::google::api::expr::parser::Parse; -using ::google::api::expr::runtime::Activation; +using ::absl_testing::IsOk; +using ::google::api::expr::runtime::CelList; using ::google::api::expr::runtime::CelValue; using ::google::api::expr::runtime::ContainerBackedListImpl; -using ::google::api::expr::runtime::CreateCelExpressionBuilder; -using ::google::api::expr::runtime::InterpreterOptions; -using ::google::api::expr::runtime::RegisterBuiltinFunctions; -enum class ListImpl : int { kLegacy = 0, kWrappedModern = 1, kRhsConstant = 2 }; +enum class ListImpl : int { + kLegacy = 0, + kModern = 1, + kRhsConstant = 2, + kRhsConstantOptimized = 3, +}; int ToNumber(ListImpl impl) { return static_cast(impl); } ListImpl FromNumber(int number) { switch (number) { case 0: return ListImpl::kLegacy; case 1: - return ListImpl::kWrappedModern; + return ListImpl::kModern; case 2: return ListImpl::kRhsConstant; + case 3: + return ListImpl::kRhsConstantOptimized; default: return ListImpl::kLegacy; } @@ -73,16 +86,19 @@ struct TestCase { std::string expr; ListImpl list_impl; int size; - CelValue result; + bool result; std::string MakeLabel(int len) const { std::string list_impl; switch (this->list_impl) { + case ListImpl::kRhsConstantOptimized: + list_impl = "rhs_constant_opt"; + break; case ListImpl::kRhsConstant: list_impl = "rhs_constant"; break; - case ListImpl::kWrappedModern: - list_impl = "wrapped_modern"; + case ListImpl::kModern: + list_impl = "modern"; break; case ListImpl::kLegacy: list_impl = "legacy"; @@ -93,43 +109,9 @@ struct TestCase { } }; -class ListStorage { - public: - virtual ~ListStorage() = default; -}; - -class LegacyListStorage : public ListStorage { - public: - LegacyListStorage(ContainerBackedListImpl x, ContainerBackedListImpl y) - : x_(std::move(x)), y_(std::move(y)) {} - - CelValue x() { return CelValue::CreateList(&x_); } - CelValue y() { return CelValue::CreateList(&y_); } - - private: - ContainerBackedListImpl x_; - ContainerBackedListImpl y_; -}; - -class ModernListStorage : public ListStorage { - public: - ModernListStorage(Value x, Value y) : x_(std::move(x)), y_(std::move(y)) {} - - CelValue x() { - return interop_internal::ModernValueToLegacyValueOrDie(&arena_, x_); - } - CelValue y() { - return interop_internal::ModernValueToLegacyValueOrDie(&arena_, y_); - } - - private: - google::protobuf::Arena arena_; - Value x_; - Value y_; -}; - -absl::StatusOr> RegisterLegacyLists( - bool overlap, int len, Activation& activation) { +absl::Status RegisterLegacyLists(bool overlap, int len, + google::protobuf::Arena* absl_nonnull arena, + Activation& activation) { std::vector x; std::vector y; x.reserve(len + 1); @@ -144,30 +126,33 @@ absl::StatusOr> RegisterLegacyLists( y.push_back(CelValue::CreateInt64(2)); } - auto result = std::make_unique( - ContainerBackedListImpl(std::move(x)), - ContainerBackedListImpl(std::move(y))); + const CelList* x_list = + google::protobuf::Arena::Create(arena, std::move(x)); + const CelList* y_list = + google::protobuf::Arena::Create(arena, std::move(y)); - activation.InsertValue("x", result->x()); - activation.InsertValue("y", result->y()); - return result; + CEL_ASSIGN_OR_RETURN(Value x_val, + cel::ModernValue(arena, CelValue::CreateList(x_list))); + CEL_ASSIGN_OR_RETURN(Value y_val, + cel::ModernValue(arena, CelValue::CreateList(y_list))); + + activation.InsertOrAssignValue("x", std::move(x_val)); + activation.InsertOrAssignValue("y", std::move(y_val)); + return absl::OkStatus(); } // Constant list literal that has the same elements as the bound test cases. std::string ConstantList(bool overlap, int len) { - std::string list_body; - for (int i = 0; i < len; i++) { - } return absl::StrCat("[", overlap ? "1, " : "", absl::StrJoin(std::vector(len, "2"), ", "), "]"); } -absl::StatusOr> RegisterModernLists( - bool overlap, int len, google::protobuf::Arena* absl_nonnull arena, - Activation& activation) { - auto x_builder = cel::NewListValueBuilder(arena); - auto y_builder = cel::NewListValueBuilder(arena); +absl::Status RegisterModernLists(bool overlap, int len, + google::protobuf::Arena* absl_nonnull arena, + Activation& activation) { + ListValueBuilderPtr x_builder = cel::NewListValueBuilder(arena); + ListValueBuilderPtr y_builder = cel::NewListValueBuilder(arena); x_builder->Reserve(len + 1); y_builder->Reserve(len + 1); @@ -182,81 +167,145 @@ absl::StatusOr> RegisterModernLists( CEL_RETURN_IF_ERROR(y_builder->Add(cel::IntValue(2))); } - auto x = std::move(*x_builder).Build(); - auto y = std::move(*y_builder).Build(); - auto result = std::make_unique(std::move(x), std::move(y)); - activation.InsertValue("x", result->x()); - activation.InsertValue("y", result->y()); + activation.InsertOrAssignValue("x", std::move(*x_builder).Build()); + activation.InsertOrAssignValue("y", std::move(*y_builder).Build()); - return result; + return absl::OkStatus(); } -absl::StatusOr> RegisterLists( - bool overlap, int len, bool use_modern, google::protobuf::Arena* absl_nonnull arena, - Activation& activation) { +absl::Status RegisterLists(bool overlap, int len, bool use_modern, + google::protobuf::Arena* absl_nonnull arena, + Activation& activation) { if (use_modern) { return RegisterModernLists(overlap, len, arena, activation); } else { - return RegisterLegacyLists(overlap, len, activation); + return RegisterLegacyLists(overlap, len, arena, activation); + } +} + +absl::StatusOr> CreateBenchmarkCompiler() { + CEL_ASSIGN_OR_RETURN(std::unique_ptr builder, + NewCompilerBuilder(cel::GetMinimalDescriptorPool())); + CEL_RETURN_IF_ERROR(builder->AddLibrary(StandardCompilerLibrary())); + CEL_RETURN_IF_ERROR(builder->AddLibrary(SetsCompilerLibrary())); + + TypeCheckerBuilder& checker = builder->GetCheckerBuilder(); + CEL_RETURN_IF_ERROR(checker.AddVariable( + MakeVariableDecl("x", ListType(checker.arena(), IntType())))); + CEL_RETURN_IF_ERROR(checker.AddVariable( + MakeVariableDecl("y", ListType(checker.arena(), IntType())))); + + return builder->Build(); +} + +absl::StatusOr> CreateBenchmarkRuntime( + bool enable_set_opt, google::protobuf::Arena* absl_nonnull arena) { + RuntimeOptions options; + CEL_ASSIGN_OR_RETURN( + RuntimeBuilder builder, + CreateStandardRuntimeBuilder(cel::GetMinimalDescriptorPool(), options)); + CEL_RETURN_IF_ERROR( + RegisterSetsFunctions(builder.function_registry(), options)); + CEL_RETURN_IF_ERROR( + EnableReferenceResolver(builder, ReferenceResolverEnabled::kAlways)); + CEL_RETURN_IF_ERROR(EnableConstantFolding(builder, arena)); + if (enable_set_opt) { + CEL_RETURN_IF_ERROR(EnableSetMembershipOptimization(builder)); } + return std::move(builder).Build(); } void RunBenchmark(const TestCase& test_case, benchmark::State& state) { - bool lists_overlap = test_case.result.BoolOrDie(); + bool lists_overlap = test_case.result; std::string expr = test_case.expr; - if (test_case.list_impl == ListImpl::kRhsConstant) { + if (test_case.list_impl == ListImpl::kRhsConstant || + test_case.list_impl == ListImpl::kRhsConstantOptimized) { expr = absl::StrReplaceAll( expr, {{"y", ConstantList(lists_overlap, test_case.size)}}); } - ASSERT_OK_AND_ASSIGN(ParsedExpr parsed_expr, Parse(expr)); - google::protobuf::Arena arena; + ASSERT_OK_AND_ASSIGN(std::unique_ptr compiler, + CreateBenchmarkCompiler()); + ASSERT_OK_AND_ASSIGN(ValidationResult validation_result, + compiler->Compile(expr)); + ASSERT_TRUE(validation_result.IsValid()) << validation_result.FormatError(); + ASSERT_OK_AND_ASSIGN(std::unique_ptr ast, + validation_result.ReleaseAst()); - InterpreterOptions options; - options.constant_folding = true; - options.constant_arena = &arena; - options.enable_qualified_identifier_rewrites = true; - auto builder = CreateCelExpressionBuilder(options); - ASSERT_OK(RegisterBuiltinFunctions(builder->GetRegistry(), options)); - ASSERT_OK(RegisterSetsFunctions(builder->GetRegistry()->InternalGetRegistry(), - cel::RuntimeOptions{})); + google::protobuf::Arena arena; ASSERT_OK_AND_ASSIGN( - auto cel_expr, builder->CreateExpression(&(parsed_expr.expr()), nullptr)); + std::unique_ptr runtime, + CreateBenchmarkRuntime( + test_case.list_impl == ListImpl::kRhsConstantOptimized, &arena)); + ASSERT_OK_AND_ASSIGN(std::unique_ptr program, + runtime->CreateProgram(std::move(ast))); Activation activation; - ASSERT_OK_AND_ASSIGN( - auto storage, - RegisterLists(test_case.result.BoolOrDie(), test_case.size, - test_case.list_impl == ListImpl::kWrappedModern, &arena, - activation)); + ASSERT_THAT(RegisterLists(test_case.result, test_case.size, + test_case.list_impl != ListImpl::kLegacy, &arena, + activation), + IsOk()); state.SetLabel(test_case.MakeLabel(test_case.size)); for (auto _ : state) { - ASSERT_OK_AND_ASSIGN(CelValue result, - cel_expr->Evaluate(activation, &arena)); + ASSERT_OK_AND_ASSIGN(Value result, program->Evaluate(&arena, activation)); ASSERT_TRUE(result.IsBool()); - ASSERT_EQ(result.BoolOrDie(), test_case.result.BoolOrDie()) + ASSERT_EQ(result.GetBool().NativeValue(), test_case.result) << test_case.test_name; } } -void BM_SetsIntersectsTrue(benchmark::State& state) { +void BM_SetsContainsTrue(benchmark::State& state) { ListImpl impl = FromNumber(state.range(0)); int size = state.range(1); - RunBenchmark({"sets.intersects_true", "sets.intersects(x, y)", impl, size, - CelValue::CreateBool(true)}, + RunBenchmark({"sets.contains_true", "sets.contains(x, y)", impl, size, true}, state); } +void BM_SetsContainsFalse(benchmark::State& state) { + ListImpl impl = FromNumber(state.range(0)); + int size = state.range(1); + + RunBenchmark( + {"sets.contains_false", "sets.contains(x, y)", impl, size, false}, state); +} + +void BM_SetsContainsComprehensionTrue(benchmark::State& state) { + ListImpl impl = FromNumber(state.range(0)); + int size = state.range(1); + + RunBenchmark( + {"comprehension_contains_true", "x.all(i, i in y)", impl, size, true}, + state); +} + +void BM_SetsContainsComprehensionFalse(benchmark::State& state) { + ListImpl impl = FromNumber(state.range(0)); + int size = state.range(1); + + RunBenchmark( + {"comprehension_contains_false", "x.all(i, i in y)", impl, size, false}, + state); +} + +void BM_SetsIntersectsTrue(benchmark::State& state) { + ListImpl impl = FromNumber(state.range(0)); + int size = state.range(1); + + RunBenchmark( + {"sets.intersects_true", "sets.intersects(x, y)", impl, size, true}, + state); +} + void BM_SetsIntersectsFalse(benchmark::State& state) { ListImpl impl = FromNumber(state.range(0)); int size = state.range(1); - RunBenchmark({"sets.intersects_false", "sets.intersects(x, y)", impl, size, - CelValue::CreateBool(false)}, - state); + RunBenchmark( + {"sets.intersects_false", "sets.intersects(x, y)", impl, size, false}, + state); } void BM_SetsIntersectsComprehensionTrue(benchmark::State& state) { @@ -264,7 +313,7 @@ void BM_SetsIntersectsComprehensionTrue(benchmark::State& state) { int size = state.range(1); RunBenchmark({"comprehension_intersects_true", "x.exists(i, i in y)", impl, - size, CelValue::CreateBool(true)}, + size, true}, state); } @@ -273,7 +322,7 @@ void BM_SetsIntersectsComprehensionFalse(benchmark::State& state) { int size = state.range(1); RunBenchmark({"comprehension_intersects_false", "x.exists(i, i in y)", impl, - size, CelValue::CreateBool(false)}, + size, false}, state); } @@ -281,50 +330,54 @@ void BM_SetsEquivalentTrue(benchmark::State& state) { ListImpl impl = FromNumber(state.range(0)); int size = state.range(1); - RunBenchmark({"sets.equivalent_true", "sets.equivalent(x, y)", impl, size, - CelValue::CreateBool(true)}, - state); + RunBenchmark( + {"sets.equivalent_true", "sets.equivalent(x, y)", impl, size, true}, + state); } void BM_SetsEquivalentFalse(benchmark::State& state) { ListImpl impl = FromNumber(state.range(0)); int size = state.range(1); - RunBenchmark({"sets.equivalent_false", "sets.equivalent(x, y)", impl, size, - CelValue::CreateBool(false)}, - state); + RunBenchmark( + {"sets.equivalent_false", "sets.equivalent(x, y)", impl, size, false}, + state); } void BM_SetsEquivalentComprehensionTrue(benchmark::State& state) { ListImpl impl = FromNumber(state.range(0)); int size = state.range(1); - RunBenchmark( - {"comprehension_equivalent_true", "x.all(i, i in y) && y.all(j, j in x)", - impl, size, CelValue::CreateBool(true)}, - state); + RunBenchmark({"comprehension_equivalent_true", + "x.all(i, i in y) && y.all(j, j in x)", impl, size, true}, + state); } void BM_SetsEquivalentComprehensionFalse(benchmark::State& state) { ListImpl impl = FromNumber(state.range(0)); int size = state.range(1); - RunBenchmark( - {"comprehension_equivalent_false", "x.all(i, i in y) && y.all(j, j in x)", - impl, size, CelValue::CreateBool(false)}, - state); + RunBenchmark({"comprehension_equivalent_false", + "x.all(i, i in y) && y.all(j, j in x)", impl, size, false}, + state); } template void BenchArgs(Benchmark* bench) { for (ListImpl impl : - {ListImpl::kLegacy, ListImpl::kWrappedModern, ListImpl::kRhsConstant}) { + {ListImpl::kLegacy, ListImpl::kModern, ListImpl::kRhsConstant, + ListImpl::kRhsConstantOptimized}) { for (int size : {1, 8, 32, 64, 256}) { bench->ArgPair(ToNumber(impl), size); } } } +BENCHMARK(BM_SetsContainsComprehensionTrue)->Apply(BenchArgs); +BENCHMARK(BM_SetsContainsComprehensionFalse)->Apply(BenchArgs); +BENCHMARK(BM_SetsContainsTrue)->Apply(BenchArgs); +BENCHMARK(BM_SetsContainsFalse)->Apply(BenchArgs); + BENCHMARK(BM_SetsIntersectsComprehensionTrue)->Apply(BenchArgs); BENCHMARK(BM_SetsIntersectsComprehensionFalse)->Apply(BenchArgs); BENCHMARK(BM_SetsIntersectsTrue)->Apply(BenchArgs); diff --git a/extensions/sets_functions_test.cc b/extensions/sets_functions_test.cc index dc6768f34..e2f191a9f 100644 --- a/extensions/sets_functions_test.cc +++ b/extensions/sets_functions_test.cc @@ -16,6 +16,7 @@ #include #include +#include #include "cel/expr/syntax.pb.h" #include "absl/status/status_matchers.h" @@ -23,6 +24,8 @@ #include "checker/validation_result.h" #include "common/ast_proto.h" #include "common/minimal_descriptor_pool.h" +#include "common/value.h" +#include "compiler/compiler.h" #include "compiler/compiler_factory.h" #include "eval/public/activation.h" #include "eval/public/builtin_func_registrar.h" @@ -31,8 +34,13 @@ #include "eval/public/cel_function_adapter.h" #include "eval/public/cel_options.h" #include "eval/public/cel_value.h" +#include "extensions/set_membership_optimization.h" #include "internal/testing.h" +#include "runtime/activation.h" +#include "runtime/runtime.h" +#include "runtime/runtime_builder.h" #include "runtime/runtime_options.h" +#include "runtime/standard_runtime_builder_factory.h" #include "google/protobuf/arena.h" namespace cel::extensions { @@ -93,6 +101,83 @@ TEST_P(CelSetsFunctionsTest, EndToEnd) { EXPECT_TRUE(out.BoolOrDie()) << test_info.expr << " -> " << out.DebugString(); } +TEST_P(CelSetsFunctionsTest, RuntimeEndToEnd) { + const TestInfo& test_info = GetParam(); + ASSERT_OK_AND_ASSIGN(std::unique_ptr compiler_builder, + NewCompilerBuilder(cel::GetMinimalDescriptorPool())); + + ASSERT_THAT(compiler_builder->AddLibrary(StandardCheckerLibrary()), IsOk()); + ASSERT_THAT(compiler_builder->AddLibrary(SetsCompilerLibrary()), IsOk()); + ASSERT_OK_AND_ASSIGN(std::unique_ptr compiler, + compiler_builder->Build()); + + ASSERT_OK_AND_ASSIGN(ValidationResult compiled, + compiler->Compile(test_info.expr)); + + ASSERT_TRUE(compiled.IsValid()) << compiled.FormatError(); + + RuntimeOptions options; + options.enable_heterogeneous_equality = true; + options.enable_empty_wrapper_null_unboxing = true; + options.enable_qualified_type_identifiers = true; + ASSERT_OK_AND_ASSIGN( + RuntimeBuilder runtime_builder, + CreateStandardRuntimeBuilder(cel::GetMinimalDescriptorPool(), options)); + ASSERT_THAT( + RegisterSetsFunctions(runtime_builder.function_registry(), options), + IsOk()); + ASSERT_OK_AND_ASSIGN(std::unique_ptr runtime, + std::move(runtime_builder).Build()); + + ASSERT_OK_AND_ASSIGN(std::unique_ptr program, + runtime->CreateProgram(*compiled.ReleaseAst())); + Arena arena; + cel::Activation activation; + ASSERT_OK_AND_ASSIGN(Value out, program->Evaluate(&arena, activation)); + ASSERT_TRUE(out.IsBool()) << test_info.expr << " -> " << out.DebugString(); + EXPECT_TRUE(out.GetBool().NativeValue()) + << test_info.expr << " -> " << out.DebugString(); +} + +TEST_P(CelSetsFunctionsTest, RuntimeEndToEndWithSetMembershipOptimization) { + const TestInfo& test_info = GetParam(); + ASSERT_OK_AND_ASSIGN(std::unique_ptr compiler_builder, + NewCompilerBuilder(cel::GetMinimalDescriptorPool())); + + ASSERT_THAT(compiler_builder->AddLibrary(StandardCheckerLibrary()), IsOk()); + ASSERT_THAT(compiler_builder->AddLibrary(SetsCompilerLibrary()), IsOk()); + ASSERT_OK_AND_ASSIGN(std::unique_ptr compiler, + compiler_builder->Build()); + + ASSERT_OK_AND_ASSIGN(ValidationResult compiled, + compiler->Compile(test_info.expr)); + + ASSERT_TRUE(compiled.IsValid()) << compiled.FormatError(); + + RuntimeOptions options; + options.enable_heterogeneous_equality = true; + options.enable_empty_wrapper_null_unboxing = true; + options.enable_qualified_type_identifiers = true; + ASSERT_OK_AND_ASSIGN( + RuntimeBuilder runtime_builder, + CreateStandardRuntimeBuilder(cel::GetMinimalDescriptorPool(), options)); + ASSERT_THAT( + RegisterSetsFunctions(runtime_builder.function_registry(), options), + IsOk()); + ASSERT_THAT(EnableSetMembershipOptimization(runtime_builder), IsOk()); + ASSERT_OK_AND_ASSIGN(std::unique_ptr runtime, + std::move(runtime_builder).Build()); + + ASSERT_OK_AND_ASSIGN(std::unique_ptr program, + runtime->CreateProgram(*compiled.ReleaseAst())); + Arena arena; + cel::Activation activation; + ASSERT_OK_AND_ASSIGN(Value out, program->Evaluate(&arena, activation)); + ASSERT_TRUE(out.IsBool()) << test_info.expr << " -> " << out.DebugString(); + EXPECT_TRUE(out.GetBool().NativeValue()) + << test_info.expr << " -> " << out.DebugString(); +} + INSTANTIATE_TEST_SUITE_P( CelSetsFunctionsTest, CelSetsFunctionsTest, testing::ValuesIn({ @@ -130,6 +215,7 @@ INSTANTIATE_TEST_SUITE_P( {"sets.equivalent([1], [1u, 1.0])"}, {"sets.equivalent([1], [1u, 1.0])"}, {"sets.equivalent([1, 2, 3], [3u, 2.0, 1])"}, + {"!sets.equivalent([-1], [])"}, {"!sets.equivalent([2, 1], [1])"}, {"!sets.equivalent([1], [1, 2])"}, {"!sets.equivalent([1, 2], [2u, 2, 2.0])"},