mirror of
https://github.com/luau-lang/luau.git
synced 2025-08-26 11:27:08 +01:00
## General - Introduce `Frontend::parseModules` for parsing a group of modules at once. - Support chained function types in the CST. ## New Type Solver - Enable write-only table properties (described in [this RFC](https://rfcs.luau.org/property-writeonly.html)). - Disable singleton inference for large tables to improve performance. - Fix a bug that occurs when we try to expand a type alias to itself. - Catch cancelation during the type-checking phase in addition to during constraint solving. - Fix stringification of the empty type pack: `()`. - Improve errors for calls being rejected on the primitive `function` type. - Rework generalization: We now generalize types as soon as the last constraint relating to them is finished. We think this will reduce the number of cases where type inference fails to complete and reduce the number of instances where `*blocked*` types appear in the inference result. ## VM/Runtime - Dynamically disable native execution for functions that incur a slowdown (relative to bytecode execution). - Improve names for `thread`/`closure`/`proto` in the Luau heap dump. --- Co-authored-by: Andy Friesen <afriesen@roblox.com> Co-authored-by: Ariel Weiss <aaronweiss@roblox.com> Co-authored-by: Aviral Goel <agoel@roblox.com> Co-authored-by: Hunter Goldstein <hgoldstein@roblox.com> Co-authored-by: Talha Pathan <tpathan@roblox.com> Co-authored-by: Varun Saini <vsaini@roblox.com> Co-authored-by: Vighnesh Vijay <vvijay@roblox.com> Co-authored-by: Vyacheslav Egorov <vegorov@roblox.com> --------- Co-authored-by: Hunter Goldstein <hgoldstein@roblox.com> Co-authored-by: Alexander Youngblood <ayoungblood@roblox.com> Co-authored-by: Menarul Alam <malam@roblox.com> Co-authored-by: Aviral Goel <agoel@roblox.com> Co-authored-by: Vighnesh <vvijay@roblox.com> Co-authored-by: Vyacheslav Egorov <vegorov@roblox.com> Co-authored-by: Ariel Weiss <aaronweiss@roblox.com> Co-authored-by: Andy Friesen <afriesen@roblox.com>
379 lines
12 KiB
C++
379 lines
12 KiB
C++
// This file is part of the Luau programming language and is licensed under MIT License; see LICENSE.txt for details
|
|
|
|
#include "Luau/Generalization.h"
|
|
#include "Luau/Scope.h"
|
|
#include "Luau/ToString.h"
|
|
#include "Luau/Type.h"
|
|
#include "Luau/TypeArena.h"
|
|
#include "Luau/Error.h"
|
|
|
|
#include "Fixture.h"
|
|
#include "ScopedFlags.h"
|
|
|
|
#include "doctest.h"
|
|
|
|
using namespace Luau;
|
|
|
|
LUAU_FASTFLAG(LuauSolverV2)
|
|
LUAU_FASTFLAG(LuauEagerGeneralization)
|
|
LUAU_FASTFLAG(DebugLuauForbidInternalTypes)
|
|
LUAU_FASTFLAG(LuauTrackInferredFunctionTypeFromCall)
|
|
|
|
TEST_SUITE_BEGIN("Generalization");
|
|
|
|
struct GeneralizationFixture
|
|
{
|
|
TypeArena arena;
|
|
BuiltinTypes builtinTypes;
|
|
ScopePtr globalScope = std::make_shared<Scope>(builtinTypes.anyTypePack);
|
|
ScopePtr scope = std::make_shared<Scope>(globalScope);
|
|
ToStringOptions opts;
|
|
|
|
DenseHashSet<TypeId> generalizedTypes_{nullptr};
|
|
NotNull<DenseHashSet<TypeId>> generalizedTypes{&generalizedTypes_};
|
|
|
|
ScopedFastFlag sff{FFlag::LuauSolverV2, true};
|
|
|
|
std::pair<TypeId, FreeType*> freshType()
|
|
{
|
|
FreeType ft{scope.get(), builtinTypes.neverType, builtinTypes.unknownType};
|
|
|
|
TypeId ty = arena.addType(ft);
|
|
FreeType* ftv = getMutable<FreeType>(ty);
|
|
REQUIRE(ftv != nullptr);
|
|
|
|
return {ty, ftv};
|
|
}
|
|
|
|
std::string toString(TypeId ty)
|
|
{
|
|
return ::Luau::toString(ty, opts);
|
|
}
|
|
|
|
std::string toString(TypePackId ty)
|
|
{
|
|
return ::Luau::toString(ty, opts);
|
|
}
|
|
|
|
std::optional<TypeId> generalize(TypeId ty)
|
|
{
|
|
return ::Luau::generalize(NotNull{&arena}, NotNull{&builtinTypes}, NotNull{scope.get()}, generalizedTypes, ty);
|
|
}
|
|
};
|
|
|
|
TEST_CASE_FIXTURE(GeneralizationFixture, "generalize_a_type_that_is_bounded_by_another_generalizable_type")
|
|
{
|
|
auto [t1, ft1] = freshType();
|
|
auto [t2, ft2] = freshType();
|
|
|
|
// t2 <: t1 <: unknown
|
|
// unknown <: t2 <: t1
|
|
|
|
ft1->lowerBound = t2;
|
|
ft2->upperBound = t1;
|
|
ft2->lowerBound = builtinTypes.unknownType;
|
|
|
|
auto t2generalized = generalize(t2);
|
|
REQUIRE(t2generalized);
|
|
|
|
CHECK(follow(t1) == follow(t2));
|
|
|
|
auto t1generalized = generalize(t1);
|
|
REQUIRE(t1generalized);
|
|
|
|
CHECK(builtinTypes.unknownType == follow(t1));
|
|
CHECK(builtinTypes.unknownType == follow(t2));
|
|
}
|
|
|
|
// Same as generalize_a_type_that_is_bounded_by_another_generalizable_type
|
|
// except that we generalize the types in the opposite order
|
|
TEST_CASE_FIXTURE(GeneralizationFixture, "generalize_a_type_that_is_bounded_by_another_generalizable_type_in_reverse_order")
|
|
{
|
|
auto [t1, ft1] = freshType();
|
|
auto [t2, ft2] = freshType();
|
|
|
|
// t2 <: t1 <: unknown
|
|
// unknown <: t2 <: t1
|
|
|
|
ft1->lowerBound = t2;
|
|
ft2->upperBound = t1;
|
|
ft2->lowerBound = builtinTypes.unknownType;
|
|
|
|
auto t1generalized = generalize(t1);
|
|
REQUIRE(t1generalized);
|
|
|
|
CHECK(follow(t1) == follow(t2));
|
|
|
|
auto t2generalized = generalize(t2);
|
|
REQUIRE(t2generalized);
|
|
|
|
CHECK(builtinTypes.unknownType == follow(t1));
|
|
CHECK(builtinTypes.unknownType == follow(t2));
|
|
}
|
|
|
|
TEST_CASE_FIXTURE(GeneralizationFixture, "dont_traverse_into_class_types_when_generalizing")
|
|
{
|
|
auto [propTy, _] = freshType();
|
|
|
|
TypeId cursedExternType = arena.addType(ExternType{"Cursed", {{"oh_no", Property::readonly(propTy)}}, std::nullopt, std::nullopt, {}, {}, "", {}});
|
|
|
|
auto genExternType = generalize(cursedExternType);
|
|
REQUIRE(genExternType);
|
|
|
|
auto genPropTy = get<ExternType>(*genExternType)->props.at("oh_no").readTy;
|
|
CHECK(is<FreeType>(*genPropTy));
|
|
}
|
|
|
|
TEST_CASE_FIXTURE(GeneralizationFixture, "cache_fully_generalized_types")
|
|
{
|
|
CHECK(generalizedTypes->empty());
|
|
|
|
TypeId tinyTable = arena.addType(
|
|
TableType{TableType::Props{{"one", builtinTypes.numberType}, {"two", builtinTypes.stringType}}, std::nullopt, TypeLevel{}, TableState::Sealed}
|
|
);
|
|
|
|
generalize(tinyTable);
|
|
|
|
CHECK(generalizedTypes->contains(tinyTable));
|
|
CHECK(generalizedTypes->contains(builtinTypes.numberType));
|
|
CHECK(generalizedTypes->contains(builtinTypes.stringType));
|
|
}
|
|
|
|
TEST_CASE_FIXTURE(GeneralizationFixture, "dont_cache_types_that_arent_done_yet")
|
|
{
|
|
TypeId freeTy = arena.addType(FreeType{NotNull{globalScope.get()}, builtinTypes.neverType, builtinTypes.stringType});
|
|
|
|
TypeId fnTy = arena.addType(FunctionType{builtinTypes.emptyTypePack, arena.addTypePack(TypePack{{builtinTypes.numberType}})});
|
|
|
|
TypeId tableTy = arena.addType(
|
|
TableType{TableType::Props{{"one", builtinTypes.numberType}, {"two", freeTy}, {"three", fnTy}}, std::nullopt, TypeLevel{}, TableState::Sealed}
|
|
);
|
|
|
|
generalize(tableTy);
|
|
|
|
CHECK(generalizedTypes->contains(fnTy));
|
|
CHECK(generalizedTypes->contains(builtinTypes.numberType));
|
|
CHECK(generalizedTypes->contains(builtinTypes.neverType));
|
|
CHECK(generalizedTypes->contains(builtinTypes.stringType));
|
|
CHECK(!generalizedTypes->contains(freeTy));
|
|
CHECK(!generalizedTypes->contains(tableTy));
|
|
}
|
|
|
|
TEST_CASE_FIXTURE(GeneralizationFixture, "functions_containing_cyclic_tables_can_be_cached")
|
|
{
|
|
TypeId selfTy = arena.addType(BlockedType{});
|
|
|
|
TypeId methodTy = arena.addType(FunctionType{
|
|
arena.addTypePack({selfTy}),
|
|
arena.addTypePack({builtinTypes.numberType}),
|
|
});
|
|
|
|
asMutable(selfTy)->ty.emplace<TableType>(
|
|
TableType::Props{{"count", builtinTypes.numberType}, {"method", methodTy}}, std::nullopt, TypeLevel{}, TableState::Sealed
|
|
);
|
|
|
|
generalize(methodTy);
|
|
|
|
CHECK(generalizedTypes->contains(methodTy));
|
|
CHECK(generalizedTypes->contains(selfTy));
|
|
CHECK(generalizedTypes->contains(builtinTypes.numberType));
|
|
}
|
|
|
|
TEST_CASE_FIXTURE(GeneralizationFixture, "union_type_traversal_doesnt_crash")
|
|
{
|
|
// t1 where t1 = ('h <: (t1 <: 'i)) | ('j <: (t1 <: 'i))
|
|
TypeId i = arena.freshType(NotNull{&builtinTypes}, globalScope.get());
|
|
TypeId h = arena.freshType(NotNull{&builtinTypes}, globalScope.get());
|
|
TypeId j = arena.freshType(NotNull{&builtinTypes}, globalScope.get());
|
|
TypeId unionType = arena.addType(UnionType{{h, j}});
|
|
getMutable<FreeType>(h)->upperBound = i;
|
|
getMutable<FreeType>(h)->lowerBound = builtinTypes.neverType;
|
|
getMutable<FreeType>(i)->upperBound = builtinTypes.unknownType;
|
|
getMutable<FreeType>(i)->lowerBound = unionType;
|
|
getMutable<FreeType>(j)->upperBound = i;
|
|
getMutable<FreeType>(j)->lowerBound = builtinTypes.neverType;
|
|
|
|
generalize(unionType);
|
|
}
|
|
|
|
TEST_CASE_FIXTURE(GeneralizationFixture, "intersection_type_traversal_doesnt_crash")
|
|
{
|
|
// t1 where t1 = ('h <: (t1 <: 'i)) & ('j <: (t1 <: 'i))
|
|
TypeId i = arena.freshType(NotNull{&builtinTypes}, globalScope.get());
|
|
TypeId h = arena.freshType(NotNull{&builtinTypes}, globalScope.get());
|
|
TypeId j = arena.freshType(NotNull{&builtinTypes}, globalScope.get());
|
|
TypeId intersectionType = arena.addType(IntersectionType{{h, j}});
|
|
|
|
getMutable<FreeType>(h)->upperBound = i;
|
|
getMutable<FreeType>(h)->lowerBound = builtinTypes.neverType;
|
|
getMutable<FreeType>(i)->upperBound = builtinTypes.unknownType;
|
|
getMutable<FreeType>(i)->lowerBound = intersectionType;
|
|
getMutable<FreeType>(j)->upperBound = i;
|
|
getMutable<FreeType>(j)->lowerBound = builtinTypes.neverType;
|
|
|
|
generalize(intersectionType);
|
|
}
|
|
|
|
TEST_CASE_FIXTURE(GeneralizationFixture, "('a) -> 'a")
|
|
{
|
|
TypeId freeTy = freshType().first;
|
|
TypeId fnTy = arena.addType(FunctionType{arena.addTypePack({freeTy}), arena.addTypePack({freeTy})});
|
|
|
|
generalize(fnTy);
|
|
|
|
CHECK("<a>(a) -> a" == toString(fnTy));
|
|
}
|
|
|
|
TEST_CASE_FIXTURE(GeneralizationFixture, "(t1, (t1 <: 'b)) -> () where t1 = ('a <: (t1 <: 'b) & {number} & {number})")
|
|
{
|
|
ScopedFastFlag sff{FFlag::LuauEagerGeneralization, true};
|
|
|
|
TableType tt;
|
|
tt.indexer = TableIndexer{builtinTypes.numberType, builtinTypes.numberType};
|
|
TypeId numberArray = arena.addType(TableType{tt});
|
|
|
|
auto [aTy, aFree] = freshType();
|
|
auto [bTy, bFree] = freshType();
|
|
|
|
aFree->upperBound = arena.addType(IntersectionType{{bTy, numberArray, numberArray}});
|
|
bFree->lowerBound = aTy;
|
|
|
|
TypeId functionTy = arena.addType(FunctionType{arena.addTypePack({aTy, bTy}), builtinTypes.emptyTypePack});
|
|
|
|
generalize(functionTy);
|
|
|
|
CHECK("(unknown & {number}, unknown) -> ()" == toString(functionTy));
|
|
}
|
|
|
|
TEST_CASE_FIXTURE(GeneralizationFixture, "(('a <: number | string)) -> string?")
|
|
{
|
|
auto [aTy, aFree] = freshType();
|
|
|
|
aFree->upperBound = arena.addType(UnionType{{builtinTypes.numberType, builtinTypes.stringType}});
|
|
|
|
TypeId fnType = arena.addType(FunctionType{arena.addTypePack({aTy}), arena.addTypePack({builtinTypes.optionalStringType})});
|
|
|
|
generalize(fnType);
|
|
|
|
CHECK("(number | string) -> string?" == toString(fnType));
|
|
}
|
|
|
|
TEST_CASE_FIXTURE(GeneralizationFixture, "(('a <: {'b})) -> ()")
|
|
{
|
|
ScopedFastFlag sff{FFlag::LuauEagerGeneralization, true};
|
|
|
|
auto [aTy, aFree] = freshType();
|
|
auto [bTy, bFree] = freshType();
|
|
|
|
TableType tt;
|
|
tt.indexer = TableIndexer{builtinTypes.numberType, bTy};
|
|
|
|
aFree->upperBound = arena.addType(tt);
|
|
|
|
TypeId functionTy = arena.addType(FunctionType{arena.addTypePack({aTy}), builtinTypes.emptyTypePack});
|
|
|
|
generalize(functionTy);
|
|
|
|
// The free type 'b is not replace with unknown because it appears in an
|
|
// invariant context.
|
|
CHECK("<a>({a}) -> ()" == toString(functionTy));
|
|
}
|
|
|
|
TEST_CASE_FIXTURE(GeneralizationFixture, "(('b <: {t1}), ('a <: t1)) -> t1 where t1 = (('a <: t1) <: 'c)")
|
|
{
|
|
auto [aTy, aFree] = freshType();
|
|
auto [bTy, bFree] = freshType();
|
|
auto [cTy, cFree] = freshType();
|
|
|
|
aFree->upperBound = cTy;
|
|
cFree->lowerBound = aTy;
|
|
|
|
TableType tt;
|
|
tt.indexer = TableIndexer{builtinTypes.numberType, cTy};
|
|
|
|
bFree->upperBound = arena.addType(tt);
|
|
|
|
TypeId functionTy = arena.addType(FunctionType{arena.addTypePack({bTy, aTy}), arena.addTypePack({cTy})});
|
|
|
|
generalize(functionTy);
|
|
|
|
CHECK("<a>({a}, a) -> a" == toString(functionTy));
|
|
}
|
|
|
|
TEST_CASE_FIXTURE(BuiltinsFixture, "generalization_traversal_should_re_traverse_unions_if_they_change_type")
|
|
{
|
|
// This test case should just not assert
|
|
CheckResult result = check(R"(
|
|
function byId(p)
|
|
return p.id
|
|
end
|
|
|
|
function foo()
|
|
|
|
local productButtonPairs = {}
|
|
local func = byId
|
|
local dir = -1
|
|
|
|
local function updateSearch()
|
|
for product, button in pairs(productButtonPairs) do
|
|
button.LayoutOrder = func(product) * dir
|
|
end
|
|
end
|
|
|
|
function(mode)
|
|
if mode == 'Name'then
|
|
else
|
|
if mode == 'New'then
|
|
func = function(p)
|
|
return p.id
|
|
end
|
|
elseif mode == 'Price'then
|
|
func = function(p)
|
|
return p.price
|
|
end
|
|
end
|
|
|
|
end
|
|
end
|
|
end
|
|
)");
|
|
}
|
|
|
|
TEST_CASE_FIXTURE(BuiltinsFixture, "generalization_should_not_leak_free_type")
|
|
{
|
|
ScopedFastFlag sffs[] = {
|
|
{FFlag::DebugLuauForbidInternalTypes, true},
|
|
{FFlag::LuauTrackInferredFunctionTypeFromCall, true}
|
|
};
|
|
|
|
// This test case should just not assert
|
|
CheckResult result = check(R"(
|
|
function foo()
|
|
|
|
local productButtonPairs = {}
|
|
local func
|
|
local dir = -1
|
|
|
|
local function updateSearch()
|
|
for product, button in pairs(productButtonPairs) do
|
|
-- This line may have a floating free type pack.
|
|
button.LayoutOrder = func(product) * dir
|
|
end
|
|
end
|
|
|
|
function(mode)
|
|
if mode == 'New'then
|
|
func = function(p)
|
|
return p.id
|
|
end
|
|
elseif mode == 'Price'then
|
|
func = function(p)
|
|
return p.price
|
|
end
|
|
end
|
|
end
|
|
end
|
|
)");
|
|
}
|
|
|
|
TEST_SUITE_END();
|