luau/tests/Generalization.test.cpp
Aviral Goel ee1c6bf0db
Sync to upstream/release/668 (#1760)
## 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>
2025-04-04 14:11:51 -07:00

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();