Sync to upstream/release/612 (#1162)

New solver

* Fix bugs where bidirectional type inference would fail to take effect
at the proper stage.
* Improve inference of mutually recursive functions
* Fix crashes

---------

Co-authored-by: Aaron Weiss <aaronweiss@roblox.com>
Co-authored-by: Andy Friesen <afriesen@roblox.com>
Co-authored-by: Vyacheslav Egorov <vegorov@roblox.com>
This commit is contained in:
Andy Friesen 2024-02-09 09:51:12 -08:00 committed by GitHub
parent 67ce75e870
commit d6c2472f0c
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
13 changed files with 815 additions and 353 deletions

View File

@ -102,6 +102,7 @@ struct FunctionCheckConstraint
TypePackId argsPack;
class AstExprCall* callSite = nullptr;
NotNull<DenseHashMap<const AstExpr*, TypeId>> astExpectedTypes;
};
// prim FreeType ExpectedType PrimitiveType

View File

@ -138,6 +138,53 @@ void forEachConstraint(const Checkpoint& start, const Checkpoint& end, const Con
f(cg->constraints[i]);
}
struct HasFreeType : TypeOnceVisitor
{
bool result = false;
HasFreeType()
{
}
bool visit(TypeId ty) override
{
if (result || ty->persistent)
return false;
return true;
}
bool visit(TypePackId tp) override
{
if (result)
return false;
return true;
}
bool visit(TypeId ty, const ClassType&) override
{
return false;
}
bool visit(TypeId ty, const FreeType&) override
{
result = true;
return false;
}
bool visit(TypePackId ty, const FreeTypePack&) override
{
result = true;
return false;
}
};
bool hasFreeType(TypeId ty)
{
HasFreeType hft{};
hft.traverse(ty);
return hft.result;
}
} // namespace
ConstraintGenerator::ConstraintGenerator(ModulePtr module, NotNull<Normalizer> normalizer, NotNull<ModuleResolver> moduleResolver,
@ -229,6 +276,8 @@ std::optional<TypeId> ConstraintGenerator::lookup(const ScopePtr& scope, DefId d
{
if (auto found = scope->lookup(def))
return *found;
else if (phi->operands.size() == 1)
return lookup(scope, phi->operands[0], prototype);
else if (!prototype)
return std::nullopt;
@ -837,6 +886,10 @@ ControlFlow ConstraintGenerator::visit(const ScopePtr& scope, AstStatLocalFuncti
FunctionSignature sig = checkFunctionSignature(scope, function->func, /* expectedType */ std::nullopt, function->name->location);
sig.bodyScope->bindings[function->name] = Binding{sig.signature, function->func->location};
bool sigFullyDefined = !hasFreeType(sig.signature);
if (sigFullyDefined)
asMutable(functionType)->ty.emplace<BoundType>(sig.signature);
DefId def = dfg->getDef(function->name);
scope->lvalueTypes[def] = functionType;
scope->rvalueRefinements[def] = functionType;
@ -847,12 +900,16 @@ ControlFlow ConstraintGenerator::visit(const ScopePtr& scope, AstStatLocalFuncti
checkFunctionBody(sig.bodyScope, function->func);
Checkpoint end = checkpoint(this);
if (!sigFullyDefined)
{
NotNull<Scope> constraintScope{sig.signatureScope ? sig.signatureScope.get() : sig.bodyScope.get()};
std::unique_ptr<Constraint> c =
std::make_unique<Constraint>(constraintScope, function->name->location, GeneralizationConstraint{functionType, sig.signature});
Constraint* previous = nullptr;
forEachConstraint(start, end, this, [&c, &previous](const ConstraintPtr& constraint) {
forEachConstraint(start, end, this,
[&c, &previous](const ConstraintPtr& constraint)
{
c->dependencies.push_back(NotNull{constraint.get()});
if (auto psc = get<PackSubtypeConstraint>(*constraint); psc && psc->returns)
@ -866,6 +923,9 @@ ControlFlow ConstraintGenerator::visit(const ScopePtr& scope, AstStatLocalFuncti
addConstraint(scope, std::move(c));
module->astTypes[function->func] = functionType;
}
else
module->astTypes[function->func] = sig.signature;
return ControlFlow::None;
}
@ -879,12 +939,19 @@ ControlFlow ConstraintGenerator::visit(const ScopePtr& scope, AstStatFunction* f
Checkpoint start = checkpoint(this);
FunctionSignature sig = checkFunctionSignature(scope, function->func, /* expectedType */ std::nullopt, function->name->location);
bool sigFullyDefined = !hasFreeType(sig.signature);
if (sigFullyDefined)
asMutable(generalizedType)->ty.emplace<BoundType>(sig.signature);
DenseHashSet<Constraint*> excludeList{nullptr};
DefId def = dfg->getDef(function->name);
std::optional<TypeId> existingFunctionTy = lookup(scope, def);
if (sigFullyDefined && existingFunctionTy && get<BlockedType>(*existingFunctionTy))
asMutable(*existingFunctionTy)->ty.emplace<BoundType>(sig.signature);
if (AstExprLocal* localName = function->name->as<AstExprLocal>())
{
if (existingFunctionTy)
@ -906,6 +973,7 @@ ControlFlow ConstraintGenerator::visit(const ScopePtr& scope, AstStatFunction* f
if (!existingFunctionTy)
ice->ice("prepopulateGlobalScope did not populate a global name", globalName->location);
if (!sigFullyDefined)
generalizedType = *existingFunctionTy;
sig.bodyScope->bindings[globalName->name] = Binding{sig.signature, globalName->location};
@ -943,12 +1011,16 @@ ControlFlow ConstraintGenerator::visit(const ScopePtr& scope, AstStatFunction* f
checkFunctionBody(sig.bodyScope, function->func);
Checkpoint end = checkpoint(this);
if (!sigFullyDefined)
{
NotNull<Scope> constraintScope{sig.signatureScope ? sig.signatureScope.get() : sig.bodyScope.get()};
std::unique_ptr<Constraint> c =
std::make_unique<Constraint>(constraintScope, function->name->location, GeneralizationConstraint{generalizedType, sig.signature});
Constraint* previous = nullptr;
forEachConstraint(start, end, this, [&c, &excludeList, &previous](const ConstraintPtr& constraint) {
forEachConstraint(start, end, this,
[&c, &excludeList, &previous](const ConstraintPtr& constraint)
{
if (!excludeList.contains(constraint.get()))
c->dependencies.push_back(NotNull{constraint.get()});
@ -962,6 +1034,7 @@ ControlFlow ConstraintGenerator::visit(const ScopePtr& scope, AstStatFunction* f
});
addConstraint(scope, std::move(c));
}
return ControlFlow::None;
}
@ -1626,24 +1699,6 @@ InferencePack ConstraintGenerator::checkPack(const ScopePtr& scope, AstExprCall*
TypePackId argPack = addTypePack(std::move(args), argTail);
FunctionType ftv(TypeLevel{}, scope.get(), argPack, rets, std::nullopt, call->self);
NotNull<Constraint> fcc = addConstraint(scope, call->func->location,
FunctionCallConstraint{
fnType,
argPack,
rets,
call,
std::move(discriminantTypes),
&module->astOverloadResolvedTypes,
});
NotNull<Constraint> foo = addConstraint(scope, call->func->location,
FunctionCheckConstraint{
fnType,
argPack,
call
}
);
/*
* To make bidirectional type checking work, we need to solve these constraints in a particular order:
*
@ -1653,14 +1708,35 @@ InferencePack ConstraintGenerator::checkPack(const ScopePtr& scope, AstExprCall*
* 4. Solve the call
*/
forEachConstraint(funcBeginCheckpoint, funcEndCheckpoint, this, [foo](const ConstraintPtr& constraint) {
foo->dependencies.emplace_back(constraint.get());
NotNull<Constraint> checkConstraint = addConstraint(scope, call->func->location,
FunctionCheckConstraint{
fnType,
argPack,
call,
NotNull{&module->astExpectedTypes}
}
);
forEachConstraint(funcBeginCheckpoint, funcEndCheckpoint, this, [checkConstraint](const ConstraintPtr& constraint) {
checkConstraint->dependencies.emplace_back(constraint.get());
});
forEachConstraint(argBeginCheckpoint, argEndCheckpoint, this, [foo, fcc](const ConstraintPtr& constraint) {
constraint->dependencies.emplace_back(foo);
NotNull<Constraint> callConstraint = addConstraint(scope, call->func->location,
FunctionCallConstraint{
fnType,
argPack,
rets,
call,
std::move(discriminantTypes),
&module->astOverloadResolvedTypes,
});
fcc->dependencies.emplace_back(constraint.get());
callConstraint->dependencies.push_back(checkConstraint);
forEachConstraint(argBeginCheckpoint, argEndCheckpoint, this, [checkConstraint, callConstraint](const ConstraintPtr& constraint) {
constraint->dependencies.emplace_back(checkConstraint);
callConstraint->dependencies.emplace_back(constraint.get());
});
return InferencePack{rets, {refinementArena.variadic(returnRefinements)}};
@ -1884,7 +1960,7 @@ Inference ConstraintGenerator::check(const ScopePtr& scope, AstExprFunction* fun
checkFunctionBody(sig.bodyScope, func);
Checkpoint endCheckpoint = checkpoint(this);
if (generalize)
if (generalize && hasFreeType(sig.signature))
{
TypeId generalizedTy = arena->addType(BlockedType{});
NotNull<Constraint> gc = addConstraint(sig.signatureScope, func->location, GeneralizationConstraint{generalizedTy, sig.signature});

View File

@ -1139,7 +1139,7 @@ bool ConstraintSolver::tryDispatch(const FunctionCheckConstraint& c, NotNull<con
return block(fn, constraint);
if (isBlocked(argsPack))
return block(argsPack, constraint);
return true;
// We know the type of the function and the arguments it expects to receive.
// We also know the TypeIds of the actual arguments that will be passed.
@ -1152,8 +1152,10 @@ bool ConstraintSolver::tryDispatch(const FunctionCheckConstraint& c, NotNull<con
// types.
// FIXME: Bidirectional type checking of overloaded functions is not yet supported.
if (auto ftv = get<FunctionType>(fn))
{
const FunctionType* ftv = get<FunctionType>(fn);
if (!ftv)
return true;
const std::vector<TypeId> expectedArgs = flatten(ftv->argTypes).first;
const std::vector<TypeId> argPackHead = flatten(argsPack).first;
@ -1161,10 +1163,13 @@ bool ConstraintSolver::tryDispatch(const FunctionCheckConstraint& c, NotNull<con
{
const TypeId expectedArgTy = follow(expectedArgs[i]);
const TypeId actualArgTy = follow(argPackHead[i]);
const AstExpr* expr = c.callSite->args.data[i];
(*c.astExpectedTypes)[expr] = expectedArgTy;
const FunctionType* expectedLambdaTy = get<FunctionType>(expectedArgTy);
const FunctionType* lambdaTy = get<FunctionType>(actualArgTy);
const AstExprFunction* lambdaExpr = c.callSite->args.data[i]->as<AstExprFunction>();
const AstExprFunction* lambdaExpr = expr->as<AstExprFunction>();
if (expectedLambdaTy && lambdaTy && lambdaExpr)
{
@ -1179,13 +1184,12 @@ bool ConstraintSolver::tryDispatch(const FunctionCheckConstraint& c, NotNull<con
}
}
}
else
else if (expr->is<AstExprConstantBool>() || expr->is<AstExprConstantString>() || expr->is<AstExprConstantNumber>() || expr->is<AstExprConstantNil>() || expr->is<AstExprTable>())
{
Unifier2 u2{arena, builtinTypes, constraint->scope, NotNull{&iceReporter}};
u2.unify(actualArgTy, expectedArgTy);
}
}
}
return true;
}

View File

@ -593,6 +593,8 @@ SubtypingResult Subtyping::isCovariantWith(SubtypingEnvironment& env, TypePackId
return SubtypingResult{false}.withSubComponent(TypePath::PackField::Tail);
}
}
else if (get<ErrorTypePack>(*subTail))
return SubtypingResult{true}.withSubComponent(TypePath::PackField::Tail);
else
unexpected(*subTail);
}
@ -643,6 +645,8 @@ SubtypingResult Subtyping::isCovariantWith(SubtypingEnvironment& env, TypePackId
return SubtypingResult{false}.withSuperComponent(TypePath::PackField::Tail);
}
}
else if (get<ErrorTypePack>(*superTail))
return SubtypingResult{true}.withSuperComponent(TypePath::PackField::Tail);
else
unexpected(*superTail);
}

View File

@ -495,11 +495,12 @@ struct TypeChecker2
return checkForFamilyInhabitance(follow(*ty), annotation->location);
}
TypePackId lookupPackAnnotation(AstTypePack* annotation)
std::optional<TypePackId> lookupPackAnnotation(AstTypePack* annotation)
{
TypePackId* tp = module->astResolvedTypePacks.find(annotation);
LUAU_ASSERT(tp);
return follow(*tp);
if (tp != nullptr)
return {follow(*tp)};
return {};
}
TypeId lookupExpectedType(AstExpr* expr)
@ -2187,9 +2188,11 @@ struct TypeChecker2
}
else if (p.typePack)
{
TypePackId tp = lookupPackAnnotation(p.typePack);
std::optional<TypePackId> tp = lookupPackAnnotation(p.typePack);
if (!tp.has_value())
continue;
if (typesProvided < typesRequired && size(tp) == 1 && finite(tp) && first(tp))
if (typesProvided < typesRequired && size(*tp) == 1 && finite(*tp) && first(*tp))
{
typesProvided += 1;
}
@ -2607,7 +2610,12 @@ struct TypeChecker2
// shape. We instead want to report the unknown property error of
// the `else` branch.
else if (context == ValueContext::LValue && !get<ClassType>(tableTy))
{
if (get<PrimitiveType>(tableTy) || get<FunctionType>(tableTy))
reportError(NotATable{tableTy}, location);
else
reportError(CannotExtendTable{tableTy, CannotExtendTable::Property, prop}, location);
}
else
reportError(UnknownProperty{tableTy, prop}, location);
}

View File

@ -92,10 +92,11 @@ private:
size_t tail_size = queue_size - head_size; // how many elements are in the tail portion (i.e. any portion that wrapped to the front)
// move the head into the new buffer
if (head_size != 0)
std::uninitialized_move(buffer + head, buffer + head + head_size, new_buffer);
// move the tail into the new buffer immediately after
if (head_size < queue_size)
if (tail_size != 0)
std::uninitialized_move(buffer, buffer + tail_size, new_buffer + head_size);
// destroy the old elements
@ -128,8 +129,16 @@ public:
, head(other.head)
, queue_size(other.queue_size)
{
// copy the contents of the other buffer to this one
std::uninitialized_copy(other.buffer, other.buffer + other.buffer_capacity, buffer);
// copy the initialized contents of the other buffer to this one
size_t head_size = std::min(other.queue_size,
other.buffer_capacity - other.head); // how many elements are in the head portion (i.e. from the head to the end of the buffer)
size_t tail_size = other.queue_size - head_size; // how many elements are in the tail portion (i.e. any portion that wrapped to the front)
if (head_size != 0)
std::uninitialized_copy(other.buffer + other.head, other.buffer + other.head + head_size, buffer + head);
if (tail_size != 0)
std::uninitialized_copy(other.buffer, other.buffer + tail_size, buffer);
}
VecDeque(const VecDeque& other, const Allocator& alloc)
@ -139,8 +148,16 @@ public:
, head(other.head)
, queue_size(other.queue_size)
{
// copy the contents of the other buffer to this one
std::uninitialized_copy(other.buffer, other.buffer + other.buffer_capacity, buffer);
// copy the initialized contents of the other buffer to this one
size_t head_size = std::min(other.queue_size,
other.buffer_capacity - other.head); // how many elements are in the head portion (i.e. from the head to the end of the buffer)
size_t tail_size = other.queue_size - head_size; // how many elements are in the tail portion (i.e. any portion that wrapped to the front)
if (head_size != 0)
std::uninitialized_copy(other.buffer + other.head, other.buffer + other.head + head_size, buffer + head);
if (tail_size != 0)
std::uninitialized_copy(other.buffer, other.buffer + tail_size, buffer);
}
VecDeque(VecDeque&& other) noexcept
@ -195,13 +212,18 @@ public:
buffer_capacity = other.buffer_capacity;
}
size_t head_size = other.capacity() - other.head; // how many elements are in the head portion (i.e. from the head to the end of the buffer)
size_t tail_size = other.size() - head_size; // how many elements are in the tail portion (i.e. any portion that wrapped to the front)
size_t head_size = std::min(other.queue_size,
other.buffer_capacity - other.head); // how many elements are in the head portion (i.e. from the head to the end of the buffer)
size_t tail_size = other.queue_size - head_size; // how many elements are in the tail portion (i.e. any portion that wrapped to the front)
// copy the contents of the other buffer's head into place
std::uninitialized_copy(other.buffer + other.head, other.buffer + head + head_size, buffer);
// Assignment doesn't try to match the capacity of 'other' and thus makes the buffer contiguous
head = 0;
queue_size = other.queue_size;
// copy the contents of the other buffer's tail into place immediately after
if (head_size != 0)
std::uninitialized_copy(other.buffer + other.head, other.buffer + other.head + head_size, buffer);
if (tail_size != 0)
std::uninitialized_copy(other.buffer, other.buffer + tail_size, buffer + head_size);
return *this;
@ -325,17 +347,16 @@ public:
T* new_buffer = this->allocate(new_capacity);
// move the head into the new buffer
if (head_size != 0)
std::uninitialized_move(buffer + head, buffer + head + head_size, new_buffer);
// move the tail into the new buffer immediately after, if we have one
if (head_size < queue_size)
std::uninitialized_move(buffer, buffer + tail_size, new_buffer + head_size);
// move the tail into the new buffer immediately after
if (tail_size != 0)
std::uninitialized_move(buffer, buffer + tail_size, new_buffer + head_size);
// destroy all the existing elements before freeing the old buffer
destroyElements();
// deallocate the old buffer
this->deallocate(buffer, old_capacity);
@ -350,7 +371,8 @@ public:
return buffer_capacity;
}
void shrink_to_fit() {
void shrink_to_fit()
{
size_t old_capacity = capacity();
size_t new_capacity = queue_size;
@ -365,10 +387,11 @@ public:
T* new_buffer = this->allocate(new_capacity);
// move the head into the new buffer
if (head_size != 0)
std::uninitialized_move(buffer + head, buffer + head + head_size, new_buffer);
// move the tail into the new buffer immediately after, if we have one
if (head_size < queue_size)
if (tail_size != 0)
std::uninitialized_move(buffer, buffer + tail_size, new_buffer + head_size);
// destroy all the existing elements before freeing the old buffer

View File

@ -2707,16 +2707,26 @@ local t: Test = { @1 }
CHECK(ac.entryMap.count("first"));
CHECK(ac.entryMap.count("second"));
CHECK_EQ(ac.context, AutocompleteContext::Property);
}
TEST_CASE_FIXTURE(ACFixture, "suggest_table_keys_no_initial_character_2")
{
ScopedFastFlag sff{FFlag::LuauAutocompleteTableKeysNoInitialCharacter, true};
check(R"(
type Test = { first: number, second: number }
local t: Test = { first = 1, @1 }
)");
ac = autocomplete('1');
auto ac = autocomplete('1');
CHECK_EQ(ac.entryMap.count("first"), 0);
CHECK(ac.entryMap.count("second"));
CHECK_EQ(ac.context, AutocompleteContext::Property);
}
TEST_CASE_FIXTURE(ACFixture, "suggest_table_keys_no_initial_character_3")
{
ScopedFastFlag sff{FFlag::LuauAutocompleteTableKeysNoInitialCharacter, true};
check(R"(
type Properties = { TextScaled: boolean, Text: string }
@ -2725,7 +2735,8 @@ local function create(props: Properties) end
create({ @1 })
)");
ac = autocomplete('1');
auto ac = autocomplete('1');
CHECK(ac.entryMap.size() > 0);
CHECK(ac.entryMap.count("TextScaled"));
CHECK(ac.entryMap.count("Text"));
CHECK_EQ(ac.context, AutocompleteContext::Property);

View File

@ -582,4 +582,28 @@ TEST_CASE_FIXTURE(BuiltinsFixture, "keyof_rfc_example")
CHECK_EQ("\"cactus\"", toString(tm->givenType));
}
TEST_CASE_FIXTURE(BuiltinsFixture, "keyof_oss_crash_gh1161")
{
if (!FFlag::DebugLuauDeferredConstraintResolution)
return;
CheckResult result = check(R"(
local EnumVariants = {
["a"] = 1, ["b"] = 2, ["c"] = 3
}
type EnumKey = keyof<typeof(EnumVariants)>
function fnA<T>(i: T): keyof<T> end
function fnB(i: EnumKey) end
local result = fnA(EnumVariants)
fnB(result)
)");
LUAU_REQUIRE_ERROR_COUNT(1, result);
CHECK(get<FunctionExitsWithoutReturning>(result.errors[0]));
}
TEST_SUITE_END();

View File

@ -1075,4 +1075,11 @@ TEST_CASE_FIXTURE(Fixture, "typeof_is_not_a_valid_alias_name")
CHECK("Type aliases cannot be named typeof" == toString(result.errors[0]));
}
TEST_CASE_FIXTURE(Fixture, "fuzzer_bug_doesnt_crash")
{
CheckResult result = check(R"(
type t0 = (t0<t0...>)
)");
LUAU_REQUIRE_ERRORS(result);
}
TEST_SUITE_END();

View File

@ -50,14 +50,31 @@ TEST_CASE_FIXTURE(Fixture, "tc_function")
TEST_CASE_FIXTURE(Fixture, "check_function_bodies")
{
CheckResult result = check("function myFunction() local a = 0 a = true end");
CheckResult result = check(R"(
function myFunction(): number
local a = 0
a = true
return a
end
)");
LUAU_REQUIRE_ERROR_COUNT(1, result);
CHECK_EQ(result.errors[0], (TypeError{Location{Position{0, 44}, Position{0, 48}}, TypeMismatch{
if (FFlag::DebugLuauDeferredConstraintResolution)
{
const TypePackMismatch* tm = get<TypePackMismatch>(result.errors[0]);
REQUIRE(tm);
CHECK(toString(tm->wantedTp) == "number");
CHECK(toString(tm->givenTp) == "boolean");
}
else
{
CHECK_EQ(result.errors[0], (TypeError{Location{Position{3, 16}, Position{3, 20}}, TypeMismatch{
builtinTypes->numberType,
builtinTypes->booleanType,
}}));
}
}
TEST_CASE_FIXTURE(Fixture, "cannot_hoist_interior_defns_into_signature")
{
@ -2227,4 +2244,69 @@ a = function(a, b) return a + b end
LUAU_REQUIRE_NO_ERRORS(result);
}
TEST_CASE_FIXTURE(BuiltinsFixture, "simple_unannotated_mutual_recursion")
{
CheckResult result = check(R"(
function even(n)
if n == 0 then
return true
else
return odd(n - 1)
end
end
function odd(n)
if n == 0 then
return false
elseif n == 1 then
return true
else
return even(n - 1)
end
end
)");
if (FFlag::DebugLuauDeferredConstraintResolution)
{
LUAU_REQUIRE_ERROR_COUNT(4, result);
CHECK(toString(result.errors[0]) == "Type family instance sub<unknown, number> is uninhabited");
CHECK(toString(result.errors[1]) == "Type family instance sub<unknown, number> is uninhabited");
CHECK(toString(result.errors[2]) == "Type family instance sub<unknown, number> is uninhabited");
CHECK(toString(result.errors[3]) == "Type family instance sub<unknown, number> is uninhabited");
}
else
{
LUAU_REQUIRE_ERROR_COUNT(1, result);
CHECK(toString(result.errors[0]) == "Unknown type used in - operation; consider adding a type annotation to 'n'");
}
}
TEST_CASE_FIXTURE(BuiltinsFixture, "simple_lightly_annotated_mutual_recursion")
{
CheckResult result = check(R"(
function even(n: number)
if n == 0 then
return true
else
return odd(n - 1)
end
end
function odd(n: number)
if n == 0 then
return false
elseif n == 1 then
return true
else
return even(n - 1)
end
end
)");
LUAU_REQUIRE_NO_ERRORS(result);
CHECK_EQ("(number) -> boolean", toString(requireType("even")));
CHECK_EQ("(number) -> boolean", toString(requireType("odd")));
}
TEST_SUITE_END();

View File

@ -144,12 +144,12 @@ TEST_CASE_FIXTURE(Fixture, "check_recursive_generic_function")
TEST_CASE_FIXTURE(Fixture, "check_mutual_generic_functions")
{
CheckResult result = check(R"(
local id2
local function id1<a>(x:a):a
function id1<a>(x:a):a
local y: string = id2("hi")
local z: number = id2(37)
return x
end
function id2<a>(x:a):a
local y: string = id1("hi")
local z: number = id1(37)
@ -159,6 +159,68 @@ TEST_CASE_FIXTURE(Fixture, "check_mutual_generic_functions")
LUAU_REQUIRE_NO_ERRORS(result);
}
TEST_CASE_FIXTURE(Fixture, "check_mutual_generic_functions_unannotated")
{
if (!FFlag::DebugLuauDeferredConstraintResolution)
return;
CheckResult result = check(R"(
function id1(x)
local y: string = id2("hi")
local z: number = id2(37)
return x
end
function id2(x)
local y: string = id1("hi")
local z: number = id1(37)
return x
end
)");
LUAU_REQUIRE_NO_ERRORS(result);
}
TEST_CASE_FIXTURE(Fixture, "check_mutual_generic_functions_errors")
{
if (!FFlag::DebugLuauDeferredConstraintResolution)
return;
CheckResult result = check(R"(
function id1(x)
local y: string = id2(37) -- odd
local z: number = id2("hi") -- even
return x
end
function id2(x)
local y: string = id1(37) -- odd
local z: number = id1("hi") -- even
return x
end
)");
LUAU_REQUIRE_ERROR_COUNT(4, result);
// odd errors
for (int i = 0; i < 4; i += 2)
{
TypeMismatch* tm = get<TypeMismatch>(result.errors[i]);
REQUIRE(tm);
CHECK_EQ("string", toString(tm->wantedType));
CHECK_EQ("number", toString(tm->givenType));
}
// even errors
for (int i = 1; i < 4; i += 2)
{
TypeMismatch* tm = get<TypeMismatch>(result.errors[i]);
REQUIRE(tm);
CHECK_EQ("number", toString(tm->wantedType));
CHECK_EQ("string", toString(tm->givenType));
}
}
TEST_CASE_FIXTURE(Fixture, "generic_functions_in_types")
{
CheckResult result = check(R"(
@ -947,7 +1009,7 @@ TEST_CASE_FIXTURE(Fixture, "instantiate_cyclic_generic_function")
TypeId arg = follow(*optionArg);
const TableType* argTable = get<TableType>(arg);
REQUIRE(argTable != nullptr);
REQUIRE_MESSAGE(argTable != nullptr, "Expected table but got " << toString(arg));
std::optional<Property> methodProp = get(argTable->props, "method");
REQUIRE(bool(methodProp));

View File

@ -287,23 +287,28 @@ TEST_CASE("shrink_to_fit_works")
CHECK_EQ(queue[j], j);
}
// To avoid hitting SSO issues, we need sufficiently long strings.
// This list of test strings consists of quotes from Ursula K. Le Guin.
const static std::string testStrings[] = {
"Love doesn't just sit there, like a stone, it has to be made, like bread; remade all the time, made new.",
const static std::string testStringSet[2][10] = {
// To hit potential SSO issues showing memory management issues, we need small strings
{"one", "two", "three", "four", "five", "six", "seven", "eight", "nine", "ten"},
// This list of non-SSO test strings consists of quotes from Ursula K. Le Guin.
{"Love doesn't just sit there, like a stone, it has to be made, like bread; remade all the time, made new.",
"People who deny the existence of dragons are often eaten by dragons. From within.",
"It is good to have an end to journey toward; but it is the journey that matters, in the end.",
"We're each of us alone, to be sure. What can you do but hold your hand out in the dark?",
"When you light a candle, you also cast a shadow.",
"We're each of us alone, to be sure. What can you do but hold your hand out in the dark?", "When you light a candle, you also cast a shadow.",
"You cannot buy the revolution. You cannot make the revolution. You can only be the revolution. It is in your spirit, or it is nowhere.",
"To learn which questions are unanswerable, and not to answer them: this skill is most needful in times of stress and darkness.",
"What sane person could live in this world and not be crazy?",
"The only thing that makes life possible is permanent, intolerable uncertainty: not knowing what comes next.",
"My imagination makes me human and makes me a fool; it gives me all the world and exiles me from it."
};
"My imagination makes me human and makes me a fool; it gives me all the world and exiles me from it."}};
TEST_CASE("string_queue_test_no_initial_capacity")
{
for (size_t stringSet = 0; stringSet < 2; stringSet++)
{
auto testStrings = testStringSet[stringSet];
// initial capacity is not set, so this should grow to be 11
Luau::VecDeque<std::string> queue;
@ -327,9 +332,14 @@ TEST_CASE("string_queue_test_no_initial_capacity")
queue.pop_front();
}
}
}
TEST_CASE("string_queue_test")
{
for (size_t stringSet = 0; stringSet < 2; stringSet++)
{
auto testStrings = testStringSet[stringSet];
// initial capacity set to 5 so that a grow is necessary
Luau::VecDeque<std::string> queue;
queue.reserve(5);
@ -354,9 +364,14 @@ TEST_CASE("string_queue_test")
queue.pop_front();
}
}
}
TEST_CASE("string_queue_test_initializer_list")
{
for (size_t stringSet = 0; stringSet < 2; stringSet++)
{
auto testStrings = testStringSet[stringSet];
Luau::VecDeque<std::string> queue{
testStrings[0],
testStrings[1],
@ -385,9 +400,14 @@ TEST_CASE("string_queue_test_initializer_list")
queue.pop_front();
}
}
}
TEST_CASE("reverse_string_queue_test")
{
for (size_t stringSet = 0; stringSet < 2; stringSet++)
{
auto testStrings = testStringSet[stringSet];
// initial capacity set to 5 so that a grow is necessary
Luau::VecDeque<std::string> queue;
queue.reserve(5);
@ -412,9 +432,14 @@ TEST_CASE("reverse_string_queue_test")
queue.pop_back();
}
}
}
TEST_CASE("random_access_string_queue_test")
{
for (size_t stringSet = 0; stringSet < 2; stringSet++)
{
auto testStrings = testStringSet[stringSet];
// initial capacity set to 5 so that a grow is necessary
Luau::VecDeque<std::string> queue;
queue.reserve(5);
@ -434,9 +459,14 @@ TEST_CASE("random_access_string_queue_test")
CHECK_EQ(queue[j], testStrings[j]);
}
}
}
TEST_CASE("clear_works_on_string_queue")
{
for (size_t stringSet = 0; stringSet < 2; stringSet++)
{
auto testStrings = testStringSet[stringSet];
// initial capacity set to 5 so that a grow is necessary
Luau::VecDeque<std::string> queue;
queue.reserve(5);
@ -457,9 +487,14 @@ TEST_CASE("clear_works_on_string_queue")
CHECK(queue.empty());
CHECK(queue.size() == 0);
}
}
TEST_CASE("pop_front_string_at_end")
{
for (size_t stringSet = 0; stringSet < 2; stringSet++)
{
auto testStrings = testStringSet[stringSet];
// initial capacity set to 5 so that a grow is necessary
Luau::VecDeque<std::string> queue;
queue.reserve(5);
@ -485,9 +520,14 @@ TEST_CASE("pop_front_string_at_end")
queue.pop_front();
}
}
}
TEST_CASE("pop_back_string_at_front")
{
for (size_t stringSet = 0; stringSet < 2; stringSet++)
{
auto testStrings = testStringSet[stringSet];
// initial capacity set to 5 so that a grow is necessary
Luau::VecDeque<std::string> queue;
queue.reserve(5);
@ -513,9 +553,14 @@ TEST_CASE("pop_back_string_at_front")
queue.pop_back();
}
}
}
TEST_CASE("string_queue_is_contiguous")
{
for (size_t stringSet = 0; stringSet < 2; stringSet++)
{
auto testStrings = testStringSet[stringSet];
// initial capacity is not set, so this should grow to be 11
Luau::VecDeque<std::string> queue{};
@ -530,10 +575,68 @@ TEST_CASE("string_queue_is_contiguous")
CHECK_EQ(queue.capacity(), 11);
CHECK(queue.is_contiguous());
for (int j = 0; j < 10; j++)
CHECK_EQ(queue[j], testStrings[j]);
// Check copy construction
Luau::VecDeque<std::string> queue2 = queue;
REQUIRE(!queue2.empty());
REQUIRE(queue2.size() == 10);
CHECK_EQ(queue2.capacity(), 11);
CHECK(queue2.is_contiguous());
for (int j = 0; j < 10; j++)
CHECK_EQ(queue2[j], testStrings[j]);
// Check copy assignment
Luau::VecDeque<std::string> queue3;
queue3 = queue;
REQUIRE(!queue3.empty());
REQUIRE(queue3.size() == 10);
CHECK_EQ(queue3.capacity(), 11);
CHECK(queue3.is_contiguous());
for (int j = 0; j < 10; j++)
CHECK_EQ(queue3[j], testStrings[j]);
// Check move construction
Luau::VecDeque<std::string> queue4 = std::move(queue3);
REQUIRE(!queue4.empty());
REQUIRE(queue4.size() == 10);
CHECK_EQ(queue4.capacity(), 11);
CHECK(queue4.is_contiguous());
for (int j = 0; j < 10; j++)
CHECK_EQ(queue4[j], testStrings[j]);
// Check move assignment
Luau::VecDeque<std::string> queue5;
queue5 = std::move(queue2);
REQUIRE(!queue5.empty());
REQUIRE(queue5.size() == 10);
CHECK_EQ(queue5.capacity(), 11);
CHECK(queue5.is_contiguous());
for (int j = 0; j < 10; j++)
CHECK_EQ(queue5[j], testStrings[j]);
}
}
TEST_CASE("string_queue_is_not_contiguous")
{
for (size_t stringSet = 0; stringSet < 2; stringSet++)
{
auto testStrings = testStringSet[stringSet];
// initial capacity is not set, so this should grow to be 11
Luau::VecDeque<std::string> queue{};
@ -555,10 +658,80 @@ TEST_CASE("string_queue_is_not_contiguous")
// checking that it is indeed sequential integers from 0 to 9
for (int j = 0; j < 10; j++)
CHECK_EQ(queue[j], testStrings[j]);
// Check copy construction
Luau::VecDeque<std::string> queue2 = queue;
REQUIRE(!queue2.empty());
REQUIRE(queue2.size() == 10);
CHECK_EQ(queue2.capacity(), 11);
CHECK(!queue2.is_contiguous());
for (int j = 0; j < 10; j++)
CHECK_EQ(queue2[j], testStrings[j]);
// Check copy assignment
Luau::VecDeque<std::string> queue3;
queue3 = queue;
REQUIRE(!queue3.empty());
REQUIRE(queue3.size() == 10);
CHECK_EQ(queue3.capacity(), 11);
CHECK(queue3.is_contiguous());
for (int j = 0; j < 10; j++)
CHECK_EQ(queue3[j], testStrings[j]);
// Check move construction
Luau::VecDeque<std::string> queue4 = std::move(queue);
REQUIRE(!queue4.empty());
REQUIRE(queue4.size() == 10);
CHECK_EQ(queue4.capacity(), 11);
CHECK(!queue4.is_contiguous());
for (int j = 0; j < 10; j++)
CHECK_EQ(queue4[j], testStrings[j]);
// Check move assignment
Luau::VecDeque<std::string> queue5;
queue5 = std::move(queue2);
REQUIRE(!queue5.empty());
REQUIRE(queue5.size() == 10);
CHECK_EQ(queue5.capacity(), 11);
CHECK(!queue5.is_contiguous());
for (int j = 0; j < 10; j++)
CHECK_EQ(queue5[j], testStrings[j]);
// Check that grow from discontiguous is handled well
queue4.push_back("zero");
queue4.push_back("?");
for (int j = 0; j < 10; j++)
CHECK_EQ(queue4[j], testStrings[j]);
CHECK_EQ(queue4[10], "zero");
CHECK_EQ(queue4[11], "?");
// Check that reserve from discontiguous is handled well
queue5.reserve(20);
for (int j = 0; j < 10; j++)
CHECK_EQ(queue5[j], testStrings[j]);
}
}
TEST_CASE("shrink_to_fit_works_with_strings")
{
for (size_t stringSet = 0; stringSet < 2; stringSet++)
{
auto testStrings = testStringSet[stringSet];
// initial capacity is not set, so this should grow to be 11
Luau::VecDeque<std::string> queue{};
@ -593,6 +766,7 @@ TEST_CASE("shrink_to_fit_works_with_strings")
for (int j = 0; j < 10; j++)
CHECK_EQ(queue[j], testStrings[j]);
}
}
struct TestStruct
{
@ -610,6 +784,11 @@ TEST_CASE("push_front_elements_are_destroyed_correctly")
queue.push_front(t);
REQUIRE(t.use_count() == 3); // Num of references to the TestStruct instance is now 3
// <-- call destructor here
// Extra check for correct copies
Luau::VecDeque<std::shared_ptr<TestStruct>> queue2 = queue;
Luau::VecDeque<std::shared_ptr<TestStruct>> queue3;
queue3 = queue;
}
// At this point the destructor should be called and we should be back down to one instance of TestStruct

View File

@ -2,24 +2,12 @@ AnnotationTests.typeof_expr
AstQuery.last_argument_function_call_type
AutocompleteTest.anonymous_autofilled_generic_on_argument_type_pack_vararg
AutocompleteTest.anonymous_autofilled_generic_type_pack_vararg
AutocompleteTest.autocomplete_interpolated_string_as_singleton
AutocompleteTest.autocomplete_response_perf1
AutocompleteTest.autocomplete_string_singleton_equality
AutocompleteTest.autocomplete_string_singleton_escape
AutocompleteTest.autocomplete_string_singletons
AutocompleteTest.do_wrong_compatible_nonself_calls
AutocompleteTest.suggest_external_module_type
AutocompleteTest.type_correct_expected_argument_type_pack_suggestion
AutocompleteTest.type_correct_expected_argument_type_suggestion
AutocompleteTest.type_correct_expected_argument_type_suggestion_optional
AutocompleteTest.type_correct_expected_argument_type_suggestion_self
AutocompleteTest.type_correct_expected_return_type_pack_suggestion
AutocompleteTest.type_correct_expected_return_type_suggestion
AutocompleteTest.type_correct_function_no_parenthesis
AutocompleteTest.type_correct_function_return_types
AutocompleteTest.type_correct_keywords
AutocompleteTest.type_correct_suggestion_for_overloads
AutocompleteTest.type_correct_suggestion_in_argument
BuiltinTests.aliased_string_format
BuiltinTests.assert_removes_falsy_types
BuiltinTests.assert_removes_falsy_types_even_from_type_pack_tail_but_only_for_the_first_type
@ -110,6 +98,8 @@ GenericsTests.bound_tables_do_not_clone_original_fields
GenericsTests.check_generic_function
GenericsTests.check_generic_local_function
GenericsTests.check_mutual_generic_functions
GenericsTests.check_mutual_generic_functions_unannotated
GenericsTests.check_mutual_generic_functions_errors
GenericsTests.check_nested_generic_function
GenericsTests.check_recursive_generic_function
GenericsTests.correctly_instantiate_polymorphic_member_functions
@ -139,7 +129,6 @@ GenericsTests.infer_generic_local_function
GenericsTests.infer_generic_property
GenericsTests.infer_nested_generic_function
GenericsTests.inferred_local_vars_can_be_polytypes
GenericsTests.instantiate_cyclic_generic_function
GenericsTests.instantiated_function_argument_names
GenericsTests.local_vars_can_be_polytypes
GenericsTests.no_stack_overflow_from_quantifying
@ -347,7 +336,6 @@ TableTests.table_unifies_into_map
TableTests.top_table_type
TableTests.type_mismatch_on_massive_table_is_cut_short
TableTests.unification_of_unions_in_a_self_referential_type
TableTests.unifying_tables_shouldnt_uaf1
TableTests.used_colon_instead_of_dot
TableTests.used_dot_instead_of_colon
TableTests.when_augmenting_an_unsealed_table_with_an_indexer_apply_the_correct_scope_to_the_indexer_type
@ -411,7 +399,6 @@ TypeInfer.no_stack_overflow_from_isoptional
TypeInfer.promote_tail_type_packs
TypeInfer.recursive_function_that_invokes_itself_with_a_refinement_of_its_parameter
TypeInfer.recursive_function_that_invokes_itself_with_a_refinement_of_its_parameter_2
TypeInfer.statements_are_topologically_sorted
TypeInfer.stringify_nested_unions_with_optionals
TypeInfer.tc_after_error_recovery_no_replacement_name_in_error
TypeInfer.type_infer_recursion_limit_no_ice
@ -445,7 +432,6 @@ TypeInferClasses.unions_of_intersections_of_classes
TypeInferClasses.we_can_report_when_someone_is_trying_to_use_a_table_rather_than_a_class
TypeInferFunctions.another_other_higher_order_function
TypeInferFunctions.calling_function_with_anytypepack_doesnt_leak_free_types
TypeInferFunctions.check_function_bodies
TypeInferFunctions.complicated_return_types_require_an_explicit_annotation
TypeInferFunctions.concrete_functions_are_not_supertypes_of_function
TypeInferFunctions.dont_assert_when_the_tarjan_limit_is_exceeded_during_generalization
@ -476,15 +462,12 @@ TypeInferFunctions.infer_generic_function_function_argument_overloaded
TypeInferFunctions.infer_generic_lib_function_function_argument
TypeInferFunctions.infer_return_type_from_selected_overload
TypeInferFunctions.infer_return_value_type
TypeInferFunctions.infer_that_function_does_not_return_a_table
TypeInferFunctions.inferred_higher_order_functions_are_quantified_at_the_right_time3
TypeInferFunctions.instantiated_type_packs_must_have_a_non_null_scope
TypeInferFunctions.list_all_overloads_if_no_overload_takes_given_argument_count
TypeInferFunctions.list_only_alternative_overloads_that_match_argument_count
TypeInferFunctions.luau_subtyping_is_np_hard
TypeInferFunctions.mutual_recursion
TypeInferFunctions.no_lossy_function_type
TypeInferFunctions.num_is_solved_after_num_or_str
TypeInferFunctions.occurs_check_failure_in_function_return_type
TypeInferFunctions.other_things_are_not_related_to_function
TypeInferFunctions.param_1_and_2_both_takes_the_same_generic_but_their_arguments_are_incompatible
@ -500,10 +483,8 @@ TypeInferFunctions.too_many_arguments
TypeInferFunctions.too_many_arguments_error_location
TypeInferFunctions.too_many_return_values_in_parentheses
TypeInferFunctions.too_many_return_values_no_function
TypeInferFunctions.toposort_doesnt_break_mutual_recursion
TypeInferLoops.cli_68448_iterators_need_not_accept_nil
TypeInferLoops.dcr_iteration_explore_raycast_minimization
TypeInferLoops.dcr_iteration_fragmented_keys
TypeInferLoops.dcr_iteration_on_never_gives_never
TypeInferLoops.dcr_xpath_candidates
TypeInferLoops.for_in_loop
@ -516,6 +497,7 @@ TypeInferLoops.for_in_loop_with_incompatible_args_to_iterator
TypeInferLoops.for_in_loop_with_next
TypeInferLoops.for_in_with_an_iterator_of_type_any
TypeInferLoops.for_in_with_generic_next
TypeInferLoops.for_in_with_just_one_iterator_is_ok
TypeInferLoops.for_loop
TypeInferLoops.ipairs_produces_integral_indices
TypeInferLoops.iterate_over_free_table
@ -545,7 +527,6 @@ TypeInferOOP.dont_suggest_using_colon_rather_than_dot_if_it_wont_help_2
TypeInferOOP.dont_suggest_using_colon_rather_than_dot_if_not_defined_with_colon
TypeInferOOP.inferred_methods_of_free_tables_have_the_same_level_as_the_enclosing_table
TypeInferOOP.inferring_hundreds_of_self_calls_should_not_suffocate_memory
TypeInferOOP.method_depends_on_table
TypeInferOOP.methods_are_topologically_sorted
TypeInferOOP.object_constructor_can_refer_to_method_of_self
TypeInferOOP.promise_type_error_too_complex