Sync to upstream/release/638 (#1360)

New Solver

* Fix some type inference issues surrounding updates to upvalues eg

```luau
local x = 0

function f()
    x = x + 1
end
```

* User-defined type function progress
* Bugfixes for normalization of negated class types. eg `SomeClass &
(class & ~SomeClass)`
* Fixes to subtyping between tables and the top `table` type.

---------

Co-authored-by: Aaron Weiss <aaronweiss@roblox.com>
Co-authored-by: Vighnesh <vvijay@roblox.com>
Co-authored-by: Aviral Goel <agoel@roblox.com>
Co-authored-by: David Cope <dcope@roblox.com>
Co-authored-by: Vyacheslav Egorov <vegorov@roblox.com>
Co-authored-by: Junseo Yoo <jyoo@roblox.com>
This commit is contained in:
Andy Friesen 2024-08-09 10:18:20 -07:00 committed by GitHub
parent ce8495a69e
commit bfad1fa777
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
33 changed files with 695 additions and 332 deletions

View File

@ -72,7 +72,7 @@ struct AnyTypeSummary
std::optional<TypePackId> lookupPackAnnotation(AstTypePack* annotation, const Module* module);
TypeId checkForTypeFunctionInhabitance(const TypeId instance, const Location location);
enum Pattern: uint64_t
enum Pattern : uint64_t
{
Casts,
FuncArg,
@ -82,7 +82,8 @@ struct AnyTypeSummary
VarAny,
TableProp,
Alias,
Assign
Assign,
TypePk
};
struct TypeInfo

View File

@ -184,9 +184,9 @@ private:
DataFlowResult visitExpr(DfgScope* scope, AstExprInterpString* i);
DataFlowResult visitExpr(DfgScope* scope, AstExprError* error);
void visitLValue(DfgScope* scope, AstExpr* e, DefId incomingDef, bool isCompoundAssignment = false);
DefId visitLValue(DfgScope* scope, AstExprLocal* l, DefId incomingDef, bool isCompoundAssignment);
DefId visitLValue(DfgScope* scope, AstExprGlobal* g, DefId incomingDef, bool isCompoundAssignment);
void visitLValue(DfgScope* scope, AstExpr* e, DefId incomingDef);
DefId visitLValue(DfgScope* scope, AstExprLocal* l, DefId incomingDef);
DefId visitLValue(DfgScope* scope, AstExprGlobal* g, DefId incomingDef);
DefId visitLValue(DfgScope* scope, AstExprIndexName* i, DefId incomingDef);
DefId visitLValue(DfgScope* scope, AstExprIndexExpr* i, DefId incomingDef);
DefId visitLValue(DfgScope* scope, AstExprError* e, DefId incomingDef);

View File

@ -207,6 +207,7 @@ private:
SubtypingResult isCovariantWith(SubtypingEnvironment& env, const ClassType* subClass, const ClassType* superClass);
SubtypingResult isCovariantWith(SubtypingEnvironment& env, TypeId subTy, const ClassType* subClass, TypeId superTy, const TableType* superTable);
SubtypingResult isCovariantWith(SubtypingEnvironment& env, const FunctionType* subFunction, const FunctionType* superFunction);
SubtypingResult isCovariantWith(SubtypingEnvironment& env, const TableType* subTable, const PrimitiveType* superPrim);
SubtypingResult isCovariantWith(SubtypingEnvironment& env, const PrimitiveType* subPrim, const TableType* superTable);
SubtypingResult isCovariantWith(SubtypingEnvironment& env, const SingletonType* subSingleton, const TableType* superTable);

View File

@ -594,10 +594,21 @@ struct TypeFunctionInstanceType
std::vector<TypeId> typeArguments;
std::vector<TypePackId> packArguments;
TypeFunctionInstanceType(NotNull<const TypeFunction> function, std::vector<TypeId> typeArguments, std::vector<TypePackId> packArguments)
std::optional<AstName> userFuncName; // Name of the user-defined type function; only available for UDTFs
std::optional<AstExprFunction*> userFuncBody; // Body of the user-defined type function; only available for UDTFs
TypeFunctionInstanceType(
NotNull<const TypeFunction> function,
std::vector<TypeId> typeArguments,
std::vector<TypePackId> packArguments,
std::optional<AstName> userFuncName = std::nullopt,
std::optional<AstExprFunction*> userFuncBody = std::nullopt
)
: function(function)
, typeArguments(typeArguments)
, packArguments(packArguments)
, userFuncName(userFuncName)
, userFuncBody(userFuncBody)
{
}

View File

@ -32,6 +32,9 @@ struct TypeFunctionContext
// The constraint being reduced in this run of the reduction
const Constraint* constraint;
std::optional<AstName> userFuncName; // Name of the user-defined type function; only available for UDTFs
std::optional<AstExprFunction*> userFuncBody; // Body of the user-defined type function; only available for UDTFs
TypeFunctionContext(NotNull<ConstraintSolver> cs, NotNull<Scope> scope, NotNull<const Constraint> constraint)
: arena(cs->arena)
, builtins(cs->builtinTypes)
@ -156,6 +159,8 @@ struct BuiltinTypeFunctions
{
BuiltinTypeFunctions();
TypeFunction userFunc;
TypeFunction notFunc;
TypeFunction lenFunc;
TypeFunction unmFunc;

View File

@ -136,6 +136,7 @@ void AnyTypeSummary::visit(const Scope* scope, AstStatReturn* ret, const Module*
const Scope* retScope = findInnerMostScope(ret->location, module);
auto ctxNode = getNode(rootSrc, ret);
bool seenTP = false;
for (auto val : ret->list)
{
@ -160,7 +161,23 @@ void AnyTypeSummary::visit(const Scope* scope, AstStatReturn* ret, const Module*
typeInfo.push_back(ti);
}
}
if (ret->list.size > 1 && !seenTP)
{
if (containsAny(retScope->returnType))
{
seenTP = true;
TelemetryTypePair types;
types.inferredType = toString(retScope->returnType);
TypeInfo ti{Pattern::TypePk, toString(ctxNode), types};
typeInfo.push_back(ti);
}
}
}
}
void AnyTypeSummary::visit(const Scope* scope, AstStatLocal* local, const Module* module, NotNull<BuiltinTypes> builtinTypes)

View File

@ -271,6 +271,7 @@ void ConstraintGenerator::visitModuleRoot(AstStatBlock* block)
TypeId domainTy = builtinTypes->neverType;
for (TypeId d : domain)
{
d = follow(d);
if (d == ty)
continue;
domainTy = simplifyUnion(builtinTypes, arena, domainTy, d).result;
@ -663,6 +664,51 @@ ControlFlow ConstraintGenerator::visitBlockWithoutChildScope(const ScopePtr& sco
astTypeAliasDefiningScopes[alias] = defnScope;
aliasDefinitionLocations[alias->name.value] = alias->location;
}
else if (auto function = stat->as<AstStatTypeFunction>())
{
// If a type function w/ same name has already been defined, error for having duplicates
if (scope->exportedTypeBindings.count(function->name.value) || scope->privateTypeBindings.count(function->name.value))
{
auto it = aliasDefinitionLocations.find(function->name.value);
LUAU_ASSERT(it != aliasDefinitionLocations.end());
reportError(function->location, DuplicateTypeDefinition{function->name.value, it->second});
continue;
}
ScopePtr defnScope = childScope(function, scope);
// Create TypeFunctionInstanceType
std::vector<TypeId> typeParams;
typeParams.reserve(function->body->args.size);
std::vector<GenericTypeDefinition> quantifiedTypeParams;
quantifiedTypeParams.reserve(function->body->args.size);
for (size_t i = 0; i < function->body->args.size; i++)
{
std::string name = format("T%zu", i);
TypeId ty = arena->addType(GenericType{name});
typeParams.push_back(ty);
GenericTypeDefinition genericTy{ty};
quantifiedTypeParams.push_back(genericTy);
}
TypeId typeFunctionTy = arena->addType(TypeFunctionInstanceType{
NotNull{&builtinTypeFunctions().userFunc},
std::move(typeParams),
{},
function->name,
function->body,
});
TypeFun typeFunction{std::move(quantifiedTypeParams), typeFunctionTy};
// Set type bindings and definition locations for this user-defined type function
scope->privateTypeBindings[function->name.value] = std::move(typeFunction);
aliasDefinitionLocations[function->name.value] = function->location;
}
}
std::optional<ControlFlow> firstControlFlow;
@ -1368,6 +1414,20 @@ ControlFlow ConstraintGenerator::visit(const ScopePtr& scope, AstStatTypeAlias*
ControlFlow ConstraintGenerator::visit(const ScopePtr& scope, AstStatTypeFunction* function)
{
// If a type function with the same name was already defined, we skip over
auto bindingIt = scope->privateTypeBindings.find(function->name.value);
if (bindingIt == scope->privateTypeBindings.end())
return ControlFlow::None;
TypeFun typeFunction = bindingIt->second;
// Adding typeAliasExpansionConstraint on user-defined type function for the constraint solver
if (auto typeFunctionTy = get<TypeFunctionInstanceType>(typeFunction.type))
{
TypeId expansionTy = arena->addType(PendingExpansionType{{}, function->name, typeFunctionTy->typeArguments, typeFunctionTy->packArguments});
addConstraint(scope, function->location, TypeAliasExpansionConstraint{/* target */ expansionTy});
}
return ControlFlow::None;
}

View File

@ -924,6 +924,10 @@ bool ConstraintSolver::tryDispatch(const TypeAliasExpansionConstraint& c, NotNul
return true;
}
// Adding ReduceConstraint on type function for the constraint solver
if (auto typeFn = get<TypeFunctionInstanceType>(follow(tf->type)))
pushConstraint(NotNull(constraint->scope.get()), constraint->location, ReduceConstraint{tf->type});
// If there are no parameters to the type function we can just use the type
// directly.
if (tf->typeParams.empty() && tf->typePackParams.empty())
@ -1051,7 +1055,6 @@ bool ConstraintSolver::tryDispatch(const TypeAliasExpansionConstraint& c, NotNul
// there are e.g. generic saturatedTypeArguments that go unused.
const TableType* tfTable = getTableType(tf->type);
//clang-format off
bool needsClone = follow(tf->type) == target || (tfTable != nullptr && tfTable == getTableType(target)) ||
std::any_of(
typeArguments.begin(),
@ -1061,7 +1064,6 @@ bool ConstraintSolver::tryDispatch(const TypeAliasExpansionConstraint& c, NotNul
return other == target;
}
);
//clang-format on
// Only tables have the properties we're trying to set.
TableType* ttv = getMutableTableType(target);

View File

@ -570,15 +570,8 @@ ControlFlow DataFlowGraphBuilder::visit(DfgScope* scope, AstStatAssign* a)
ControlFlow DataFlowGraphBuilder::visit(DfgScope* scope, AstStatCompoundAssign* c)
{
// TODO: This needs revisiting because this is incorrect. The `c->var` part is both being read and written to,
// but the `c->var` only has one pointer address, so we need to come up with a way to store both.
// For now, it's not important because we don't have type states, but it is going to be important, e.g.
//
// local a = 5 -- a-1
// a += 5 -- a-2 = a-1 + 5
// We can't just visit `c->var` as a rvalue and then separately traverse `c->var` as an lvalue, since that's O(n^2).
DefId def = visitExpr(scope, c->value).def;
visitLValue(scope, c->var, def, /* isCompoundAssignment */ true);
(void) visitExpr(scope, c->value);
(void) visitExpr(scope, c->var);
return ControlFlow::None;
}
@ -920,14 +913,14 @@ DataFlowResult DataFlowGraphBuilder::visitExpr(DfgScope* scope, AstExprError* er
return {defArena->freshCell(), nullptr};
}
void DataFlowGraphBuilder::visitLValue(DfgScope* scope, AstExpr* e, DefId incomingDef, bool isCompoundAssignment)
void DataFlowGraphBuilder::visitLValue(DfgScope* scope, AstExpr* e, DefId incomingDef)
{
auto go = [&]()
{
if (auto l = e->as<AstExprLocal>())
return visitLValue(scope, l, incomingDef, isCompoundAssignment);
return visitLValue(scope, l, incomingDef);
else if (auto g = e->as<AstExprGlobal>())
return visitLValue(scope, g, incomingDef, isCompoundAssignment);
return visitLValue(scope, g, incomingDef);
else if (auto i = e->as<AstExprIndexName>())
return visitLValue(scope, i, incomingDef);
else if (auto i = e->as<AstExprIndexExpr>())
@ -941,15 +934,8 @@ void DataFlowGraphBuilder::visitLValue(DfgScope* scope, AstExpr* e, DefId incomi
graph.astDefs[e] = go();
}
DefId DataFlowGraphBuilder::visitLValue(DfgScope* scope, AstExprLocal* l, DefId incomingDef, bool isCompoundAssignment)
DefId DataFlowGraphBuilder::visitLValue(DfgScope* scope, AstExprLocal* l, DefId incomingDef)
{
// We need to keep the previous def around for a compound assignment.
if (isCompoundAssignment)
{
DefId def = lookup(scope, l->local);
graph.compoundAssignDefs[l] = def;
}
// In order to avoid alias tracking, we need to clip the reference to the parent def.
if (scope->canUpdateDefinition(l->local))
{
@ -962,15 +948,8 @@ DefId DataFlowGraphBuilder::visitLValue(DfgScope* scope, AstExprLocal* l, DefId
return visitExpr(scope, static_cast<AstExpr*>(l)).def;
}
DefId DataFlowGraphBuilder::visitLValue(DfgScope* scope, AstExprGlobal* g, DefId incomingDef, bool isCompoundAssignment)
DefId DataFlowGraphBuilder::visitLValue(DfgScope* scope, AstExprGlobal* g, DefId incomingDef)
{
// We need to keep the previous def around for a compound assignment.
if (isCompoundAssignment)
{
DefId def = lookup(scope, g->name);
graph.compoundAssignDefs[g] = def;
}
// In order to avoid alias tracking, we need to clip the reference to the parent def.
if (scope->canUpdateDefinition(g->name))
{

View File

@ -2186,6 +2186,11 @@ void Normalizer::intersectClasses(NormalizedClassType& heres, const NormalizedCl
if (isSubclass(thereTy, hereTy))
{
// If thereTy is a subtype of hereTy, we need to replace hereTy
// by thereTy and combine their negation lists.
//
// If any types in the negation list are not subtypes of
// thereTy, they need to be removed from the negation list.
TypeIds negations = std::move(hereNegations);
for (auto nIt = negations.begin(); nIt != negations.end();)
@ -2209,22 +2214,45 @@ void Normalizer::intersectClasses(NormalizedClassType& heres, const NormalizedCl
}
else if (isSubclass(hereTy, thereTy))
{
// If thereTy is a supertype of hereTy, we need to extend the
// negation list of hereTy by that of thereTy.
//
// If any of the types of thereTy's negations are not subtypes
// of hereTy, they must not be added to hereTy's negation list.
//
// If any of the types of thereTy's negations are supertypes of
// hereTy, then hereTy must be removed entirely.
//
// If any of the types of thereTy's negations are supertypes of
// the negations of herety, the former must supplant the latter.
TypeIds negations = thereNegations;
bool erasedHere = false;
for (auto nIt = negations.begin(); nIt != negations.end();)
{
if (!isSubclass(*nIt, hereTy))
if (isSubclass(hereTy, *nIt))
{
nIt = negations.erase(nIt);
}
else
{
++nIt;
}
// eg SomeClass & (class & ~SomeClass)
// or SomeClass & (class & ~ParentClass)
heres.classes.erase(hereTy);
it = heres.ordering.erase(it);
erasedHere = true;
break;
}
// eg SomeClass & (class & ~Unrelated)
if (!isSubclass(*nIt, hereTy))
nIt = negations.erase(nIt);
else
++nIt;
}
if (!erasedHere)
{
unionClasses(hereNegations, negations);
break;
++it;
}
}
else if (hereTy == thereTy)
{

View File

@ -230,7 +230,7 @@ std::pair<OverloadResolver::Analysis, ErrorVec> OverloadResolver::checkOverload_
// function arguments are options, then this function call
// is ok.
const size_t firstUnsatisfiedArgument = argExprs->size();
const size_t firstUnsatisfiedArgument = args->head.size();
const auto [requiredHead, _requiredTail] = flatten(fn->argTypes);
// If too many arguments were supplied, this overload

View File

@ -1144,7 +1144,7 @@ std::optional<TypeId> TypeSimplifier::basicIntersect(TypeId left, TypeId right)
return left;
bool areDisjoint = true;
for (const auto& [name, leftProp]: lt->props)
for (const auto& [name, leftProp] : lt->props)
{
if (rt->props.count(name))
{
@ -1156,16 +1156,10 @@ std::optional<TypeId> TypeSimplifier::basicIntersect(TypeId left, TypeId right)
if (areDisjoint)
{
TableType::Props mergedProps = lt->props;
for (const auto& [name, rightProp]: rt->props)
for (const auto& [name, rightProp] : rt->props)
mergedProps[name] = rightProp;
return arena->addType(TableType{
mergedProps,
std::nullopt,
TypeLevel{},
lt->scope,
TableState::Sealed
});
return arena->addType(TableType{mergedProps, std::nullopt, TypeLevel{}, lt->scope, TableState::Sealed});
}
}

View File

@ -128,7 +128,7 @@ static TypeId shallowClone(TypeId ty, TypeArena& dest, const TxnLog* log, bool a
return dest.addType(NegationType{a.ty});
else if constexpr (std::is_same_v<T, TypeFunctionInstanceType>)
{
TypeFunctionInstanceType clone{a.function, a.typeArguments, a.packArguments};
TypeFunctionInstanceType clone{a.function, a.typeArguments, a.packArguments, a.userFuncName, a.userFuncBody};
return dest.addType(std::move(clone));
}
else

View File

@ -639,6 +639,8 @@ SubtypingResult Subtyping::isCovariantWith(SubtypingEnvironment& env, TypeId sub
result = isCovariantWith(env, p);
else if (auto p = get2<ClassType, TableType>(subTy, superTy))
result = isCovariantWith(env, subTy, p.first, superTy, p.second);
else if (auto p = get2<TableType, PrimitiveType>(subTy, superTy))
result = isCovariantWith(env, p);
else if (auto p = get2<PrimitiveType, TableType>(subTy, superTy))
result = isCovariantWith(env, p);
else if (auto p = get2<SingletonType, TableType>(subTy, superTy))
@ -1368,6 +1370,15 @@ SubtypingResult Subtyping::isCovariantWith(SubtypingEnvironment& env, const Func
return result;
}
SubtypingResult Subtyping::isCovariantWith(SubtypingEnvironment& env, const TableType* subTable, const PrimitiveType* superPrim)
{
SubtypingResult result{false};
if (superPrim->type == PrimitiveType::Table)
result.isSubtype = true;
return result;
}
SubtypingResult Subtyping::isCovariantWith(SubtypingEnvironment& env, const PrimitiveType* subPrim, const TableType* superTable)
{
SubtypingResult result{false};
@ -1387,6 +1398,11 @@ SubtypingResult Subtyping::isCovariantWith(SubtypingEnvironment& env, const Prim
}
}
}
else if (subPrim->type == PrimitiveType::Table)
{
const bool isSubtype = superTable->props.empty() && !superTable->indexer.has_value();
return {isSubtype};
}
return result;
}

View File

@ -1036,6 +1036,9 @@ struct TypeStringifier
void operator()(TypeId, const TypeFunctionInstanceType& tfitv)
{
if (tfitv.userFuncName) // Special stringification for user-defined type functions
state.emit(tfitv.userFuncName->value);
else
state.emit(tfitv.function->name);
state.emit("<");

View File

@ -358,6 +358,9 @@ struct TypeFunctionReducer
if (tryGuessing(subject))
return;
ctx.userFuncName = tfit->userFuncName;
ctx.userFuncBody = tfit->userFuncBody;
TypeFunctionReductionResult<TypeId> result = tfit->function->reducer(subject, tfit->typeArguments, tfit->packArguments, NotNull{&ctx});
handleTypeFunctionReduction(subject, result);
}
@ -567,6 +570,24 @@ static std::optional<TypeFunctionReductionResult<TypeId>> tryDistributeTypeFunct
return std::nullopt;
}
TypeFunctionReductionResult<TypeId> userDefinedTypeFunction(
TypeId instance,
const std::vector<TypeId>& typeParams,
const std::vector<TypePackId>& packParams,
NotNull<TypeFunctionContext> ctx
)
{
if (!ctx->userFuncName || !ctx->userFuncBody)
{
ctx->ice->ice("all user-defined type functions must have an associated function definition");
return {std::nullopt, true, {}, {}};
}
// TODO: implementation of user-defined type functions goes here
return {std::nullopt, true, {}, {}};
}
TypeFunctionReductionResult<TypeId> notTypeFunction(
TypeId instance,
const std::vector<TypeId>& typeParams,
@ -2253,7 +2274,8 @@ TypeFunctionReductionResult<TypeId> rawgetTypeFunction(
}
BuiltinTypeFunctions::BuiltinTypeFunctions()
: notFunc{"not", notTypeFunction}
: userFunc{"user", userDefinedTypeFunction}
, notFunc{"not", notTypeFunction}
, lenFunc{"len", lenTypeFunction}
, unmFunc{"unm", unmTypeFunction}
, addFunc{"add", addTypeFunction}

View File

@ -2243,15 +2243,24 @@ std::optional<AstExprBinary::Op> Parser::checkBinaryConfusables(const BinaryOpPr
// where `binop' is any binary operator with a priority higher than `limit'
AstExpr* Parser::parseExpr(unsigned int limit)
{
// clang-format off
static const BinaryOpPriority binaryPriority[] = {
{6, 6}, {6, 6}, {7, 7}, {7, 7}, {7, 7}, {7, 7}, // `+' `-' `*' `/' `//' `%'
{10, 9}, {5, 4}, // power and concat (right associative)
{3, 3}, {3, 3}, // equality and inequality
{3, 3}, {3, 3}, {3, 3}, {3, 3}, // order
{2, 2}, {1, 1} // logical (and/or)
{6, 6}, // '+'
{6, 6}, // '-'
{7, 7}, // '*'
{7, 7}, // '/'
{7, 7}, // '//'
{7, 7}, // `%'
{10, 9}, // power (right associative)
{5, 4}, // concat (right associative)
{3, 3}, // inequality
{3, 3}, // equality
{3, 3}, // '<'
{3, 3}, // '<='
{3, 3}, // '>'
{3, 3}, // '>='
{2, 2}, // logical and
{1, 1} // logical or
};
// clang-format on
static_assert(sizeof(binaryPriority) / sizeof(binaryPriority[0]) == size_t(AstExprBinary::Op__Count), "binaryPriority needs an entry per op");

View File

@ -70,6 +70,53 @@ export type t8<t8> = t0 &(<t0 ...>(true | any)->(''))
}
}
TEST_CASE_FIXTURE(ATSFixture, "typepacks")
{
fileResolver.source["game/Gui/Modules/A"] = R"(
local function fallible(t: number): ...any
if t > 0 then
return true, t -- should catch this
end
return false, "must be positive" -- should catch this
end
)";
CheckResult result1 = frontend.check("game/Gui/Modules/A");
LUAU_REQUIRE_NO_ERRORS(result1);
ModulePtr module = frontend.moduleResolver.getModule("game/Gui/Modules/A");
if (FFlag::StudioReportLuauAny)
{
LUAU_ASSERT(module->ats.typeInfo.size() == 3);
LUAU_ASSERT(module->ats.typeInfo[1].code == Pattern::TypePk);
LUAU_ASSERT(module->ats.typeInfo[0].node == "local function fallible(t: number): ...any\n if t > 0 then\n return true, t\n end\n return false, 'must be positive'\nend");
}
}
TEST_CASE_FIXTURE(ATSFixture, "typepacks_no_ret")
{
fileResolver.source["game/Gui/Modules/A"] = R"(
-- TODO: if partially typed, we'd want to know too
local function fallible(t: number)
if t > 0 then
return true, t
end
return false, "must be positive"
end
)";
CheckResult result1 = frontend.check("game/Gui/Modules/A");
LUAU_REQUIRE_ERROR_COUNT(1, result1);
ModulePtr module = frontend.moduleResolver.getModule("game/Gui/Modules/A");
if (FFlag::StudioReportLuauAny)
{
LUAU_ASSERT(module->ats.typeInfo.size() == 0);
}
}
TEST_CASE_FIXTURE(ATSFixture, "var_typepack_any_gen_table")
{
fileResolver.source["game/Gui/Modules/A"] = R"(
@ -223,7 +270,10 @@ end
{
LUAU_ASSERT(module->ats.typeInfo.size() == 1);
LUAU_ASSERT(module->ats.typeInfo[0].code == Pattern::FuncArg);
LUAU_ASSERT(module->ats.typeInfo[0].node == "function f(x: any)\nif not x then\nx = {\n y = math.random(0, 2^31-1),\n left = nil,\n right = nil\n}\nelse\n local expected = x * 5\nend\nend");
LUAU_ASSERT(
module->ats.typeInfo[0].node == "function f(x: any)\nif not x then\nx = {\n y = math.random(0, 2^31-1),\n left = nil,\n right = "
"nil\n}\nelse\n local expected = x * 5\nend\nend"
);
}
}
@ -478,7 +528,13 @@ initialize()
{
LUAU_ASSERT(module->ats.typeInfo.size() == 11);
LUAU_ASSERT(module->ats.typeInfo[0].code == Pattern::FuncArg);
LUAU_ASSERT(module->ats.typeInfo[0].node == "local function onCharacterAdded(character: Model)\n\n character.DescendantAdded:Connect(function(descendant)\n if descendant:IsA('BasePart')then\n descendant.CollisionGroup = CHARACTER_COLLISION_GROUP\n end\n end)\n\n\n for _, descendant in character:GetDescendants()do\n if descendant:IsA('BasePart')then\n descendant.CollisionGroup = CHARACTER_COLLISION_GROUP\n end\n end\nend");
LUAU_ASSERT(
module->ats.typeInfo[0].node ==
"local function onCharacterAdded(character: Model)\n\n character.DescendantAdded:Connect(function(descendant)\n if "
"descendant:IsA('BasePart')then\n descendant.CollisionGroup = CHARACTER_COLLISION_GROUP\n end\n end)\n\n\n for _, descendant in "
"character:GetDescendants()do\n if descendant:IsA('BasePart')then\n descendant.CollisionGroup = CHARACTER_COLLISION_GROUP\n end\n "
"end\nend"
);
}
}
@ -541,7 +597,14 @@ initialize()
{
LUAU_ASSERT(module->ats.typeInfo.size() == 7);
LUAU_ASSERT(module->ats.typeInfo[0].code == Pattern::FuncArg);
LUAU_ASSERT(module->ats.typeInfo[0].node == "local function setupKiosk(kiosk: Model)\n local spawnLocation = kiosk:FindFirstChild('SpawnLocation')\n assert(spawnLocation, `{kiosk:GetFullName()} has no SpawnLocation part`)\n local promptPart = kiosk:FindFirstChild('Prompt')\n assert(promptPart, `{kiosk:GetFullName()} has no Prompt part`)\n\n\n spawnLocation.Transparency = 1\n\n\n local spawnPrompt = spawnPromptTemplate:Clone()\n spawnPrompt.Parent = promptPart\n\n spawnPrompt.Triggered:Connect(function(player: Player)\n\n destroyPlayerCars(player)\n\n spawnCar(spawnLocation.CFrame, player)\n end)\nend");
LUAU_ASSERT(
module->ats.typeInfo[0].node ==
"local function setupKiosk(kiosk: Model)\n local spawnLocation = kiosk:FindFirstChild('SpawnLocation')\n assert(spawnLocation, "
"`{kiosk:GetFullName()} has no SpawnLocation part`)\n local promptPart = kiosk:FindFirstChild('Prompt')\n assert(promptPart, "
"`{kiosk:GetFullName()} has no Prompt part`)\n\n\n spawnLocation.Transparency = 1\n\n\n local spawnPrompt = "
"spawnPromptTemplate:Clone()\n spawnPrompt.Parent = promptPart\n\n spawnPrompt.Triggered:Connect(function(player: Player)\n\n "
"destroyPlayerCars(player)\n\n spawnCar(spawnLocation.CFrame, player)\n end)\nend"
);
}
}

View File

@ -13,6 +13,7 @@
#include "Luau/Scope.h"
#include "Luau/ToString.h"
#include "Luau/Type.h"
#include "Luau/TypeFunction.h"
#include "IostreamOptional.h"
#include "ScopedFlags.h"

View File

@ -847,6 +847,8 @@ TEST_CASE_FIXTURE(NormalizeFixture, "negations_of_classes")
"(Parent | Unrelated | boolean | buffer | function | number | string | table | thread)?" ==
toString(normal("Not<cls & Not<Parent> & Not<Child> & Not<Unrelated>>"))
);
CHECK("Child" == toString(normal("(Child | Unrelated) & Not<Unrelated>")));
}
TEST_CASE_FIXTURE(NormalizeFixture, "classes_and_unknown")
@ -998,17 +1000,13 @@ TEST_CASE_FIXTURE(NormalizeFixture, "truthy_table_property_and_optional_table_wi
ScopedFastFlag sff{FFlag::DebugLuauDeferredConstraintResolution, true};
// { x: ~(false?) }
TypeId t1 = arena.addType(TableType{
TableType::Props{{"x", builtinTypes->truthyType}}, std::nullopt, TypeLevel{}, TableState::Sealed
});
TypeId t1 = arena.addType(TableType{TableType::Props{{"x", builtinTypes->truthyType}}, std::nullopt, TypeLevel{}, TableState::Sealed});
// { x: number? }?
TypeId t2 = arena.addType(UnionType{{
arena.addType(TableType{
TableType::Props{{"x", builtinTypes->optionalNumberType}}, std::nullopt, TypeLevel{}, TableState::Sealed
}),
builtinTypes->nilType
}});
TypeId t2 = arena.addType(UnionType{
{arena.addType(TableType{TableType::Props{{"x", builtinTypes->optionalNumberType}}, std::nullopt, TypeLevel{}, TableState::Sealed}),
builtinTypes->nilType}
});
TypeId intersection = arena.addType(IntersectionType{{t2, t1}});

View File

@ -2085,7 +2085,6 @@ TEST_CASE_FIXTURE(Fixture, "class_indexer")
TEST_CASE_FIXTURE(Fixture, "parse_variadics")
{
//clang-format off
AstStatBlock* stat = parseEx(R"(
function foo(bar, ...: number): ...string
end
@ -2094,7 +2093,6 @@ TEST_CASE_FIXTURE(Fixture, "parse_variadics")
type Bar = () -> (number, ...boolean)
)")
.root;
//clang-format on
REQUIRE(stat);
REQUIRE_EQ(stat->body.size, 3);

View File

@ -893,6 +893,9 @@ TEST_CASE_FIXTURE(SubtypeFixture, "{ @metatable { x: number } } <!: { x: number
CHECK_IS_NOT_SUBTYPE(meta({{"x", builtinTypes->numberType}}), tbl({{"x", builtinTypes->numberType}}));
}
TEST_IS_SUBTYPE(builtinTypes->tableType, tbl({}));
TEST_IS_SUBTYPE(tbl({}), builtinTypes->tableType);
// Negated subtypes
TEST_IS_NOT_SUBTYPE(negate(builtinTypes->neverType), builtinTypes->stringType);
TEST_IS_SUBTYPE(negate(builtinTypes->unknownType), builtinTypes->stringType);
@ -1213,7 +1216,8 @@ TEST_CASE_FIXTURE(SubtypeFixture, "(...any) -> () <: <T>(T...) -> ()")
// See https://github.com/luau-lang/luau/issues/767
TEST_CASE_FIXTURE(SubtypeFixture, "(...unknown) -> () <: <T>(T...) -> ()")
{
TypeId unknownsToNothing = arena.addType(FunctionType{arena.addTypePack(VariadicTypePack{builtinTypes->unknownType}), builtinTypes->emptyTypePack});
TypeId unknownsToNothing =
arena.addType(FunctionType{arena.addTypePack(VariadicTypePack{builtinTypes->unknownType}), builtinTypes->emptyTypePack});
TypeId genericTToAnys = arena.addType(FunctionType{genericAs, builtinTypes->emptyTypePack});
CHECK_MESSAGE(subtyping.isSubtype(unknownsToNothing, genericTToAnys).isSubtype, "(...unknown) -> () <: <T>(T...) -> ()");
@ -1222,25 +1226,11 @@ TEST_CASE_FIXTURE(SubtypeFixture, "(...unknown) -> () <: <T>(T...) -> ()")
TEST_CASE_FIXTURE(SubtypeFixture, "bill")
{
TypeId a = arena.addType(TableType{
{{"a", builtinTypes->stringType}},
TableIndexer{
builtinTypes->stringType,
builtinTypes->numberType
},
TypeLevel{},
nullptr,
TableState::Sealed
{{"a", builtinTypes->stringType}}, TableIndexer{builtinTypes->stringType, builtinTypes->numberType}, TypeLevel{}, nullptr, TableState::Sealed
});
TypeId b = arena.addType(TableType{
{{"a", builtinTypes->stringType}},
TableIndexer{
builtinTypes->stringType,
builtinTypes->numberType
},
TypeLevel{},
nullptr,
TableState::Sealed
{{"a", builtinTypes->stringType}}, TableIndexer{builtinTypes->stringType, builtinTypes->numberType}, TypeLevel{}, nullptr, TableState::Sealed
});
CHECK(subtyping.isSubtype(a, b).isSubtype);
@ -1250,22 +1240,17 @@ TEST_CASE_FIXTURE(SubtypeFixture, "bill")
// TEST_CASE_FIXTURE(SubtypeFixture, "({[string]: number, a: string}) -> () <: ({[string]: number, a: string}) -> ()")
TEST_CASE_FIXTURE(SubtypeFixture, "fred")
{
auto makeTheType = [&]() {
auto makeTheType = [&]()
{
TypeId argType = arena.addType(TableType{
{{"a", builtinTypes->stringType}},
TableIndexer{
builtinTypes->stringType,
builtinTypes->numberType
},
TableIndexer{builtinTypes->stringType, builtinTypes->numberType},
TypeLevel{},
nullptr,
TableState::Sealed
});
return arena.addType(FunctionType {
arena.addTypePack({argType}),
builtinTypes->emptyTypePack
});
return arena.addType(FunctionType{arena.addTypePack({argType}), builtinTypes->emptyTypePack});
};
TypeId a = makeTheType();

View File

@ -159,8 +159,10 @@ n4 [label="VariadicTypePack 4"];
n4 -> n5;
n5 [label="string"];
n1 -> n6 [label="ret"];
n6 [label="TypePack 6"];
n6 -> n3;
n6 [label="BoundTypePack 6"];
n6 -> n7;
n7 [label="TypePack 7"];
n7 -> n3;
})",
toDot(requireType("f"), opts)
);

View File

@ -13,6 +13,7 @@ using namespace Luau;
LUAU_FASTFLAG(LuauRecursiveTypeParameterRestriction);
LUAU_FASTFLAG(DebugLuauDeferredConstraintResolution);
LUAU_FASTFLAG(LuauAttributeSyntax);
LUAU_FASTFLAG(LuauUserDefinedTypeFunctions)
TEST_SUITE_BEGIN("ToString");
@ -21,8 +22,13 @@ TEST_CASE_FIXTURE(Fixture, "primitive")
CheckResult result = check("local a = nil local b = 44 local c = 'lalala' local d = true");
LUAU_REQUIRE_NO_ERRORS(result);
if (FFlag::DebugLuauDeferredConstraintResolution)
CHECK("nil" == toString(requireType("a")));
else
{
// A variable without an annotation and with a nil literal should infer as 'free', not 'nil'
CHECK_NE("nil", toString(requireType("a")));
}
CHECK_EQ("number", toString(requireType("b")));
CHECK_EQ("string", toString(requireType("c")));
@ -39,6 +45,8 @@ TEST_CASE_FIXTURE(Fixture, "bound_types")
TEST_CASE_FIXTURE(Fixture, "free_types")
{
ScopedFastFlag sff{FFlag::DebugLuauDeferredConstraintResolution, false};
CheckResult result = check("local a");
LUAU_REQUIRE_NO_ERRORS(result);
@ -95,7 +103,6 @@ TEST_CASE_FIXTURE(Fixture, "table_respects_use_line_break")
ToStringOptions opts;
opts.useLineBreaks = true;
//clang-format off
if (FFlag::DebugLuauDeferredConstraintResolution)
CHECK_EQ(
"{\n"
@ -114,7 +121,6 @@ TEST_CASE_FIXTURE(Fixture, "table_respects_use_line_break")
"|}",
toString(requireType("a"), opts)
);
//clang-format on
}
TEST_CASE_FIXTURE(Fixture, "nil_or_nil_is_nil_not_question_mark")
@ -160,6 +166,8 @@ TEST_CASE_FIXTURE(Fixture, "named_metatable")
TEST_CASE_FIXTURE(BuiltinsFixture, "named_metatable_toStringNamedFunction")
{
ScopedFastFlag sff{FFlag::DebugLuauDeferredConstraintResolution, false};
CheckResult result = check(R"(
local function createTbl(): NamedMetatable
return setmetatable({}, {})
@ -199,14 +207,24 @@ TEST_CASE_FIXTURE(BuiltinsFixture, "exhaustive_toString_of_cyclic_table")
CHECK_EQ(std::string::npos, a.find("CYCLE"));
CHECK_EQ(std::string::npos, a.find("TRUNCATED"));
//clang-format off
if (FFlag::DebugLuauDeferredConstraintResolution)
{
CHECK(
"t2 where "
"t1 = { __index: t1, __mul: ((t2, number) -> t2) & ((t2, t2) -> t2), new: () -> t2 } ; "
"t2 = { @metatable t1, { x: number, y: number, z: number } }" ==
a
);
}
else
{
CHECK_EQ(
"t2 where "
"t1 = { __index: t1, __mul: ((t2, number) -> t2) & ((t2, t2) -> t2), new: () -> t2 } ; "
"t2 = { @metatable t1, {| x: number, y: number, z: number |} }",
a
);
//clang-format on
}
}
@ -263,14 +281,12 @@ TEST_CASE_FIXTURE(Fixture, "complex_intersections_printed_on_multiple_lines")
opts.useLineBreaks = true;
opts.compositeTypesSingleLineLimit = 2;
//clang-format off
CHECK_EQ(
"boolean\n"
"& number\n"
"& string",
toString(requireType("a"), opts)
);
//clang-format on
}
TEST_CASE_FIXTURE(Fixture, "overloaded_functions_always_printed_on_multiple_lines")
@ -282,13 +298,11 @@ TEST_CASE_FIXTURE(Fixture, "overloaded_functions_always_printed_on_multiple_line
ToStringOptions opts;
opts.useLineBreaks = true;
//clang-format off
CHECK_EQ(
"((number) -> number)\n"
"& ((string) -> string)",
toString(requireType("a"), opts)
);
//clang-format on
}
TEST_CASE_FIXTURE(Fixture, "simple_unions_printed_on_one_line")
@ -313,14 +327,12 @@ TEST_CASE_FIXTURE(Fixture, "complex_unions_printed_on_multiple_lines")
opts.compositeTypesSingleLineLimit = 2;
opts.useLineBreaks = true;
//clang-format off
CHECK_EQ(
"boolean\n"
"| number\n"
"| string",
toString(requireType("a"), opts)
);
//clang-format on
}
TEST_CASE_FIXTURE(Fixture, "quit_stringifying_table_type_when_length_is_exceeded")
@ -582,6 +594,8 @@ TEST_CASE_FIXTURE(Fixture, "toStringDetailed")
TEST_CASE_FIXTURE(Fixture, "toStringErrorPack")
{
ScopedFastFlag sff{FFlag::DebugLuauDeferredConstraintResolution, false};
CheckResult result = check(R"(
local function target(callback: nil) return callback(4, "hello") end
)");
@ -666,6 +680,9 @@ TEST_CASE_FIXTURE(Fixture, "no_parentheses_around_cyclic_function_type_in_inters
LUAU_REQUIRE_NO_ERRORS(result);
if (FFlag::DebugLuauDeferredConstraintResolution)
CHECK("(() -> t1) & ((number) -> ()) where t1 = () -> t1" == toString(requireType("a")));
else
CHECK_EQ("((number) -> ()) & t1 where t1 = () -> t1", toString(requireType("a")));
}
@ -824,6 +841,11 @@ TEST_CASE_FIXTURE(Fixture, "pick_distinct_names_for_mixed_explicit_and_implicit_
function foo<a>(x: a, y) end
)");
if (FFlag::DebugLuauDeferredConstraintResolution)
{
CHECK("<a>(a, 'b) -> ()" == toString(requireType("foo")));
}
else
CHECK("<a, b>(a, b) -> ()" == toString(requireType("foo")));
}
@ -934,4 +956,21 @@ TEST_CASE_FIXTURE(Fixture, "cycle_rooted_in_a_pack")
CHECK("tp1 where tp1 = {| BaseField: unknown, BaseMethod: (tp1) -> () |}, number" == toString(thePack));
}
TEST_CASE_FIXTURE(Fixture, "correct_stringification_user_defined_type_functions")
{
TypeFunction user{"user", nullptr};
TypeFunctionInstanceType tftt{
NotNull{&user},
std::vector<TypeId>{builtinTypes->numberType}, // Type Function Arguments
{},
{AstName{"woohoo"}}, // Type Function Name
std::nullopt
};
Type tv{tftt};
if (FFlag::DebugLuauDeferredConstraintResolution && FFlag::LuauUserDefinedTypeFunctions)
CHECK_EQ(toString(&tv, {}), "woohoo<number>");
}
TEST_SUITE_END();

View File

@ -217,8 +217,14 @@ TEST_CASE_FIXTURE(Fixture, "add_function_at_work")
CHECK(toString(requireType("a")) == "number");
CHECK(toString(requireType("b")) == "add<number, string>");
CHECK(toString(requireType("c")) == "add<string, number>");
CHECK(toString(result.errors[0]) == "Operator '+' could not be applied to operands of types number and string; there is no corresponding overload for __add");
CHECK(toString(result.errors[1]) == "Operator '+' could not be applied to operands of types string and number; there is no corresponding overload for __add");
CHECK(
toString(result.errors[0]) ==
"Operator '+' could not be applied to operands of types number and string; there is no corresponding overload for __add"
);
CHECK(
toString(result.errors[1]) ==
"Operator '+' could not be applied to operands of types string and number; there is no corresponding overload for __add"
);
}
TEST_CASE_FIXTURE(BuiltinsFixture, "cyclic_add_function_at_work")
@ -290,7 +296,8 @@ TEST_CASE_FIXTURE(Fixture, "internal_functions_raise_errors")
LUAU_REQUIRE_ERROR_COUNT(1, result);
CHECK(
toString(result.errors[0]) == "Operator '+' could not be applied to operands of types unknown and unknown; there is no corresponding overload for __add"
toString(result.errors[0]) ==
"Operator '+' could not be applied to operands of types unknown and unknown; there is no corresponding overload for __add"
);
}

View File

@ -1115,6 +1115,20 @@ type Foo<T> = Foo<T> | string
REQUIRE(err);
}
TEST_CASE_FIXTURE(BuiltinsFixture, "type_alias_adds_reduce_constraint_for_type_function")
{
if (!FFlag::DebugLuauDeferredConstraintResolution || !FFlag::LuauUserDefinedTypeFunctions)
return;
CheckResult result = check(R"(
type plus<T> = add<number, T>
local sum: plus<number> = 10
)");
LUAU_CHECK_NO_ERRORS(result);
}
TEST_CASE_FIXTURE(Fixture, "user_defined_type_function_errors")
{
if (!FFlag::LuauUserDefinedTypeFunctions)

View File

@ -692,16 +692,16 @@ TEST_CASE_FIXTURE(BuiltinsFixture, "bad_select_should_not_crash")
end
)");
LUAU_REQUIRE_ERROR_COUNT(2, result);
if (FFlag::DebugLuauDeferredConstraintResolution)
{
// The argument count is the same, but the errors are currently cyclic type family instance ones.
// This isn't great, but the desired behavior here was that it didn't cause a crash and that is still true.
// The larger fix for this behavior will likely be integration of egraph-based normalization throughout the new solver.
// Counterintuitively, the parametr l0 is unconstrained and therefore it is valid to pass nil.
// The new solver therefore considers that parameter to be optional.
LUAU_REQUIRE_ERROR_COUNT(1, result);
CHECK("Argument count mismatch. Function expects 1 argument, but none are specified" == toString(result.errors[0]));
}
else
{
LUAU_REQUIRE_ERROR_COUNT(2, result);
CHECK_EQ("Argument count mismatch. Function '_' expects at least 1 argument, but none are specified", toString(result.errors[0]));
CHECK_EQ("Argument count mismatch. Function 'select' expects 1 argument, but none are specified", toString(result.errors[1]));
}

View File

@ -447,14 +447,17 @@ end
LUAU_REQUIRE_ERROR_COUNT(1, result);
if (FFlag::DebugLuauDeferredConstraintResolution)
{
CHECK_EQ(R"(Type pack 'X & Y & Z' could not be converted into 'number'; type X & Y & Z[0][0] (X) is not a subtype of number[0] (number)
CHECK_EQ(
R"(Type pack 'X & Y & Z' could not be converted into 'number'; type X & Y & Z[0][0] (X) is not a subtype of number[0] (number)
type X & Y & Z[0][1] (Y) is not a subtype of number[0] (number)
type X & Y & Z[0][2] (Z) is not a subtype of number[0] (number))",
toString(result.errors[0]));
toString(result.errors[0])
);
}
else
CHECK_EQ(
toString(result.errors[0]), R"(Type 'X & Y & Z' could not be converted into 'number'; none of the intersection parts are compatible)");
toString(result.errors[0]), R"(Type 'X & Y & Z' could not be converted into 'number'; none of the intersection parts are compatible)"
);
}
TEST_CASE_FIXTURE(Fixture, "overload_is_not_a_function")
@ -497,13 +500,16 @@ TEST_CASE_FIXTURE(Fixture, "intersect_bool_and_false")
LUAU_REQUIRE_ERROR_COUNT(1, result);
if (FFlag::DebugLuauDeferredConstraintResolution)
{
CHECK_EQ(R"(Type 'boolean & false' could not be converted into 'true'; type boolean & false[0] (boolean) is not a subtype of true (true)
CHECK_EQ(
R"(Type 'boolean & false' could not be converted into 'true'; type boolean & false[0] (boolean) is not a subtype of true (true)
type boolean & false[1] (false) is not a subtype of true (true))",
toString(result.errors[0]));
toString(result.errors[0])
);
}
else
CHECK_EQ(
toString(result.errors[0]), "Type 'boolean & false' could not be converted into 'true'; none of the intersection parts are compatible");
toString(result.errors[0]), "Type 'boolean & false' could not be converted into 'true'; none of the intersection parts are compatible"
);
}
TEST_CASE_FIXTURE(Fixture, "intersect_false_and_bool_and_false")
@ -522,10 +528,13 @@ TEST_CASE_FIXTURE(Fixture, "intersect_false_and_bool_and_false")
R"(Type 'boolean & false & false' could not be converted into 'true'; type boolean & false & false[0] (false) is not a subtype of true (true)
type boolean & false & false[1] (boolean) is not a subtype of true (true)
type boolean & false & false[2] (false) is not a subtype of true (true))",
toString(result.errors[0]));
toString(result.errors[0])
);
else
CHECK_EQ(toString(result.errors[0]),
"Type 'boolean & false & false' could not be converted into 'true'; none of the intersection parts are compatible");
CHECK_EQ(
toString(result.errors[0]),
"Type 'boolean & false & false' could not be converted into 'true'; none of the intersection parts are compatible"
);
}
TEST_CASE_FIXTURE(Fixture, "intersect_saturate_overloaded_functions")

