Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
40 changes: 33 additions & 7 deletions checker/internal/type_inference_context.cc
Original file line number Diff line number Diff line change
Expand Up @@ -153,6 +153,19 @@ std::optional<Type> 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) {
Expand Down Expand Up @@ -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) {
Expand Down Expand Up @@ -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;
}
Expand Down
262 changes: 262 additions & 0 deletions checker/internal/type_inference_context_test.cc
Original file line number Diff line number Diff line change
Expand Up @@ -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<TypeInferenceContext::OverloadResolution> 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<TypeInferenceContext::OverloadResolution> 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<TypeInferenceContext::OverloadResolution> 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
34 changes: 34 additions & 0 deletions checker/optional_test.cc
Original file line number Diff line number Diff line change
Expand Up @@ -335,5 +335,39 @@ INSTANTIATE_TEST_SUITE_P(
"== null",
_, "no matching overload for '_==_'"}));

class OptionalListTypePermutationsTest
: public testing::TestWithParam<std::string> {};

TEST_P(OptionalListTypePermutationsTest, ResolvesToListDyn) {
ASSERT_OK_AND_ASSIGN(
std::unique_ptr<TypeCheckerBuilder> builder,
CreateTypeCheckerBuilder(GetSharedTestingDescriptorPool()));
ASSERT_THAT(builder->AddLibrary(StandardCheckerLibrary()), IsOk());
ASSERT_THAT(builder->AddLibrary(OptionalCheckerLibrary()), IsOk());
ASSERT_OK_AND_ASSIGN(std::unique_ptr<TypeChecker> 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<TypeSpec>(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
6 changes: 3 additions & 3 deletions checker/standard_library.cc
Original file line number Diff line number Diff line change
Expand Up @@ -112,13 +112,13 @@ Type TypeDynType() {

Type TypeListType() {
static absl::NoDestructor<Type> kInstance(
TypeType(BuiltinsArena(), ListOfA()));
TypeType(BuiltinsArena(), ListType(BuiltinsArena(), DynType())));
return *kInstance;
}

Type TypeMapType() {
static absl::NoDestructor<Type> kInstance(
TypeType(BuiltinsArena(), MapOfAB()));
static absl::NoDestructor<Type> kInstance(TypeType(
BuiltinsArena(), MapType(BuiltinsArena(), DynType(), DynType())));
return *kInstance;
}

Expand Down
Loading