From f9658fc1d24025a25f0eae0067723a9c1a762cc1 Mon Sep 17 00:00:00 2001 From: Alexandre Macabies Date: Tue, 24 Feb 2026 08:48:35 -0800 Subject: [PATCH] Introduce list().first() and list().last(). PiperOrigin-RevId: 874634033 --- checker/optional.cc | 18 +++++++++++++++++ checker/optional_test.cc | 4 ++++ runtime/optional_types.cc | 37 ++++++++++++++++++++++++++++++++++ runtime/optional_types_test.cc | 4 ++++ 4 files changed, 63 insertions(+) diff --git a/checker/optional.cc b/checker/optional.cc index 4e29b653c..cd23127e7 100644 --- a/checker/optional.cc +++ b/checker/optional.cc @@ -83,6 +83,8 @@ class OptionalNames { static constexpr char kOptionalOrValue[] = "orValue"; static constexpr char kOptionalSelect[] = "_?._"; static constexpr char kOptionalIndex[] = "_[?_]"; + static constexpr char kOptionalFirst[] = "first"; + static constexpr char kOptionalLast[] = "last"; }; class OptionalOverloads { @@ -107,6 +109,8 @@ class OptionalOverloads { "map_optindex_optional_value"; static constexpr char kOptionalMapOptionalIndexValue[] = "optional_map_optindex_optional_value"; + static constexpr char kListFirst[] = "list_first"; + static constexpr char kListLast[] = "list_last"; // Syntactic sugar for chained indexing. static constexpr char kOptionalListIndexInt[] = "optional_list_index_int"; static constexpr char kOptionalMapIndexValue[] = "optional_map_index_value"; @@ -181,6 +185,18 @@ absl::Status RegisterOptionalDecls(TypeCheckerBuilder& builder) { OptionalOfV(), OptionalMapOfKV(), TypeParamType("K")))); + CEL_ASSIGN_OR_RETURN( + auto first, + MakeFunctionDecl(OptionalNames::kOptionalFirst, + MakeMemberOverloadDecl(OptionalOverloads::kListFirst, + OptionalOfV(), ListOfV()))); + + CEL_ASSIGN_OR_RETURN( + auto last, + MakeFunctionDecl(OptionalNames::kOptionalLast, + MakeMemberOverloadDecl(OptionalOverloads::kListLast, + OptionalOfV(), ListOfV()))); + CEL_ASSIGN_OR_RETURN( auto index, MakeFunctionDecl( @@ -203,6 +219,8 @@ absl::Status RegisterOptionalDecls(TypeCheckerBuilder& builder) { CEL_RETURN_IF_ERROR(builder.AddFunction(std::move(or_value))); CEL_RETURN_IF_ERROR(builder.AddFunction(std::move(opt_index))); CEL_RETURN_IF_ERROR(builder.AddFunction(std::move(select))); + CEL_RETURN_IF_ERROR(builder.AddFunction(std::move(first))); + CEL_RETURN_IF_ERROR(builder.AddFunction(std::move(last))); CEL_RETURN_IF_ERROR(builder.MergeFunction(std::move(index))); return absl::OkStatus(); diff --git a/checker/optional_test.cc b/checker/optional_test.cc index 8285e51df..28ae9a889 100644 --- a/checker/optional_test.cc +++ b/checker/optional_test.cc @@ -259,6 +259,10 @@ INSTANTIATE_TEST_SUITE_P( IsOptionalType(TypeSpec(PrimitiveType::kInt64))}, TestCase{"{0: {0: 1}}[?1]['']", _, "no matching overload for '_[_]'"}, TestCase{"{0: {0: 1}}[?1][?'']", _, "no matching overload for '_[?_]'"}, + TestCase{"[1, 2, 3].first()", + IsOptionalType(TypeSpec(PrimitiveType::kInt64))}, + TestCase{"[1, 2, 3].last()", + IsOptionalType(TypeSpec(PrimitiveType::kInt64))}, TestCase{"optional.of('abc').optMap(x, x + 'def')", IsOptionalType(TypeSpec(PrimitiveType::kString))}, TestCase{"optional.of('abc').optFlatMap(x, optional.of(x + 'def'))", diff --git a/runtime/optional_types.cc b/runtime/optional_types.cc index 0ba6b66be..6678a05ed 100644 --- a/runtime/optional_types.cc +++ b/runtime/optional_types.cc @@ -229,6 +229,33 @@ absl::StatusOr OptionalOptIndexOptionalValue( return ErrorValue{runtime_internal::CreateNoMatchingOverloadError("_[?_]")}; } +absl::StatusOr ListFirst(const cel::ListValue& list, + const google::protobuf::DescriptorPool* descriptor_pool, + google::protobuf::MessageFactory* message_factory, + google::protobuf::Arena* arena) { + CEL_ASSIGN_OR_RETURN(size_t size, list.Size()); + if (size == 0) { + return Value(OptionalValue::None()); + } + CEL_ASSIGN_OR_RETURN(Value value, + list.Get(0, descriptor_pool, message_factory, arena)); + return Value(OptionalValue::Of(std::move(value), arena)); +} + +absl::StatusOr ListLast(const cel::ListValue& list, + const google::protobuf::DescriptorPool* descriptor_pool, + google::protobuf::MessageFactory* message_factory, + google::protobuf::Arena* arena) { + CEL_ASSIGN_OR_RETURN(size_t size, list.Size()); + if (size == 0) { + return Value(OptionalValue::None()); + } + CEL_ASSIGN_OR_RETURN(Value value, + list.Get(static_cast(size) - 1, descriptor_pool, + message_factory, arena)); + return Value(OptionalValue::Of(std::move(value), arena)); +} + absl::StatusOr ListUnwrapOpt( const ListValue& list, const google::protobuf::DescriptorPool* absl_nonnull descriptor_pool, @@ -332,6 +359,16 @@ absl::Status RegisterOptionalTypeFunctions(FunctionRegistry& registry, "unwrapOpt", true), UnaryFunctionAdapter, ListValue>::WrapFunction( &ListUnwrapOpt))); + CEL_RETURN_IF_ERROR(registry.Register( + UnaryFunctionAdapter, ListValue>::CreateDescriptor( + "first", true), + UnaryFunctionAdapter, ListValue>::WrapFunction( + &ListFirst))); + CEL_RETURN_IF_ERROR(registry.Register( + UnaryFunctionAdapter, ListValue>::CreateDescriptor( + "last", true), + UnaryFunctionAdapter, ListValue>::WrapFunction( + &ListLast))); return absl::OkStatus(); } diff --git a/runtime/optional_types_test.cc b/runtime/optional_types_test.cc index 07029732f..455e51988 100644 --- a/runtime/optional_types_test.cc +++ b/runtime/optional_types_test.cc @@ -296,6 +296,10 @@ INSTANTIATE_TEST_SUITE_P( {"list_unwrapOpt_no_none", "[optional.of(42), optional.of(\"a\")].unwrapOpt() == [42, \"a\"]", BoolValueIs(true)}, + {"list_first", "[1, 2, 3].first()", OptionalValueIs(IntValueIs(1))}, + {"list_first_empty", "[].first()", OptionalValueIsEmpty()}, + {"list_last", "[1, 2, 3].last()", OptionalValueIs(IntValueIs(3))}, + {"list_last_empty", "[].last()", OptionalValueIsEmpty()}, }), /*enable_short_circuiting*/ testing::Bool()));