diff --git a/checker/internal/type_inference_context.cc b/checker/internal/type_inference_context.cc index 4f738b804..4f0cb2d5c 100644 --- a/checker/internal/type_inference_context.cc +++ b/checker/internal/type_inference_context.cc @@ -153,6 +153,19 @@ std::optional WrapperToPrimitive(const Type& t) { } } +// Tests whether the type contains any type params directly or transitively. +bool HasTypeParam(const Type& type) { + if (type.kind() == TypeKind::kTypeParam) { + return true; + } + for (const auto& param : type.GetParameters()) { + if (HasTypeParam(param)) { + return true; + } + } + return false; +} + } // namespace Type TypeInferenceContext::InstantiateTypeParams(const Type& type) { @@ -252,6 +265,26 @@ bool TypeInferenceContext::IsAssignableInternal( Type to_subs = Substitute(to, prospective_substitutions); Type from_subs = Substitute(from, prospective_substitutions); + if (from_subs.kind() == TypeKind::kType && + to_subs.kind() == TypeKind::kType) { + Type from_inner = from_subs.AsType()->GetType(); + Type to_inner = to_subs.AsType()->GetType(); + // If either type contains a type parameter (e.g., type(T) in foo(data, + // type(T)) -> T), delegate to inner type unification to bind or validate + // type parameter substitutions. Returns true if the inner types + // structurally match, unify with an unbound type param, or conform to an + // existing binding in 'prospective_substitutions'. Returns false on + // structural/kind mismatches (e.g., int vs list(T)), occurs-check cycles, + // or conflicting type param bindings. + if (HasTypeParam(from_inner) || HasTypeParam(to_inner)) { + return IsAssignableInternal(from_inner, to_inner, + prospective_substitutions); + } + // Concrete types are coassignable in CEL (e.g., type(1) == type("a"), + // type([1]) == list). + return true; + } + // Types always assignable to themselves. // Remainder is checking for assignability across different types. if (to_subs == from_subs) { @@ -313,13 +346,6 @@ bool TypeInferenceContext::IsAssignableInternal( } } - if (from_subs.kind() == TypeKind::kType && - to_subs.kind() == TypeKind::kType) { - // Types are always assignable to themselves (even if differently - // parameterized). - return true; - } - if (to_subs.kind() == TypeKind::kEnum && from_subs.kind() == TypeKind::kInt) { return true; } diff --git a/checker/internal/type_inference_context_test.cc b/checker/internal/type_inference_context_test.cc index 458d08ff1..becf0a979 100644 --- a/checker/internal/type_inference_context_test.cc +++ b/checker/internal/type_inference_context_test.cc @@ -846,5 +846,267 @@ TEST(TypeInferenceContextTest, AssignabilityContextReset) { IsTypeKind(TypeKind::kDouble)); } +TEST(TypeInferenceContextTest, + TypeTypeAssignability_ConcreteTypes_Coassignable) { + google::protobuf::Arena arena; + TypeInferenceContext context(&arena); + + Type int_type = TypeType(&arena, IntType()); + Type string_type = TypeType(&arena, StringType()); + + EXPECT_TRUE(context.IsAssignable(int_type, string_type)); + EXPECT_TRUE(context.IsAssignable(string_type, int_type)); +} + +TEST(TypeInferenceContextTest, TypeTypeAssignability_MapContainerErasure) { + google::protobuf::Arena arena; + TypeInferenceContext context(&arena); + + Type map_int_uint = TypeType(&arena, MapType(&arena, IntType(), UintType())); + Type map_dyn_dyn = TypeType(&arena, MapType(&arena, DynType(), DynType())); + + EXPECT_TRUE(context.IsAssignable(map_int_uint, map_dyn_dyn)); +} + +TEST(TypeInferenceContextTest, TypeTypeAssignability_ListContainerErasure) { + google::protobuf::Arena arena; + TypeInferenceContext context(&arena); + + Type list_int = TypeType(&arena, ListType(&arena, IntType())); + Type list_dyn = TypeType(&arena, ListType(&arena, DynType())); + + EXPECT_TRUE(context.IsAssignable(list_int, list_dyn)); +} + +TEST(TypeInferenceContextTest, + TypeTypeAssignability_TypeParamTarget_BindsConcreteType) { + google::protobuf::Arena arena; + TypeInferenceContext context(&arena); + + Type type_param_t = + context.InstantiateTypeParams(TypeType(&arena, TypeParamType("T"))); + Type from_type = TypeType(&arena, IntType()); + + EXPECT_TRUE(context.IsAssignable(from_type, type_param_t)); + + Type resolved_type = context.FinalizeType(type_param_t); + ASSERT_THAT(resolved_type, IsTypeKind(TypeKind::kType)); + EXPECT_THAT(resolved_type.AsType()->GetParameters(), + ElementsAre(IsTypeKind(TypeKind::kInt))); +} + +TEST(TypeInferenceContextTest, + TypeTypeAssignability_TypeParamSource_BindsConcreteType) { + google::protobuf::Arena arena; + TypeInferenceContext context(&arena); + + Type type_param_t = + context.InstantiateTypeParams(TypeType(&arena, TypeParamType("T"))); + Type to_type = TypeType(&arena, IntType()); + + EXPECT_TRUE(context.IsAssignable(type_param_t, to_type)); + + Type resolved_type = context.FinalizeType(type_param_t); + ASSERT_THAT(resolved_type, IsTypeKind(TypeKind::kType)); + EXPECT_THAT(resolved_type.AsType()->GetParameters(), + ElementsAre(IsTypeKind(TypeKind::kInt))); +} + +TEST(TypeInferenceContextTest, TypeTypeAssignability_NestedTypeParam_Unifies) { + google::protobuf::Arena arena; + TypeInferenceContext context(&arena); + + Type type_param_t = + context.InstantiateTypeParams(TypeType(&arena, TypeParamType("T"))); + Type type_param_r = context.InstantiateTypeParams( + TypeType(&arena, TypeType(&arena, TypeParamType("R")))); + + EXPECT_TRUE(context.IsAssignable(type_param_t, type_param_r)); +} + +TEST(TypeInferenceContextTest, + TypeTypeAssignability_DeeplyNestedTypeParam_BindsConcreteType) { + google::protobuf::Arena arena; + TypeInferenceContext context(&arena); + + Type from_type = TypeType(&arena, TypeType(&arena, IntType())); + Type to_type = context.InstantiateTypeParams( + TypeType(&arena, TypeType(&arena, TypeParamType("T")))); + + EXPECT_TRUE(context.IsAssignable(from_type, to_type)); + + Type resolved_type = context.FinalizeType(to_type); + ASSERT_THAT(resolved_type, IsTypeKind(TypeKind::kType)); + Type inner_type = resolved_type.AsType()->GetType(); + ASSERT_THAT(inner_type, IsTypeKind(TypeKind::kType)); + EXPECT_THAT(inner_type.AsType()->GetParameters(), + ElementsAre(IsTypeKind(TypeKind::kInt))); +} + +TEST(TypeInferenceContextTest, + TypeTypeAssignability_CompositeListTypeParam_BindsConcreteType) { + google::protobuf::Arena arena; + TypeInferenceContext context(&arena); + + Type from_type = TypeType(&arena, ListType(&arena, IntType())); + Type to_type = context.InstantiateTypeParams( + TypeType(&arena, ListType(&arena, TypeParamType("T")))); + + EXPECT_TRUE(context.IsAssignable(from_type, to_type)); + + Type resolved_type = context.FinalizeType(to_type); + ASSERT_THAT(resolved_type, IsTypeKind(TypeKind::kType)); + Type inner_type = resolved_type.AsType()->GetType(); + ASSERT_THAT(inner_type, IsTypeKind(TypeKind::kList)); + EXPECT_THAT(inner_type.AsList()->GetElement(), IsTypeKind(TypeKind::kInt)); +} + +TEST(TypeInferenceContextTest, + TypeTypeAssignability_CompositeMapTypeParam_BindsConcreteTypes) { + google::protobuf::Arena arena; + TypeInferenceContext context(&arena); + + Type from_type = TypeType(&arena, MapType(&arena, StringType(), IntType())); + Type to_type = context.InstantiateTypeParams(TypeType( + &arena, MapType(&arena, TypeParamType("K"), TypeParamType("V")))); + + EXPECT_TRUE(context.IsAssignable(from_type, to_type)); + + Type resolved_type = context.FinalizeType(to_type); + ASSERT_THAT(resolved_type, IsTypeKind(TypeKind::kType)); + Type inner_type = resolved_type.AsType()->GetType(); + ASSERT_THAT(inner_type, IsTypeKind(TypeKind::kMap)); + EXPECT_THAT(inner_type.AsMap()->GetKey(), IsTypeKind(TypeKind::kString)); + EXPECT_THAT(inner_type.AsMap()->GetValue(), IsTypeKind(TypeKind::kInt)); +} + +TEST(TypeInferenceContextTest, + TypeTypeAssignability_OptionalTypeParam_Unifies) { + google::protobuf::Arena arena; + TypeInferenceContext context(&arena); + + Type from_type = TypeType(&arena, OptionalType(&arena, IntType())); + Type to_type = context.InstantiateTypeParams( + TypeType(&arena, OptionalType(&arena, TypeParamType("T")))); + + EXPECT_TRUE(context.IsAssignable(from_type, to_type)); + + Type resolved_type = context.FinalizeType(to_type); + ASSERT_THAT(resolved_type, IsTypeKind(TypeKind::kType)); + Type inner_type = resolved_type.AsType()->GetType(); + ASSERT_THAT(inner_type, IsTypeKind(TypeKind::kOpaque)); + EXPECT_THAT(inner_type.AsOpaque()->GetParameters(), + ElementsAre(IsTypeKind(TypeKind::kInt))); +} + +TEST(TypeInferenceContextTest, + TypeTypeAssignability_IncompatibleTypeParams_ReturnsFalse) { + google::protobuf::Arena arena; + TypeInferenceContext context(&arena); + + Type from_type = context.InstantiateTypeParams( + TypeType(&arena, ListType(&arena, TypeParamType("T")))); + Type to_type = TypeType(&arena, IntType()); + + EXPECT_FALSE(context.IsAssignable(from_type, to_type)); +} + +TEST(TypeInferenceContextTest, + TypeTypeAssignability_ConflictingBoundTypeParam_ReturnsFalse) { + google::protobuf::Arena arena; + TypeInferenceContext context(&arena); + + Type param_t = + context.InstantiateTypeParams(TypeType(&arena, TypeParamType("T"))); + EXPECT_TRUE(context.IsAssignable(TypeType(&arena, StringType()), param_t)); + EXPECT_FALSE(context.IsAssignable(param_t, TypeType(&arena, IntType()))); +} + +TEST(TypeInferenceContextTest, + TypeTypeAssignability_OccursCheck_FailsOnSelfReference) { + google::protobuf::Arena arena; + TypeInferenceContext context(&arena); + + Type param_t = + context.InstantiateTypeParams(TypeType(&arena, TypeParamType("T"))); + Type to_type = TypeType(&arena, param_t); + + EXPECT_FALSE(context.IsAssignable(param_t, to_type)); +} + +TEST(TypeInferenceContextTest, + TypeTypeAssignability_OccursCheck_FailsOnTransitiveCycle) { + google::protobuf::Arena arena; + TypeInferenceContext context(&arena); + + Type param_t = + context.InstantiateTypeParams(TypeType(&arena, TypeParamType("T"))); + Type param_r = + context.InstantiateTypeParams(TypeType(&arena, TypeParamType("R"))); + + EXPECT_TRUE(context.IsAssignable(param_t, TypeType(&arena, param_r))); + EXPECT_FALSE(context.IsAssignable(param_r, TypeType(&arena, param_t))); +} + +TEST(TypeInferenceContextTest, + TypeTypeOverloadResolution_TypeParamInTypeType_ResolvesReturnTypeInt) { + google::protobuf::Arena arena; + TypeInferenceContext context(&arena); + + ASSERT_OK_AND_ASSIGN( + FunctionDecl decl, + MakeFunctionDecl("cast", + MakeOverloadDecl("cast_t", TypeParamType("T"), DynType(), + TypeType(&arena, TypeParamType("T"))))); + + std::optional resolution = + context.ResolveOverload(decl, {StringType(), TypeType(&arena, IntType())}, + false); + ASSERT_TRUE(resolution.has_value()); + EXPECT_THAT(context.FinalizeType(resolution->result_type), + IsTypeKind(TypeKind::kInt)); +} + +TEST(TypeInferenceContextTest, + TypeTypeOverloadResolution_TypeParamInTypeType_ResolvesReturnTypeString) { + google::protobuf::Arena arena; + TypeInferenceContext context(&arena); + + ASSERT_OK_AND_ASSIGN( + FunctionDecl decl, + MakeFunctionDecl("cast", + MakeOverloadDecl("cast_t", TypeParamType("T"), DynType(), + TypeType(&arena, TypeParamType("T"))))); + + std::optional resolution = + context.ResolveOverload(decl, {IntType(), TypeType(&arena, StringType())}, + false); + ASSERT_TRUE(resolution.has_value()); + EXPECT_THAT(context.FinalizeType(resolution->result_type), + IsTypeKind(TypeKind::kString)); +} + +TEST(TypeInferenceContextTest, + TypeTypeOverloadResolution_TypeParamInComposite_ResolvesReturnType) { + google::protobuf::Arena arena; + TypeInferenceContext context(&arena); + + ASSERT_OK_AND_ASSIGN( + FunctionDecl decl, + MakeFunctionDecl( + "first_elem_type", + MakeOverloadDecl( + "first_elem_type_overload", TypeParamType("T"), DynType(), + TypeType(&arena, ListType(&arena, TypeParamType("T")))))); + + std::optional resolution = + context.ResolveOverload( + decl, {StringType(), TypeType(&arena, ListType(&arena, IntType()))}, + false); + ASSERT_TRUE(resolution.has_value()); + EXPECT_THAT(context.FinalizeType(resolution->result_type), + IsTypeKind(TypeKind::kInt)); +} + } // namespace } // namespace cel::checker_internal diff --git a/checker/optional_test.cc b/checker/optional_test.cc index 87c14f0cd..aa8a8712c 100644 --- a/checker/optional_test.cc +++ b/checker/optional_test.cc @@ -335,5 +335,39 @@ INSTANTIATE_TEST_SUITE_P( "== null", _, "no matching overload for '_==_'"})); +class OptionalListTypePermutationsTest + : public testing::TestWithParam {}; + +TEST_P(OptionalListTypePermutationsTest, ResolvesToListDyn) { + ASSERT_OK_AND_ASSIGN( + std::unique_ptr builder, + CreateTypeCheckerBuilder(GetSharedTestingDescriptorPool())); + ASSERT_THAT(builder->AddLibrary(StandardCheckerLibrary()), IsOk()); + ASSERT_THAT(builder->AddLibrary(OptionalCheckerLibrary()), IsOk()); + ASSERT_OK_AND_ASSIGN(std::unique_ptr checker, + std::move(*builder).Build()); + + const std::string& expr = GetParam(); + ASSERT_OK_AND_ASSIGN(auto ast, MakeTestParsedAst(expr)); + ASSERT_OK_AND_ASSIGN(auto result, checker->Check(std::move(ast))); + EXPECT_THAT(result.GetIssues(), IsEmpty()); + ASSERT_OK_AND_ASSIGN(auto checked_ast, result.ReleaseAst()); + EXPECT_EQ(checked_ast->GetTypeOrDyn(checked_ast->root_expr().id()), + TypeSpec(ListTypeSpec(std::make_unique(DynTypeSpec())))); + ASSERT_EQ(checked_ast->root_expr().list_expr().elements().size(), 3); + for (const auto& elem : checked_ast->root_expr().list_expr().elements()) { + EXPECT_TRUE(checked_ast->GetTypeOrDyn(elem.expr().id()).has_type()); + } +} + +INSTANTIATE_TEST_SUITE_P( + OptionalTests, OptionalListTypePermutationsTest, + ::testing::Values("[type([]), int, type(optional.none())]", + "[type([]), type(optional.none()), int]", + "[int, type([]), type(optional.none())]", + "[int, type(optional.none()), type([])]", + "[type(optional.none()), type([]), int]", + "[type(optional.none()), int, type([])]")); + } // namespace } // namespace cel diff --git a/checker/standard_library.cc b/checker/standard_library.cc index 744a171ef..0162db003 100644 --- a/checker/standard_library.cc +++ b/checker/standard_library.cc @@ -112,13 +112,13 @@ Type TypeDynType() { Type TypeListType() { static absl::NoDestructor kInstance( - TypeType(BuiltinsArena(), ListOfA())); + TypeType(BuiltinsArena(), ListType(BuiltinsArena(), DynType()))); return *kInstance; } Type TypeMapType() { - static absl::NoDestructor kInstance( - TypeType(BuiltinsArena(), MapOfAB())); + static absl::NoDestructor kInstance(TypeType( + BuiltinsArena(), MapType(BuiltinsArena(), DynType(), DynType()))); return *kInstance; }