View File

@ -55,11 +55,26 @@ TEST_CASE_FIXTURE(Fixture, "typeguard_inference_incomplete")
end
)";
const std::string expectedWithNewSolver = R"(
function f(a:{fn:()->(unknown,...unknown)}): ()
if type(a) == 'boolean'then
local a1:{fn:()->(unknown,...unknown)}&boolean=a
elseif a.fn()then
local a2:{fn:()->(unknown,...unknown)}&(class|function|nil|number|string|thread|buffer|table)=a
end
end
)";
if (FFlag::DebugLuauDeferredConstraintResolution)
CHECK_EQ(expectedWithNewSolver, decorateWithTypes(code));
else
CHECK_EQ(expected, decorateWithTypes(code));
}
TEST_CASE_FIXTURE(BuiltinsFixture, "luau-polyfill.Array.filter")
{
ScopedFastFlag sff{FFlag::DebugLuauDeferredConstraintResolution, false};
// This test exercises the fact that we should reduce sealed/unsealed/free tables
// res is a unsealed table with type {((T & ~nil)?) & any}
// Because we do not reduce it fully, we cannot unify it with `Array<T> = { [number] : T}
@ -157,6 +172,8 @@ TEST_CASE_FIXTURE(Fixture, "it_should_be_agnostic_of_actual_size")
// For now, infer it as just a free table.
TEST_CASE_FIXTURE(BuiltinsFixture, "setmetatable_constrains_free_type_into_free_table")
{
ScopedFastFlag sff{FFlag::DebugLuauDeferredConstraintResolution, false};
CheckResult result = check(R"(
local a = {}
local b
@ -175,6 +192,8 @@ TEST_CASE_FIXTURE(BuiltinsFixture, "setmetatable_constrains_free_type_into_free_
// Luau currently doesn't yet know how to allow assignments when the binding was refined.
TEST_CASE_FIXTURE(Fixture, "while_body_are_also_refined")
{
ScopedFastFlag sff{FFlag::DebugLuauDeferredConstraintResolution, false};
CheckResult result = check(R"(
type Node<T> = { value: T, child: Node<T>? }
@ -198,6 +217,8 @@ TEST_CASE_FIXTURE(Fixture, "while_body_are_also_refined")
// We should be type checking the metamethod at the call site of setmetatable.
TEST_CASE_FIXTURE(BuiltinsFixture, "error_on_eq_metamethod_returning_a_type_other_than_boolean")
{
ScopedFastFlag sff{FFlag::DebugLuauDeferredConstraintResolution, false};
CheckResult result = check(R"(
local tab = {a = 1}
setmetatable(tab, {__eq = function(a, b): number
@ -258,11 +279,8 @@ TEST_CASE_FIXTURE(Fixture, "discriminate_from_x_not_equal_to_nil")
if (FFlag::DebugLuauDeferredConstraintResolution)
{
CHECK_EQ("{ x: string, y: number }", toString(requireTypeAtPosition({5, 28})));
// Should be { x: nil, y: nil }
CHECK_EQ("{ x: nil, y: nil } | { x: string, y: number }", toString(requireTypeAtPosition({7, 28})));
CHECK_EQ("{ x: nil, y: nil }", toString(requireTypeAtPosition({7, 28})));
}
else
{
@ -341,10 +359,19 @@ TEST_CASE_FIXTURE(Fixture, "do_not_ice_when_trying_to_pick_first_of_generic_type
LUAU_REQUIRE_NO_ERRORS(result);
if (FFlag::DebugLuauDeferredConstraintResolution)
{
CHECK("() -> ()" == toString(requireType("f")));
CHECK("() -> ()" == toString(requireType("g")));
CHECK("nil" == toString(requireType("x")));
}
else
{
// f and g should have the type () -> ()
CHECK_EQ("() -> (a...)", toString(requireType("f")));
CHECK_EQ("<a...>() -> (a...)", toString(requireType("g")));
CHECK_EQ("any", toString(requireType("x"))); // any is returned instead of ICE for now
}
}
TEST_CASE_FIXTURE(Fixture, "specialization_binds_with_prototypes_too_early")
@ -355,6 +382,9 @@ TEST_CASE_FIXTURE(Fixture, "specialization_binds_with_prototypes_too_early")
local s2s: (string) -> string = id
)");
if (FFlag::DebugLuauDeferredConstraintResolution)
LUAU_REQUIRE_NO_ERRORS(result);
else
LUAU_REQUIRE_ERRORS(result); // Should not have any errors.
}
@ -487,6 +517,8 @@ TEST_CASE_FIXTURE(Fixture, "dcr_can_partially_dispatch_a_constraint")
TEST_CASE_FIXTURE(Fixture, "free_options_cannot_be_unified_together")
{
ScopedFastFlag sff{FFlag::DebugLuauDeferredConstraintResolution, false};
TypeArena arena;
TypeId nilType = builtinTypes->nilType;
@ -569,7 +601,7 @@ return wrapStrictTable(Constants, "Constants")
std::optional<TypeId> result = first(m->returnType);
REQUIRE(result);
if (FFlag::DebugLuauDeferredConstraintResolution)
CHECK_EQ("(any & ~(*error-type* | table))?", toString(*result));
CHECK_EQ("unknown", toString(*result));
else
CHECK_MESSAGE(get<AnyType>(*result), *result);
}
@ -610,7 +642,11 @@ return wrapStrictTable(Constants, "Constants")
std::optional<TypeId> result = first(m->returnType);
REQUIRE(result);
CHECK(get<AnyType>(*result));
if (FFlag::DebugLuauDeferredConstraintResolution)
CHECK("unknown" == toString(*result));
else
CHECK("any" == toString(*result));
}
namespace
@ -793,6 +829,10 @@ TEST_CASE_FIXTURE(Fixture, "assign_table_with_refined_property_with_a_similar_ty
end
)");
if (FFlag::DebugLuauDeferredConstraintResolution)
LUAU_REQUIRE_NO_ERRORS(result); // This is wrong. We should be rejecting this assignment.
else
{
LUAU_REQUIRE_ERROR_COUNT(1, result);
const std::string expected = R"(Type
'{| x: number? |}'
@ -802,10 +842,13 @@ caused by:
Property 'x' is not compatible.
Type 'number?' could not be converted into 'number' in an invariant context)";
CHECK_EQ(expected, toString(result.errors[0]));
}
}
TEST_CASE_FIXTURE(BuiltinsFixture, "table_insert_with_a_singleton_argument")
{
ScopedFastFlag sff{FFlag::DebugLuauDeferredConstraintResolution, false};
CheckResult result = check(R"(
local function foo(t, x)
if x == "hi" or x == "bye" then
@ -861,15 +904,25 @@ TEST_CASE_FIXTURE(Fixture, "expected_type_should_be_a_helpful_deduction_guide_fo
local x: Ref<number?> = useRef(nil)
)");
if (FFlag::DebugLuauDeferredConstraintResolution)
{
// This bug is fixed in the new solver.
LUAU_REQUIRE_ERROR_COUNT(1, result);
}
else
{
// This is actually wrong! Sort of. It's doing the wrong thing, it's actually asking whether
// `{| val: number? |} <: {| val: nil |}`
// instead of the correct way, which is
// `{| val: nil |} <: {| val: number? |}`
LUAU_REQUIRE_NO_ERRORS(result);
}
}
TEST_CASE_FIXTURE(Fixture, "floating_generics_should_not_be_allowed")
{
ScopedFastFlag sff{FFlag::DebugLuauDeferredConstraintResolution, false};
CheckResult result = check(R"(
local assign : <T, U, V, W>(target: T, source0: U?, source1: V?, source2: W?, ...any) -> T & U & V & W = (nil :: any)
@ -892,6 +945,8 @@ TEST_CASE_FIXTURE(Fixture, "floating_generics_should_not_be_allowed")
TEST_CASE_FIXTURE(Fixture, "free_options_can_be_unified_together")
{
ScopedFastFlag sff{FFlag::DebugLuauDeferredConstraintResolution, false};
TypeArena arena;
TypeId nilType = builtinTypes->nilType;
@ -935,8 +990,10 @@ TEST_CASE_FIXTURE(Fixture, "unify_more_complex_unions_that_include_nil")
LUAU_REQUIRE_NO_ERRORS(result);
}
TEST_CASE_FIXTURE(Fixture, "optional_class_instances_are_invariant")
TEST_CASE_FIXTURE(Fixture, "optional_class_instances_are_invariant_old_solver")
{
ScopedFastFlag sff{FFlag::DebugLuauDeferredConstraintResolution, false};
createSomeClasses(&frontend);
CheckResult result = check(R"(
@ -951,6 +1008,24 @@ TEST_CASE_FIXTURE(Fixture, "optional_class_instances_are_invariant")
LUAU_REQUIRE_NO_ERRORS(result);
}
TEST_CASE_FIXTURE(Fixture, "optional_class_instances_are_invariant_new_solver")
{
ScopedFastFlag sff{FFlag::DebugLuauDeferredConstraintResolution, true};
createSomeClasses(&frontend);
CheckResult result = check(R"(
function foo(ref: {read current: Parent?})
end
function bar(ref: {read current: Child?})
foo(ref)
end
)");
LUAU_REQUIRE_ERROR_COUNT(0, result);
}
TEST_CASE_FIXTURE(BuiltinsFixture, "luau-polyfill.Map.entries")
{
@ -1000,6 +1075,9 @@ end
// We would prefer this unification to be able to complete, but at least it should not crash
TEST_CASE_FIXTURE(BuiltinsFixture, "table_unification_infinite_recursion")
{
// The new solver doesn't recurse as heavily in this situation.
ScopedFastFlag sff{FFlag::DebugLuauDeferredConstraintResolution, false};
#if defined(_NOOPT) || defined(_DEBUG)
ScopedFastInt LuauTypeInferRecursionLimit{FInt::LuauTypeInferRecursionLimit, 100};
#endif
@ -1027,16 +1105,8 @@ local tbl = require(game.A)
tbl:f3()
)";
if (FFlag::DebugLuauDeferredConstraintResolution)
{
// TODO: DCR should transform RecursionLimitException into a CodeTooComplex error (currently it rethows it as InternalCompilerError)
CHECK_THROWS_AS(frontend.check("game/B"), Luau::InternalCompilerError);
}
else
{
CheckResult result = frontend.check("game/B");
LUAU_REQUIRE_ERROR_COUNT(1, result);
}
}
// Ideally, unification with any will not cause a 2^n normalization of a function overload
@ -1148,7 +1218,8 @@ TEST_CASE_FIXTURE(BuiltinsFixture, "luau_roact_useState_minimization")
update("hello")
)");
LUAU_REQUIRE_NO_ERRORS(result);
// We actually expect this code to be fine.
LUAU_REQUIRE_ERRORS(result);
}
TEST_CASE_FIXTURE(BuiltinsFixture, "bin_prov")

View File

@ -317,14 +317,19 @@ TEST_CASE_FIXTURE(BuiltinsFixture, "typeguard_in_if_condition_position")
TEST_CASE_FIXTURE(BuiltinsFixture, "typeguard_in_assert_position")
{
CheckResult result = check(R"(
local a
function f(a)
assert(type(a) == "number")
local b = a
return b
end
)");
LUAU_REQUIRE_NO_ERRORS(result);
REQUIRE_EQ("number", toString(requireType("b")));
if (FFlag::DebugLuauDeferredConstraintResolution)
CHECK("<a>(a) -> a & number" == toString(requireType("f")));
else
CHECK("<a>(a) -> number" == toString(requireType("f")));
}
TEST_CASE_FIXTURE(BuiltinsFixture, "refine_unknown_to_table_then_test_a_prop")
@ -440,15 +445,13 @@ TEST_CASE_FIXTURE(BuiltinsFixture, "call_an_incompatible_function_after_using_ty
end
)");
if (FFlag::DebugLuauDeferredConstraintResolution)
LUAU_REQUIRE_ERROR_COUNT(1, result);
else
LUAU_REQUIRE_ERROR_COUNT(2, result);
CHECK_EQ("Type 'string' could not be converted into 'number'", toString(result.errors[0]));
CHECK("Type 'string' could not be converted into 'number'" == toString(result.errors[0]));
CHECK(Location{{ 7, 18}, {7, 19}} == result.errors[0].location);
if (!FFlag::DebugLuauDeferredConstraintResolution)
CHECK_EQ("Type 'string' could not be converted into 'number'", toString(result.errors[1]));
CHECK("Type 'string' could not be converted into 'number'" == toString(result.errors[1]));
CHECK(Location{{ 13, 18}, {13, 19}} == result.errors[1].location);
}
TEST_CASE_FIXTURE(BuiltinsFixture, "impossible_type_narrow_is_not_an_error")
@ -485,7 +488,8 @@ TEST_CASE_FIXTURE(Fixture, "truthy_constraint_on_properties")
if (FFlag::DebugLuauDeferredConstraintResolution)
{
CHECK("{| x: number |}" == toString(requireTypeAtPosition({4, 23})));
// CLI-115281 - Types produced by refinements don't always get simplified
CHECK("{ x: number? } & { x: ~(false?) }" == toString(requireTypeAtPosition({4, 23})));
CHECK("number" == toString(requireTypeAtPosition({5, 26})));
}
@ -699,6 +703,9 @@ TEST_CASE_FIXTURE(BuiltinsFixture, "type_narrow_to_vector")
LUAU_REQUIRE_NO_ERRORS(result);
if (FFlag::DebugLuauDeferredConstraintResolution)
CHECK_EQ("never", toString(requireTypeAtPosition({3, 28})));
else
CHECK_EQ("*error-type*", toString(requireTypeAtPosition({3, 28})));
}
@ -722,11 +729,23 @@ TEST_CASE_FIXTURE(BuiltinsFixture, "nonoptional_type_can_narrow_to_nil_if_sense_
LUAU_REQUIRE_NO_ERRORS(result);
if (FFlag::DebugLuauDeferredConstraintResolution)
{
// CLI-115281 Types produced by refinements do not consistently get simplified
CHECK_EQ("(nil & string)?", toString(requireTypeAtPosition({4, 24}))); // type(v) == "nil"
CHECK_EQ("(boolean | buffer | class | function | number | string | table | thread) & string", toString(requireTypeAtPosition({6, 24}))); // type(v) ~= "nil"
CHECK_EQ("(nil & string)?", toString(requireTypeAtPosition({10, 24}))); // equivalent to type(v) == "nil"
CHECK_EQ("(boolean | buffer | class | function | number | string | table | thread) & string", toString(requireTypeAtPosition({12, 24}))); // equivalent to type(v) ~= "nil"
}
else
{
CHECK_EQ("nil", toString(requireTypeAtPosition({4, 24}))); // type(v) == "nil"
CHECK_EQ("string", toString(requireTypeAtPosition({6, 24}))); // type(v) ~= "nil"
CHECK_EQ("nil", toString(requireTypeAtPosition({10, 24}))); // equivalent to type(v) == "nil"
CHECK_EQ("string", toString(requireTypeAtPosition({12, 24}))); // equivalent to type(v) ~= "nil"
}
}
TEST_CASE_FIXTURE(BuiltinsFixture, "typeguard_not_to_be_string")
@ -844,6 +863,12 @@ TEST_CASE_FIXTURE(BuiltinsFixture, "type_guard_narrowed_into_nothingness")
LUAU_REQUIRE_NO_ERRORS(result);
if (FFlag::DebugLuauDeferredConstraintResolution)
{
// CLI-115281 Types produced by refinements do not consistently get simplified
CHECK_EQ("{ x: number } & ~table", toString(requireTypeAtPosition({3, 28})));
}
else
CHECK_EQ("never", toString(requireTypeAtPosition({3, 28})));
}
@ -950,7 +975,10 @@ TEST_CASE_FIXTURE(Fixture, "not_t_or_some_prop_of_t")
LUAU_REQUIRE_NO_ERRORS(result);
if (FFlag::DebugLuauDeferredConstraintResolution)
CHECK_EQ("{| x: true |}?", toString(requireTypeAtPosition({3, 28})));
{
// CLI-115281 Types produced by refinements do not consistently get simplified
CHECK_EQ("({ x: boolean } & { x: ~(false?) })?", toString(requireTypeAtPosition({3, 28})));
}
else
CHECK_EQ("{| x: boolean |}?", toString(requireTypeAtPosition({3, 28})));
}
@ -1196,11 +1224,17 @@ TEST_CASE_FIXTURE(Fixture, "discriminate_from_truthiness_of_x")
LUAU_REQUIRE_NO_ERRORS(result);
CHECK_EQ(R"({| tag: "exists", x: string |})", toString(requireTypeAtPosition({5, 28})));
if (FFlag::DebugLuauDeferredConstraintResolution)
CHECK_EQ(R"({| tag: "missing", x: nil |})", toString(requireTypeAtPosition({7, 28})));
{
// CLI-115281 Types produced by refinements do not consistently get simplified
CHECK("{ tag: \"exists\", x: string } & { x: ~(false?) }" == toString(requireTypeAtPosition({5, 28})));
CHECK("({ tag: \"exists\", x: string } & { x: ~~(false?) }) | { tag: \"missing\", x: nil }" == toString(requireTypeAtPosition({7, 28})));
}
else
{
CHECK_EQ(R"({| tag: "exists", x: string |})", toString(requireTypeAtPosition({5, 28})));
CHECK_EQ(R"({| tag: "exists", x: string |} | {| tag: "missing", x: nil |})", toString(requireTypeAtPosition({7, 28})));
}
}
TEST_CASE_FIXTURE(Fixture, "discriminate_tag")
@ -1328,8 +1362,8 @@ TEST_CASE_FIXTURE(RefinementClassFixture, "discriminate_from_isa_of_x")
if (FFlag::DebugLuauDeferredConstraintResolution)
{
CHECK_EQ(R"({ tag: "Part", x: Part })", toString(requireTypeAtPosition({5, 28})));
CHECK_EQ(R"({ tag: "Folder", x: Folder })", toString(requireTypeAtPosition({7, 28})));
CHECK(R"({ tag: "Part", x: Part })" == toString(requireTypeAtPosition({5, 28})));
CHECK(R"({ tag: "Folder", x: Folder })" == toString(requireTypeAtPosition({7, 28})));
}
else
{
@ -1340,6 +1374,9 @@ TEST_CASE_FIXTURE(RefinementClassFixture, "discriminate_from_isa_of_x")
TEST_CASE_FIXTURE(RefinementClassFixture, "typeguard_cast_free_table_to_vector")
{
// CLI-115286 - Refining via type(x) == 'vector' does not work in the new solver
ScopedFastFlag sff{FFlag::DebugLuauDeferredConstraintResolution, false};
CheckResult result = check(R"(
local function f(vec)
local X, Y, Z = vec.X, vec.Y, vec.Z
@ -1527,6 +1564,10 @@ TEST_CASE_FIXTURE(RefinementClassFixture, "refine_param_of_type_folder_or_part_w
TEST_CASE_FIXTURE(RefinementClassFixture, "isa_type_refinement_must_be_known_ahead_of_time")
{
// CLI-115087 - The new solver does not consistently combine tables with
// class types when they appear in the upper bounds of a free type.
ScopedFastFlag sff{FFlag::DebugLuauDeferredConstraintResolution, false};
CheckResult result = check(R"(
local function f(x): Instance
if x:IsA("Folder") then
@ -1819,7 +1860,7 @@ TEST_CASE_FIXTURE(Fixture, "refine_a_property_of_some_global")
{
LUAU_REQUIRE_ERROR_COUNT(3, result);
CHECK_EQ("~(false?)", toString(requireTypeAtPosition({4, 30})));
CHECK_EQ("*error-type* | buffer | class | function | number | string | table | thread | true", toString(requireTypeAtPosition({4, 30})));
}
}
@ -1843,9 +1884,9 @@ TEST_CASE_FIXTURE(BuiltinsFixture, "dataflow_analysis_can_tell_refinements_when_
end
if typeof(s) == "nil" then
local foo = s
local foo = s -- line 18
else
local foo = s
local foo = s -- line 20
end
end
)");
@ -1860,7 +1901,8 @@ TEST_CASE_FIXTURE(BuiltinsFixture, "dataflow_analysis_can_tell_refinements_when_
if (FFlag::DebugLuauDeferredConstraintResolution)
{
CHECK_EQ("never", toString(requireTypeAtPosition({18, 28})));
// CLI-115281 - Types produced by refinements don't always get simplified
CHECK_EQ("nil & string", toString(requireTypeAtPosition({18, 28})));
CHECK_EQ("string", toString(requireTypeAtPosition({20, 28})));
}
else
@ -1948,7 +1990,10 @@ TEST_CASE_FIXTURE(BuiltinsFixture, "type_annotations_arent_relevant_when_doing_d
CHECK_EQ("nil", toString(requireTypeAtPosition({8, 28})));
if (FFlag::DebugLuauDeferredConstraintResolution)
CHECK_EQ("never", toString(requireTypeAtPosition({9, 28})));
{
// CLI-115478 - This should be never
CHECK_EQ("nil", toString(requireTypeAtPosition({9, 28})));
}
else
CHECK_EQ("nil", toString(requireTypeAtPosition({9, 28})));
}
@ -2044,7 +2089,6 @@ TEST_CASE_FIXTURE(BuiltinsFixture, "refine_unknown_to_table")
TEST_CASE_FIXTURE(BuiltinsFixture, "conditional_refinement_should_stay_error_suppressing")
{
ScopedFastFlag sff{FFlag::DebugLuauDeferredConstraintResolution, true};
// this test is DCR-only as an instance of DCR fixing a bug in the old solver
CheckResult result = check(R"(
local function test(element: any?)
@ -2065,6 +2109,12 @@ TEST_CASE_FIXTURE(BuiltinsFixture, "globals_can_be_narrowed_too")
end
)");
if (FFlag::DebugLuauDeferredConstraintResolution)
{
// CLI-114134
CHECK("string & typeof(string)" == toString(requireTypeAtPosition(Position{2, 24})));
}
else
CHECK("never" == toString(requireTypeAtPosition(Position{2, 24})));
}
@ -2096,17 +2146,6 @@ TEST_CASE_FIXTURE(BuiltinsFixture, "luau_polyfill_isindexkey_refine_conjunction_
LUAU_REQUIRE_NO_ERRORS(result);
}
TEST_CASE_FIXTURE(BuiltinsFixture, "globals_can_be_narrowed_too")
{
CheckResult result = check(R"(
if typeof(string) == 'string' then
local foo = string
end
)");
CHECK("never" == toString(requireTypeAtPosition(Position{2, 24})));
}
TEST_CASE_FIXTURE(BuiltinsFixture, "ex")
{
CheckResult result = check(R"(

View File

@ -2536,7 +2536,10 @@ local y: number = tmp.p.y
LUAU_REQUIRE_ERROR_COUNT(1, result);
if (FFlag::DebugLuauDeferredConstraintResolution)
CHECK("Type 'tmp' could not be converted into 'HasSuper'; at [read \"p\"], { x: number, y: number } is not exactly Super" == toString(result.errors[0]));
CHECK(
"Type 'tmp' could not be converted into 'HasSuper'; at [read \"p\"], { x: number, y: number } is not exactly Super" ==
toString(result.errors[0])
);
else
{
const std::string expected = R"(Type 'tmp' could not be converted into 'HasSuper'

View File

@ -33,6 +33,10 @@ until _._
TEST_CASE_FIXTURE(Fixture, "return_types_can_be_disjoint")
{
// CLI-114134 We need egraphs to consistently reduce the cyclic union
// introduced by the increment here.
ScopedFastFlag sff{FFlag::DebugLuauDeferredConstraintResolution, false};
CheckResult result = check(R"(
local count = 0
function most_of_the_natural_numbers(): number?
@ -51,6 +55,27 @@ TEST_CASE_FIXTURE(Fixture, "return_types_can_be_disjoint")
REQUIRE(utv != nullptr);
}
TEST_CASE_FIXTURE(Fixture, "return_types_can_be_disjoint_using_compound_assignment")
{
CheckResult result = check(R"(
local count = 0
function most_of_the_natural_numbers(): number?
if count < 10 then
-- count = count + 1
count += 1
return count
else
return nil
end
end
)");
LUAU_REQUIRE_NO_ERRORS(result);
const FunctionType* utv = get<FunctionType>(requireType("most_of_the_natural_numbers"));
REQUIRE(utv != nullptr);
}
TEST_CASE_FIXTURE(Fixture, "allow_specific_assign")
{
CheckResult result = check(R"(
@ -95,6 +120,9 @@ TEST_CASE_FIXTURE(Fixture, "optional_arguments")
TEST_CASE_FIXTURE(Fixture, "optional_arguments_table")
{
// CLI-115588 - Bidirectional inference does not happen for assignments
ScopedFastFlag sff{FFlag::DebugLuauDeferredConstraintResolution, false};
CheckResult result = check(R"(
local a:{a:string, b:string?}
a = {a="ok"}
@ -209,7 +237,7 @@ TEST_CASE_FIXTURE(Fixture, "index_on_a_union_type_with_missing_property")
CHECK_EQ("Key 'x' is missing from 'B' in the type 'A | B'", toString(result.errors[0]));
if (FFlag::DebugLuauDeferredConstraintResolution)
CHECK_EQ("(A | B) -> number | *error-type*", toString(requireType("f")));
CHECK_EQ("(A | B) -> number", toString(requireType("f")));
else
CHECK_EQ("(A | B) -> *error-type*", toString(requireType("f")));
}
@ -261,10 +289,6 @@ TEST_CASE_FIXTURE(Fixture, "optional_union_members")
LUAU_REQUIRE_ERROR_COUNT(1, result);
CHECK_EQ("Value of type 'A?' could be nil", toString(result.errors[0]));
if (FFlag::DebugLuauDeferredConstraintResolution)
CHECK_EQ("(A?) -> number | *error-type*", toString(requireType("f")));
else
CHECK_EQ("(A?) -> number", toString(requireType("f")));
}
@ -282,10 +306,6 @@ TEST_CASE_FIXTURE(Fixture, "optional_union_functions")
LUAU_REQUIRE_ERROR_COUNT(1, result);
CHECK_EQ("Value of type 'A?' could be nil", toString(result.errors[0]));
if (FFlag::DebugLuauDeferredConstraintResolution)
CHECK_EQ("(A?) -> number | *error-type*", toString(requireType("f")));
else
CHECK_EQ("(A?) -> number", toString(requireType("f")));
}
@ -303,10 +323,6 @@ TEST_CASE_FIXTURE(Fixture, "optional_union_methods")
LUAU_REQUIRE_ERROR_COUNT(1, result);
CHECK_EQ("Value of type 'A?' could be nil", toString(result.errors[0]));
if (FFlag::DebugLuauDeferredConstraintResolution)
CHECK_EQ("(A?) -> number | *error-type*", toString(requireType("f")));
else
CHECK_EQ("(A?) -> number", toString(requireType("f")));
}
@ -456,6 +472,8 @@ end
TEST_CASE_FIXTURE(Fixture, "unify_unsealed_table_union_check")
{
ScopedFastFlag sff{FFlag::DebugLuauDeferredConstraintResolution, false};
CheckResult result = check(R"(
local x = { x = 3 }
type A = number?
@ -537,16 +555,19 @@ Table type 'X' not compatible with type '{| w: number |}' because the former is
TEST_CASE_FIXTURE(Fixture, "error_detailed_union_all")
{
CheckResult result = check(R"(
type X = { x: number }
type Y = { y: number }
type Z = { z: number }
type X = { x: number }
type Y = { y: number }
type Z = { z: number }
type XYZ = X | Y | Z
type XYZ = X | Y | Z
local a: XYZ = { w = 4 }
local a: XYZ = { w = 4 }
)");
LUAU_REQUIRE_ERROR_COUNT(1, result);
if (FFlag::DebugLuauDeferredConstraintResolution)
CHECK(toString(result.errors[0]) == "Type '{ w: number }' could not be converted into 'X | Y | Z'");
else
CHECK_EQ(toString(result.errors[0]), R"(Type 'a' could not be converted into 'X | Y | Z'; none of the union options are compatible)");
}
@ -559,11 +580,16 @@ local a: X? = { w = 4 }
)");
LUAU_REQUIRE_ERROR_COUNT(1, result);
if (FFlag::DebugLuauDeferredConstraintResolution)
CHECK("Type '{ w: number }' could not be converted into 'X?'" == toString(result.errors[0]));
else
{
const std::string expected = R"(Type 'a' could not be converted into 'X?'
caused by:
None of the union options are compatible. For example:
Table type 'a' not compatible with type 'X' because the former is missing field 'x')";
CHECK_EQ(expected, toString(result.errors[0]));
}
}
// We had a bug where a cyclic union caused a stack overflow.
@ -615,6 +641,7 @@ TEST_CASE_FIXTURE(Fixture, "indexing_into_a_cyclic_union_doesnt_crash")
TEST_CASE_FIXTURE(BuiltinsFixture, "table_union_write_indirect")
{
ScopedFastFlag sff{FFlag::DebugLuauDeferredConstraintResolution, false};
CheckResult result = check(R"(
type A = { x: number, y: (number) -> string } | { z: number, y: (number) -> string }
@ -690,6 +717,8 @@ TEST_CASE_FIXTURE(Fixture, "union_of_generic_typepack_functions")
TEST_CASE_FIXTURE(Fixture, "union_of_functions_mentioning_generics")
{
ScopedFastFlag sff{FFlag::DebugLuauDeferredConstraintResolution, false};
CheckResult result = check(R"(
function f<a,b>()
function g(x : (a) -> a?)
@ -708,6 +737,8 @@ TEST_CASE_FIXTURE(Fixture, "union_of_functions_mentioning_generics")
TEST_CASE_FIXTURE(Fixture, "union_of_functions_mentioning_generic_typepacks")
{
ScopedFastFlag sff{FFlag::DebugLuauDeferredConstraintResolution, false};
CheckResult result = check(R"(
function f<a...>()
function g(x : (number, a...) -> (number?, a...))
@ -727,6 +758,8 @@ could not be converted into
TEST_CASE_FIXTURE(Fixture, "union_of_functions_with_mismatching_arg_arities")
{
ScopedFastFlag sff{FFlag::DebugLuauDeferredConstraintResolution, false};
CheckResult result = check(R"(
function f(x : (number) -> number?)
local y : ((number?) -> number) | ((number | string) -> nil) = x -- OK
@ -744,6 +777,8 @@ could not be converted into
TEST_CASE_FIXTURE(Fixture, "union_of_functions_with_mismatching_result_arities")
{
ScopedFastFlag sff{FFlag::DebugLuauDeferredConstraintResolution, false};
CheckResult result = check(R"(
function f(x : () -> (number | string))
local y : (() -> number) | (() -> string) = x -- OK
@ -761,6 +796,8 @@ could not be converted into
TEST_CASE_FIXTURE(Fixture, "union_of_functions_with_variadics")
{
ScopedFastFlag sff{FFlag::DebugLuauDeferredConstraintResolution, false};
CheckResult result = check(R"(
function f(x : (...nil) -> (...number?))
local y : ((...string?) -> (...number)) | ((...number?) -> nil) = x -- OK
@ -786,15 +823,27 @@ TEST_CASE_FIXTURE(Fixture, "union_of_functions_with_mismatching_arg_variadics")
)");
LUAU_REQUIRE_ERROR_COUNT(1, result);
if (FFlag::DebugLuauDeferredConstraintResolution)
{
CHECK(R"(Type
'(number) -> ()'
could not be converted into
'((...number?) -> ()) | ((number?) -> ())')" == toString(result.errors[0]));
}
else
{
const std::string expected = R"(Type
'(number) -> ()'
could not be converted into
'((...number?) -> ()) | ((number?) -> ())'; none of the union options are compatible)";
CHECK_EQ(expected, toString(result.errors[0]));
}
}
TEST_CASE_FIXTURE(Fixture, "union_of_functions_with_mismatching_result_variadics")
{
ScopedFastFlag sff{FFlag::DebugLuauDeferredConstraintResolution, false};
CheckResult result = check(R"(
function f(x : () -> (number?, ...number))
local y : (() -> (...number)) | (() -> nil) = x -- OK
@ -824,7 +873,7 @@ TEST_CASE_FIXTURE(Fixture, "less_greedy_unification_with_union_types")
LUAU_REQUIRE_NO_ERRORS(result);
CHECK_EQ("({| x: number |} | {| x: string |}) -> {| x: number |} | {| x: string |}", toString(requireType("f")));
CHECK_EQ("(({ read x: unknown } & { x: number }) | ({ read x: unknown } & { x: string })) -> { x: number } | { x: string }", toString(requireType("f")));
}
TEST_CASE_FIXTURE(Fixture, "less_greedy_unification_with_union_types_2")
@ -840,10 +889,7 @@ TEST_CASE_FIXTURE(Fixture, "less_greedy_unification_with_union_types_2")
LUAU_REQUIRE_NO_ERRORS(result);
if (FFlag::DebugLuauDeferredConstraintResolution)
CHECK_EQ("({ x: number } | { x: string }) -> number | string", toString(requireType("f")));
else
CHECK_EQ("({| x: number |} | {| x: string |}) -> number | string", toString(requireType("f")));
}
TEST_CASE_FIXTURE(Fixture, "union_table_any_property")
@ -864,6 +910,8 @@ TEST_CASE_FIXTURE(Fixture, "union_table_any_property")
TEST_CASE_FIXTURE(Fixture, "union_function_any_args")
{
ScopedFastFlag sff{FFlag::DebugLuauDeferredConstraintResolution, false};
CheckResult result = check(R"(
function f(sup : ((...any) -> (...any))?, sub : ((number) -> (...any)))
sup = sub
@ -886,6 +934,8 @@ TEST_CASE_FIXTURE(Fixture, "optional_any")
TEST_CASE_FIXTURE(Fixture, "generic_function_with_optional_arg")
{
ScopedFastFlag sff{FFlag::DebugLuauDeferredConstraintResolution, false};
CheckResult result = check(R"(
function f<T>(x : T?) : {T}
local result = {}

View File

@ -63,53 +63,9 @@ Negations.cofinite_strings_can_be_compared_for_equality
Normalize.higher_order_function_with_annotation
Normalize.negations_of_tables
Normalize.specific_functions_cannot_be_negated
ProvisionalTests.assign_table_with_refined_property_with_a_similar_type_is_illegal
ProvisionalTests.discriminate_from_x_not_equal_to_nil
ProvisionalTests.do_not_ice_when_trying_to_pick_first_of_generic_type_pack
ProvisionalTests.error_on_eq_metamethod_returning_a_type_other_than_boolean
ProvisionalTests.expected_type_should_be_a_helpful_deduction_guide_for_function_calls
ProvisionalTests.floating_generics_should_not_be_allowed
ProvisionalTests.free_options_can_be_unified_together
ProvisionalTests.free_options_cannot_be_unified_together
ProvisionalTests.generic_type_leak_to_module_interface
ProvisionalTests.generic_type_leak_to_module_interface_variadic
ProvisionalTests.luau-polyfill.Array.filter
ProvisionalTests.luau_roact_useState_minimization
ProvisionalTests.optional_class_instances_are_invariant
ProvisionalTests.setmetatable_constrains_free_type_into_free_table
ProvisionalTests.specialization_binds_with_prototypes_too_early
ProvisionalTests.table_insert_with_a_singleton_argument
ProvisionalTests.table_unification_infinite_recursion
ProvisionalTests.typeguard_inference_incomplete
ProvisionalTests.while_body_are_also_refined
RefinementTest.call_an_incompatible_function_after_using_typeguard
RefinementTest.dataflow_analysis_can_tell_refinements_when_its_appropriate_to_refine_into_nil_or_never
RefinementTest.discriminate_from_isa_of_x
RefinementTest.discriminate_from_truthiness_of_x
RefinementTest.globals_can_be_narrowed_too
RefinementTest.isa_type_refinement_must_be_known_ahead_of_time
RefinementTest.nonoptional_type_can_narrow_to_nil_if_sense_is_true
RefinementTest.not_t_or_some_prop_of_t
RefinementTest.refine_a_param_that_got_resolved_during_constraint_solving_stage
RefinementTest.refine_a_property_of_some_global
RefinementTest.refine_param_of_type_folder_or_part_without_using_typeof
RefinementTest.refine_unknown_to_table_then_clone_it
RefinementTest.truthy_constraint_on_properties
RefinementTest.type_annotations_arent_relevant_when_doing_dataflow_analysis
RefinementTest.type_guard_narrowed_into_nothingness
RefinementTest.type_narrow_to_vector
RefinementTest.typeguard_cast_free_table_to_vector
RefinementTest.typeguard_in_assert_position
RefinementTest.x_as_any_if_x_is_instance_elseif_x_is_table
RefinementTest.x_is_not_instance_or_else_not_part
ToDot.function
ToString.exhaustive_toString_of_cyclic_table
ToString.free_types
ToString.named_metatable_toStringNamedFunction
ToString.no_parentheses_around_cyclic_function_type_in_intersection
ToString.pick_distinct_names_for_mixed_explicit_and_implicit_generics
ToString.primitive
ToString.toStringErrorPack
TryUnifyTests.members_of_failed_typepack_unification_are_unified_with_errorType
TryUnifyTests.result_of_failed_typepack_unification_is_constrained
TryUnifyTests.uninhabited_table_sub_anything
@ -261,24 +217,4 @@ TypeSingletons.return_type_of_f_is_not_widened
TypeSingletons.table_properties_type_error_escapes
TypeSingletons.widen_the_supertype_if_it_is_free_and_subtype_has_singleton
TypeStatesTest.typestates_preserve_error_suppression_properties
UnionTypes.error_detailed_optional
UnionTypes.error_detailed_union_all
UnionTypes.generic_function_with_optional_arg
UnionTypes.index_on_a_union_type_with_missing_property
UnionTypes.less_greedy_unification_with_union_types
UnionTypes.optional_arguments_table
UnionTypes.optional_union_functions
UnionTypes.optional_union_members
UnionTypes.optional_union_methods
UnionTypes.return_types_can_be_disjoint
UnionTypes.table_union_write_indirect
UnionTypes.unify_unsealed_table_union_check
UnionTypes.union_function_any_args
UnionTypes.union_of_functions_mentioning_generic_typepacks
UnionTypes.union_of_functions_mentioning_generics
UnionTypes.union_of_functions_with_mismatching_arg_arities
UnionTypes.union_of_functions_with_mismatching_arg_variadics
UnionTypes.union_of_functions_with_mismatching_result_arities
UnionTypes.union_of_functions_with_mismatching_result_variadics
UnionTypes.union_of_functions_with_variadics
VisitType.throw_when_limit_is_exceeded