luau/tests/TypeInfer.tryUnify.test.cpp

499 lines
15 KiB
C++
Raw Normal View History

// This file is part of the Luau programming language and is licensed under MIT License; see LICENSE.txt for details
2023-02-24 18:24:22 +00:00
#include "Luau/Common.h"
#include "Luau/Scope.h"
2023-02-24 18:24:22 +00:00
#include "Luau/Symbol.h"
#include "Luau/TypeInfer.h"
2023-01-03 17:33:19 +00:00
#include "Luau/Type.h"
#include "Fixture.h"
#include "doctest.h"
using namespace Luau;
2023-02-24 18:24:22 +00:00
LUAU_FASTFLAG(DebugLuauDeferredConstraintResolution)
struct TryUnifyFixture : Fixture
{
TypeArena arena;
ScopePtr globalScope{new Scope{arena.addTypePack({TypeId{}})}};
InternalErrorReporter iceHandler;
UnifierSharedState unifierState{&iceHandler};
2023-01-03 17:33:19 +00:00
Normalizer normalizer{&arena, builtinTypes, NotNull{&unifierState}};
2023-05-25 21:46:51 +01:00
Unifier state{NotNull{&normalizer}, NotNull{globalScope.get()}, Location{}, Variance::Covariant};
};
TEST_SUITE_BEGIN("TryUnifyTests");
TEST_CASE_FIXTURE(TryUnifyFixture, "primitives_unify")
{
2023-03-17 14:59:30 +00:00
ScopedFastFlag sff[] = {
{"LuauTransitiveSubtyping", true},
};
2023-01-03 17:33:19 +00:00
Type numberOne{TypeVariant{PrimitiveType{PrimitiveType::Number}}};
Type numberTwo = numberOne;
2022-01-06 22:10:07 +00:00
state.tryUnify(&numberTwo, &numberOne);
2023-03-17 14:59:30 +00:00
CHECK(!state.failure);
CHECK(state.errors.empty());
}
TEST_CASE_FIXTURE(TryUnifyFixture, "compatible_functions_are_unified")
{
2023-03-17 14:59:30 +00:00
ScopedFastFlag sff[] = {
{"LuauTransitiveSubtyping", true},
};
2023-01-03 17:33:19 +00:00
Type functionOne{
2023-03-10 19:20:04 +00:00
TypeVariant{FunctionType(arena.addTypePack({arena.freshType(globalScope->level)}), arena.addTypePack({builtinTypes->numberType}))}};
2023-01-03 17:33:19 +00:00
Type functionTwo{TypeVariant{
FunctionType(arena.addTypePack({arena.freshType(globalScope->level)}), arena.addTypePack({arena.freshType(globalScope->level)}))}};
2022-01-06 22:10:07 +00:00
state.tryUnify(&functionTwo, &functionOne);
2023-03-17 14:59:30 +00:00
CHECK(!state.failure);
CHECK(state.errors.empty());
2022-03-11 16:31:18 +00:00
state.log.commit();
2022-01-06 22:10:07 +00:00
CHECK_EQ(functionOne, functionTwo);
}
TEST_CASE_FIXTURE(TryUnifyFixture, "incompatible_functions_are_preserved")
{
2023-03-17 14:59:30 +00:00
ScopedFastFlag sff[] = {
{"LuauTransitiveSubtyping", true},
};
TypePackVar argPackOne{TypePack{{arena.freshType(globalScope->level)}, std::nullopt}};
2023-01-03 17:33:19 +00:00
Type functionOne{
2023-03-10 19:20:04 +00:00
TypeVariant{FunctionType(arena.addTypePack({arena.freshType(globalScope->level)}), arena.addTypePack({builtinTypes->numberType}))}};
2023-01-03 17:33:19 +00:00
Type functionOneSaved = functionOne;
TypePackVar argPackTwo{TypePack{{arena.freshType(globalScope->level)}, std::nullopt}};
2023-01-03 17:33:19 +00:00
Type functionTwo{
2023-03-10 19:20:04 +00:00
TypeVariant{FunctionType(arena.addTypePack({arena.freshType(globalScope->level)}), arena.addTypePack({builtinTypes->stringType}))}};
2023-01-03 17:33:19 +00:00
Type functionTwoSaved = functionTwo;
2022-01-06 22:10:07 +00:00
state.tryUnify(&functionTwo, &functionOne);
2023-03-17 14:59:30 +00:00
CHECK(state.failure);
CHECK(!state.errors.empty());
CHECK_EQ(functionOne, functionOneSaved);
CHECK_EQ(functionTwo, functionTwoSaved);
}
TEST_CASE_FIXTURE(TryUnifyFixture, "tables_can_be_unified")
{
2023-03-17 14:59:30 +00:00
ScopedFastFlag sff[] = {
{"LuauTransitiveSubtyping", true},
};
2023-01-03 17:33:19 +00:00
Type tableOne{TypeVariant{
TableType{{{"foo", {arena.freshType(globalScope->level)}}}, std::nullopt, globalScope->level, TableState::Unsealed},
}};
2023-01-03 17:33:19 +00:00
Type tableTwo{TypeVariant{
TableType{{{"foo", {arena.freshType(globalScope->level)}}}, std::nullopt, globalScope->level, TableState::Unsealed},
}};
2023-04-28 12:55:55 +01:00
CHECK_NE(*getMutable<TableType>(&tableOne)->props["foo"].type(), *getMutable<TableType>(&tableTwo)->props["foo"].type());
2022-01-06 22:10:07 +00:00
state.tryUnify(&tableTwo, &tableOne);
2023-03-17 14:59:30 +00:00
CHECK(!state.failure);
CHECK(state.errors.empty());
2022-03-11 16:31:18 +00:00
state.log.commit();
2022-01-06 22:10:07 +00:00
2023-04-28 12:55:55 +01:00
CHECK_EQ(*getMutable<TableType>(&tableOne)->props["foo"].type(), *getMutable<TableType>(&tableTwo)->props["foo"].type());
}
TEST_CASE_FIXTURE(TryUnifyFixture, "incompatible_tables_are_preserved")
{
2023-03-17 14:59:30 +00:00
ScopedFastFlag sff[] = {
{"LuauTransitiveSubtyping", true},
};
2023-01-03 17:33:19 +00:00
Type tableOne{TypeVariant{
2023-03-10 19:20:04 +00:00
TableType{{{"foo", {arena.freshType(globalScope->level)}}, {"bar", {builtinTypes->numberType}}}, std::nullopt, globalScope->level,
TableState::Unsealed},
}};
2023-01-03 17:33:19 +00:00
Type tableTwo{TypeVariant{
2023-03-10 19:20:04 +00:00
TableType{{{"foo", {arena.freshType(globalScope->level)}}, {"bar", {builtinTypes->stringType}}}, std::nullopt, globalScope->level,
TableState::Unsealed},
}};
2023-04-28 12:55:55 +01:00
CHECK_NE(*getMutable<TableType>(&tableOne)->props["foo"].type(), *getMutable<TableType>(&tableTwo)->props["foo"].type());
2022-01-06 22:10:07 +00:00
state.tryUnify(&tableTwo, &tableOne);
2023-03-17 14:59:30 +00:00
CHECK(state.failure);
CHECK_EQ(1, state.errors.size());
2023-04-28 12:55:55 +01:00
CHECK_NE(*getMutable<TableType>(&tableOne)->props["foo"].type(), *getMutable<TableType>(&tableTwo)->props["foo"].type());
}
2022-12-02 10:46:05 +00:00
TEST_CASE_FIXTURE(TryUnifyFixture, "uninhabited_intersection_sub_never")
{
CheckResult result = check(R"(
function f(arg : string & number) : never
return arg
end
)");
LUAU_REQUIRE_NO_ERRORS(result);
}
TEST_CASE_FIXTURE(TryUnifyFixture, "uninhabited_intersection_sub_anything")
{
CheckResult result = check(R"(
function f(arg : string & number) : boolean
return arg
end
)");
LUAU_REQUIRE_NO_ERRORS(result);
}
TEST_CASE_FIXTURE(TryUnifyFixture, "uninhabited_table_sub_never")
{
CheckResult result = check(R"(
function f(arg : { prop : string & number }) : never
return arg
end
)");
LUAU_REQUIRE_NO_ERRORS(result);
}
TEST_CASE_FIXTURE(TryUnifyFixture, "uninhabited_table_sub_anything")
{
CheckResult result = check(R"(
function f(arg : { prop : string & number }) : boolean
return arg
end
)");
LUAU_REQUIRE_NO_ERRORS(result);
}
TEST_CASE_FIXTURE(TryUnifyFixture, "members_of_failed_typepack_unification_are_unified_with_errorType")
{
2023-06-02 19:17:31 +01:00
ScopedFastFlag sff{"LuauAlwaysCommitInferencesOfFunctionCalls", true};
CheckResult result = check(R"(
function f(arg: number) end
local a
local b
f(a, b)
)");
LUAU_REQUIRE_ERROR_COUNT(1, result);
2023-06-02 19:17:31 +01:00
CHECK_EQ("number", toString(requireType("a")));
2022-11-04 17:02:37 +00:00
CHECK_EQ("*error-type*", toString(requireType("b")));
2021-11-18 22:21:07 +00:00
}
TEST_CASE_FIXTURE(TryUnifyFixture, "result_of_failed_typepack_unification_is_constrained")
{
2023-06-02 19:17:31 +01:00
ScopedFastFlag sff{"LuauAlwaysCommitInferencesOfFunctionCalls", true};
2021-11-18 22:21:07 +00:00
CheckResult result = check(R"(
function f(arg: number) return arg end
local a
local b
local c = f(a, b)
)");
LUAU_REQUIRE_ERROR_COUNT(1, result);
2023-06-02 19:17:31 +01:00
CHECK_EQ("number", toString(requireType("a")));
2022-11-04 17:02:37 +00:00
CHECK_EQ("*error-type*", toString(requireType("b")));
2021-11-18 22:21:07 +00:00
CHECK_EQ("number", toString(requireType("c")));
}
TEST_CASE_FIXTURE(TryUnifyFixture, "typepack_unification_should_trim_free_tails")
{
CheckResult result = check(R"(
--!strict
local function f(v: number)
if v % 2 == 0 then
return true
end
end
return function()
return (f(1))
end
)");
LUAU_REQUIRE_ERROR_COUNT(1, result);
2022-03-04 16:19:20 +00:00
CHECK_EQ("(number) -> boolean", toString(requireType("f")));
}
TEST_CASE_FIXTURE(TryUnifyFixture, "variadic_type_pack_unification")
{
2023-03-10 19:20:04 +00:00
TypePackVar testPack{TypePack{{builtinTypes->numberType, builtinTypes->stringType}, std::nullopt}};
TypePackVar variadicPack{VariadicTypePack{builtinTypes->numberType}};
2022-01-06 22:10:07 +00:00
state.tryUnify(&testPack, &variadicPack);
2023-03-17 14:59:30 +00:00
CHECK(state.failure);
CHECK(!state.errors.empty());
}
TEST_CASE_FIXTURE(TryUnifyFixture, "variadic_tails_respect_progress")
{
2023-03-10 19:20:04 +00:00
TypePackVar variadicPack{VariadicTypePack{builtinTypes->booleanType}};
TypePackVar a{TypePack{{builtinTypes->numberType, builtinTypes->stringType, builtinTypes->booleanType, builtinTypes->booleanType}}};
TypePackVar b{TypePack{{builtinTypes->numberType, builtinTypes->stringType}, &variadicPack}};
2022-01-06 22:10:07 +00:00
state.tryUnify(&b, &a);
2023-03-17 14:59:30 +00:00
CHECK(!state.failure);
CHECK(state.errors.empty());
}
TEST_CASE_FIXTURE(TryUnifyFixture, "variadics_should_use_reversed_properly")
{
CheckResult result = check(R"(
--!strict
local function f<T>(...: T): ...T
return ...
end
local x: string = f(1)
)");
LUAU_REQUIRE_ERROR_COUNT(1, result);
TypeMismatch* tm = get<TypeMismatch>(result.errors[0]);
REQUIRE(tm);
CHECK_EQ(toString(tm->givenType), "number");
CHECK_EQ(toString(tm->wantedType), "string");
}
2022-05-13 20:16:50 +01:00
TEST_CASE_FIXTURE(BuiltinsFixture, "cli_41095_concat_log_in_sealed_table_unification")
{
CheckResult result = check(R"(
--!strict
table.insert()
)");
LUAU_REQUIRE_ERROR_COUNT(2, result);
CHECK_EQ(toString(result.errors[0]), "No overload for function accepts 0 arguments.");
2023-02-24 18:24:22 +00:00
if (FFlag::DebugLuauDeferredConstraintResolution)
CHECK_EQ(toString(result.errors[1]), "Available overloads: <V>({V}, V) -> (); and <V>({V}, number, V) -> ()");
else
CHECK_EQ(toString(result.errors[1]), "Available overloads: ({a}, a) -> (); and ({a}, number, a) -> ()");
}
2022-01-06 22:10:07 +00:00
TEST_CASE_FIXTURE(TryUnifyFixture, "free_tail_is_grown_properly")
{
2023-03-10 19:20:04 +00:00
TypePackId threeNumbers =
arena.addTypePack(TypePack{{builtinTypes->numberType, builtinTypes->numberType, builtinTypes->numberType}, std::nullopt});
TypePackId numberAndFreeTail = arena.addTypePack(TypePack{{builtinTypes->numberType}, arena.addTypePack(TypePackVar{FreeTypePack{TypeLevel{}}})});
2022-01-06 22:10:07 +00:00
2023-03-17 14:59:30 +00:00
CHECK(state.canUnify(numberAndFreeTail, threeNumbers).empty());
2022-01-06 22:10:07 +00:00
}
2022-02-03 23:09:37 +00:00
TEST_CASE_FIXTURE(TryUnifyFixture, "recursive_metatable_getmatchtag")
{
2023-01-03 17:33:19 +00:00
Type redirect{FreeType{TypeLevel{}}};
Type table{TableType{}};
Type metatable{MetatableType{&redirect, &table}};
redirect = BoundType{&metatable}; // Now we have a metatable that is recursive on the table type
2023-03-10 19:20:04 +00:00
Type variant{UnionType{{&metatable, builtinTypes->numberType}}};
2022-02-03 23:09:37 +00:00
state.tryUnify(&metatable, &variant);
}
2022-02-11 18:43:14 +00:00
TEST_CASE_FIXTURE(TryUnifyFixture, "cli_50320_follow_in_any_unification")
{
TypePackVar free{FreeTypePack{TypeLevel{}}};
TypePackVar target{TypePack{}};
2023-01-03 17:33:19 +00:00
Type func{FunctionType{&free, &free}};
2022-02-11 18:43:14 +00:00
state.tryUnify(&free, &target);
// Shouldn't assert or error.
2023-03-10 19:20:04 +00:00
state.tryUnify(&func, builtinTypes->anyType);
2022-02-11 18:43:14 +00:00
}
2022-04-07 21:53:47 +01:00
TEST_CASE_FIXTURE(TryUnifyFixture, "txnlog_preserves_type_owner")
{
2023-01-03 17:33:19 +00:00
TypeId a = arena.addType(Type{FreeType{TypeLevel{}}});
2023-03-10 19:20:04 +00:00
TypeId b = builtinTypes->numberType;
2022-04-07 21:53:47 +01:00
state.tryUnify(a, b);
state.log.commit();
CHECK_EQ(a->owningArena, &arena);
}
TEST_CASE_FIXTURE(TryUnifyFixture, "txnlog_preserves_pack_owner")
{
TypePackId a = arena.addTypePack(TypePackVar{FreeTypePack{TypeLevel{}}});
2023-03-10 19:20:04 +00:00
TypePackId b = builtinTypes->anyTypePack;
2022-04-07 21:53:47 +01:00
state.tryUnify(a, b);
state.log.commit();
CHECK_EQ(a->owningArena, &arena);
}
2022-09-29 23:11:54 +01:00
TEST_CASE_FIXTURE(TryUnifyFixture, "metatables_unify_against_shape_of_free_table")
{
2023-03-17 14:59:30 +00:00
ScopedFastFlag sff[] = {
{"LuauTransitiveSubtyping", true},
{"DebugLuauDeferredConstraintResolution", true},
};
2022-09-29 23:11:54 +01:00
2023-01-03 17:33:19 +00:00
TableType::Props freeProps{
2023-03-10 19:20:04 +00:00
{"foo", {builtinTypes->numberType}},
2022-09-29 23:11:54 +01:00
};
2023-01-03 17:33:19 +00:00
TypeId free = arena.addType(TableType{freeProps, std::nullopt, TypeLevel{}, TableState::Free});
2022-09-29 23:11:54 +01:00
2023-01-03 17:33:19 +00:00
TableType::Props indexProps{
2023-03-10 19:20:04 +00:00
{"foo", {builtinTypes->stringType}},
2022-09-29 23:11:54 +01:00
};
2023-01-03 17:33:19 +00:00
TypeId index = arena.addType(TableType{indexProps, std::nullopt, TypeLevel{}, TableState::Sealed});
2022-09-29 23:11:54 +01:00
2023-01-03 17:33:19 +00:00
TableType::Props mtProps{
2022-09-29 23:11:54 +01:00
{"__index", {index}},
};
2023-01-03 17:33:19 +00:00
TypeId mt = arena.addType(TableType{mtProps, std::nullopt, TypeLevel{}, TableState::Sealed});
2022-09-29 23:11:54 +01:00
2023-01-03 17:33:19 +00:00
TypeId target = arena.addType(TableType{TableState::Unsealed, TypeLevel{}});
TypeId metatable = arena.addType(MetatableType{target, mt});
2022-09-29 23:11:54 +01:00
state.tryUnify(metatable, free);
state.log.commit();
REQUIRE_EQ(state.errors.size(), 1);
std::string expected = "Type '{ @metatable {| __index: {| foo: string |} |}, { } }' could not be converted into '{- foo: number -}'\n"
2022-10-13 23:59:53 +01:00
"caused by:\n"
" Type 'number' could not be converted into 'string'";
2022-09-29 23:11:54 +01:00
CHECK_EQ(toString(state.errors[0]), expected);
}
2022-12-02 10:46:05 +00:00
TEST_CASE_FIXTURE(TryUnifyFixture, "fuzz_tail_unification_issue")
{
2023-03-10 19:20:04 +00:00
TypePackVar variadicAny{VariadicTypePack{builtinTypes->anyType}};
TypePackVar packTmp{TypePack{{builtinTypes->anyType}, &variadicAny}};
TypePackVar packSub{TypePack{{builtinTypes->anyType, builtinTypes->anyType}, &packTmp}};
2022-12-02 10:46:05 +00:00
2023-01-03 17:33:19 +00:00
Type freeTy{FreeType{TypeLevel{}}};
2022-12-02 10:46:05 +00:00
TypePackVar freeTp{FreeTypePack{TypeLevel{}}};
TypePackVar packSuper{TypePack{{&freeTy}, &freeTp}};
state.tryUnify(&packSub, &packSuper);
}
2023-01-20 12:02:39 +00:00
TEST_CASE_FIXTURE(BuiltinsFixture, "fuzz_unify_any_should_check_log")
{
CheckResult result = check(R"(
repeat
_._,_ = nil
until _
local l0:(any)&(typeof(_)),l0:(any)|(any) = _,_
)");
LUAU_REQUIRE_ERRORS(result);
}
2023-05-12 13:15:01 +01:00
static TypeId createTheType(TypeArena& arena, NotNull<BuiltinTypes> builtinTypes, Scope* scope, TypeId freeTy)
{
/*
({|
render: (
(('a) -> ()) | {| current: 'a |}
) -> nil
|}) -> ()
*/
TypePackId emptyPack = arena.addTypePack({});
return arena.addType(FunctionType{
arena.addTypePack({arena.addType(TableType{
TableType::Props{{{"render",
Property(arena.addType(FunctionType{
arena.addTypePack({arena.addType(UnionType{{arena.addType(FunctionType{arena.addTypePack({freeTy}), emptyPack}),
arena.addType(TableType{TableType::Props{{"current", {freeTy}}}, std::nullopt, TypeLevel{}, scope, TableState::Sealed})}})}),
arena.addTypePack({builtinTypes->nilType})}))}}},
std::nullopt, TypeLevel{}, scope, TableState::Sealed})}),
emptyPack});
};
// See CLI-71190
TEST_CASE_FIXTURE(TryUnifyFixture, "unifying_two_unions_under_dcr_does_not_create_a_BoundType_cycle")
{
const std::shared_ptr<Scope> scope = globalScope;
const std::shared_ptr<Scope> nestedScope = std::make_shared<Scope>(scope);
const TypeId outerType = arena.freshType(scope.get());
const TypeId outerType2 = arena.freshType(scope.get());
const TypeId innerType = arena.freshType(nestedScope.get());
2023-06-02 19:17:31 +01:00
ScopedFastFlag sffs[]{
{"DebugLuauDeferredConstraintResolution", true},
{"LuauAlwaysCommitInferencesOfFunctionCalls", true},
};
2023-05-12 13:15:01 +01:00
state.enableScopeTests();
SUBCASE("equal_scopes")
{
TypeId one = createTheType(arena, builtinTypes, scope.get(), outerType);
TypeId two = createTheType(arena, builtinTypes, scope.get(), outerType2);
state.tryUnify(one, two);
state.log.commit();
ToStringOptions opts;
CHECK(follow(outerType) == follow(outerType2));
}
SUBCASE("outer_scope_is_subtype")
{
TypeId one = createTheType(arena, builtinTypes, scope.get(), outerType);
TypeId two = createTheType(arena, builtinTypes, scope.get(), innerType);
state.tryUnify(one, two);
state.log.commit();
ToStringOptions opts;
CHECK(follow(outerType) == follow(innerType));
// The scope of outerType exceeds that of innerType. The latter should be bound to the former.
const BoundType* bt = get_if<BoundType>(&innerType->ty);
REQUIRE(bt);
CHECK(bt->boundTo == outerType);
}
SUBCASE("outer_scope_is_supertype")
{
TypeId one = createTheType(arena, builtinTypes, scope.get(), innerType);
TypeId two = createTheType(arena, builtinTypes, scope.get(), outerType);
state.tryUnify(one, two);
state.log.commit();
ToStringOptions opts;
CHECK(follow(outerType) == follow(innerType));
// The scope of outerType exceeds that of innerType. The latter should be bound to the former.
const BoundType* bt = get_if<BoundType>(&innerType->ty);
REQUIRE(bt);
CHECK(bt->boundTo == outerType);
}
}
TEST_SUITE_END();