mirror of
https://github.com/luau-lang/luau.git
synced 2025-04-05 11:20:54 +01:00
## New Type Solver 1. Update resolved types for singleton unions and intersections to avoid crashing when type checking type assertions. 2. Generalize free return type pack of a function type inferred at call site to ensure that the free type does not leak to another module. 3. Fix crash from cyclic indexers by reducing if possible or producing an error otherwise. 4. Fix handling of irreducible type functions to prevent type inference from failing. 5. Fix handling of recursive metatables to avoid infinite recursion. ## New and Old Type Solver Fix accidental capture of all exceptions in multi-threaded typechecking by converting all typechecking exceptions to `InternalCompilerError` and only capturing those. ## Fragment Autocomplete 1. Add a block based diff algorithm based on class index and span for re-typechecking. This reduces the granularity of fragment autocomplete to avoid flakiness when the fragment does not have enough type information. 2. Fix bugs arising from incorrect scope selection for autocompletion. ## Roundtrippable AST Store type alias location in `TypeFun` class to ensure it is accessible for exported types as part of the public interface. ## Build System 1. Bump minimum supported CMake version to 3.10 since GitHub is phasing out the currently supported minimum version 3.0, released 11 years ago. 2. Fix compilation when `HARDSTACKTESTS` is enabled. ## Miscellaneous Flag removals and cleanup of unused code. ## Internal Contributors Co-authored-by: Andy Friesen <afriesen@roblox.com> Co-authored-by: Ariel Weiss <aaronweiss@roblox.com> Co-authored-by: Hunter Goldstein <hgoldstein@roblox.com> Co-authored-by: Talha Pathan <tpathan@roblox.com> Co-authored-by: Vighnesh Vijay <vvijay@roblox.com> Co-authored-by: Vyacheslav Egorov <vegorov@roblox.com> ## External Contributors Thanks to [@grh-official](https://github.com/grh-official) for PR #1759 **Full Changelog**: https://github.com/luau-lang/luau/compare/0.667...0.668 --------- Co-authored-by: Hunter Goldstein <hgoldstein@roblox.com> Co-authored-by: Varun Saini <61795485+vrn-sn@users.noreply.github.com> Co-authored-by: Alexander Youngblood <ayoungblood@roblox.com> Co-authored-by: Menarul Alam <malam@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>
294 lines
8.9 KiB
C++
294 lines
8.9 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(DebugLuauForbidInternalTypes)
|
|
LUAU_FASTFLAG(LuauTrackInteriorFreeTypesOnScope)
|
|
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 cursedClass = arena.addType(ClassType{"Cursed", {{"oh_no", Property::readonly(propTy)}}, std::nullopt, std::nullopt, {}, {}, "", {}});
|
|
|
|
auto genClass = generalize(cursedClass);
|
|
REQUIRE(genClass);
|
|
|
|
auto genPropTy = get<ClassType>(*genClass)->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(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::LuauTrackInteriorFreeTypesOnScope, 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();
|