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

@ -82,7 +82,8 @@ struct AnyTypeSummary
VarAny, VarAny,
TableProp, TableProp,
Alias, Alias,
Assign Assign,
TypePk
}; };
struct TypeInfo struct TypeInfo

View File

@ -184,9 +184,9 @@ private:
DataFlowResult visitExpr(DfgScope* scope, AstExprInterpString* i); DataFlowResult visitExpr(DfgScope* scope, AstExprInterpString* i);
DataFlowResult visitExpr(DfgScope* scope, AstExprError* error); DataFlowResult visitExpr(DfgScope* scope, AstExprError* error);
void visitLValue(DfgScope* scope, AstExpr* e, DefId incomingDef, bool isCompoundAssignment = false); void visitLValue(DfgScope* scope, AstExpr* e, DefId incomingDef);
DefId visitLValue(DfgScope* scope, AstExprLocal* l, DefId incomingDef, bool isCompoundAssignment); DefId visitLValue(DfgScope* scope, AstExprLocal* l, DefId incomingDef);
DefId visitLValue(DfgScope* scope, AstExprGlobal* g, DefId incomingDef, bool isCompoundAssignment); DefId visitLValue(DfgScope* scope, AstExprGlobal* g, DefId incomingDef);
DefId visitLValue(DfgScope* scope, AstExprIndexName* i, DefId incomingDef); DefId visitLValue(DfgScope* scope, AstExprIndexName* i, DefId incomingDef);
DefId visitLValue(DfgScope* scope, AstExprIndexExpr* i, DefId incomingDef); DefId visitLValue(DfgScope* scope, AstExprIndexExpr* i, DefId incomingDef);
DefId visitLValue(DfgScope* scope, AstExprError* e, 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, const ClassType* subClass, const ClassType* superClass);
SubtypingResult isCovariantWith(SubtypingEnvironment& env, TypeId subTy, const ClassType* subClass, TypeId superTy, const TableType* superTable); 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 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 PrimitiveType* subPrim, const TableType* superTable);
SubtypingResult isCovariantWith(SubtypingEnvironment& env, const SingletonType* subSingleton, 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<TypeId> typeArguments;
std::vector<TypePackId> packArguments; 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) : function(function)
, typeArguments(typeArguments) , typeArguments(typeArguments)
, packArguments(packArguments) , packArguments(packArguments)
, userFuncName(userFuncName)
, userFuncBody(userFuncBody)
{ {
} }

View File

@ -32,6 +32,9 @@ struct TypeFunctionContext
// The constraint being reduced in this run of the reduction // The constraint being reduced in this run of the reduction
const Constraint* constraint; 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) TypeFunctionContext(NotNull<ConstraintSolver> cs, NotNull<Scope> scope, NotNull<const Constraint> constraint)
: arena(cs->arena) : arena(cs->arena)
, builtins(cs->builtinTypes) , builtins(cs->builtinTypes)
@ -156,6 +159,8 @@ struct BuiltinTypeFunctions
{ {
BuiltinTypeFunctions(); BuiltinTypeFunctions();
TypeFunction userFunc;
TypeFunction notFunc; TypeFunction notFunc;
TypeFunction lenFunc; TypeFunction lenFunc;
TypeFunction unmFunc; 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); const Scope* retScope = findInnerMostScope(ret->location, module);
auto ctxNode = getNode(rootSrc, ret); auto ctxNode = getNode(rootSrc, ret);
bool seenTP = false;
for (auto val : ret->list) for (auto val : ret->list)
{ {
@ -160,8 +161,24 @@ void AnyTypeSummary::visit(const Scope* scope, AstStatReturn* ret, const Module*
typeInfo.push_back(ti); 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) 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; TypeId domainTy = builtinTypes->neverType;
for (TypeId d : domain) for (TypeId d : domain)
{ {
d = follow(d);
if (d == ty) if (d == ty)
continue; continue;
domainTy = simplifyUnion(builtinTypes, arena, domainTy, d).result; domainTy = simplifyUnion(builtinTypes, arena, domainTy, d).result;
@ -663,6 +664,51 @@ ControlFlow ConstraintGenerator::visitBlockWithoutChildScope(const ScopePtr& sco
astTypeAliasDefiningScopes[alias] = defnScope; astTypeAliasDefiningScopes[alias] = defnScope;
aliasDefinitionLocations[alias->name.value] = alias->location; 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; std::optional<ControlFlow> firstControlFlow;
@ -1368,6 +1414,20 @@ ControlFlow ConstraintGenerator::visit(const ScopePtr& scope, AstStatTypeAlias*
ControlFlow ConstraintGenerator::visit(const ScopePtr& scope, AstStatTypeFunction* function) 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; return ControlFlow::None;
} }

View File

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

View File

@ -570,15 +570,8 @@ ControlFlow DataFlowGraphBuilder::visit(DfgScope* scope, AstStatAssign* a)
ControlFlow DataFlowGraphBuilder::visit(DfgScope* scope, AstStatCompoundAssign* c) 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, (void) visitExpr(scope, c->value);
// but the `c->var` only has one pointer address, so we need to come up with a way to store both. (void) visitExpr(scope, c->var);
// 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);
return ControlFlow::None; return ControlFlow::None;
} }
@ -920,14 +913,14 @@ DataFlowResult DataFlowGraphBuilder::visitExpr(DfgScope* scope, AstExprError* er
return {defArena->freshCell(), nullptr}; 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 = [&]() auto go = [&]()
{ {
if (auto l = e->as<AstExprLocal>()) if (auto l = e->as<AstExprLocal>())
return visitLValue(scope, l, incomingDef, isCompoundAssignment); return visitLValue(scope, l, incomingDef);
else if (auto g = e->as<AstExprGlobal>()) 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>()) else if (auto i = e->as<AstExprIndexName>())
return visitLValue(scope, i, incomingDef); return visitLValue(scope, i, incomingDef);
else if (auto i = e->as<AstExprIndexExpr>()) else if (auto i = e->as<AstExprIndexExpr>())
@ -941,15 +934,8 @@ void DataFlowGraphBuilder::visitLValue(DfgScope* scope, AstExpr* e, DefId incomi
graph.astDefs[e] = go(); 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. // In order to avoid alias tracking, we need to clip the reference to the parent def.
if (scope->canUpdateDefinition(l->local)) 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; 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. // In order to avoid alias tracking, we need to clip the reference to the parent def.
if (scope->canUpdateDefinition(g->name)) if (scope->canUpdateDefinition(g->name))
{ {

View File

@ -2186,6 +2186,11 @@ void Normalizer::intersectClasses(NormalizedClassType& heres, const NormalizedCl
if (isSubclass(thereTy, hereTy)) 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); TypeIds negations = std::move(hereNegations);
for (auto nIt = negations.begin(); nIt != negations.end();) for (auto nIt = negations.begin(); nIt != negations.end();)
@ -2209,22 +2214,45 @@ void Normalizer::intersectClasses(NormalizedClassType& heres, const NormalizedCl
} }
else if (isSubclass(hereTy, thereTy)) 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; TypeIds negations = thereNegations;
bool erasedHere = false;
for (auto nIt = negations.begin(); nIt != negations.end();) for (auto nIt = negations.begin(); nIt != negations.end();)
{ {
if (!isSubclass(*nIt, hereTy)) if (isSubclass(hereTy, *nIt))
{ {
nIt = negations.erase(nIt); // eg SomeClass & (class & ~SomeClass)
} // or SomeClass & (class & ~ParentClass)
else heres.classes.erase(hereTy);
{ it = heres.ordering.erase(it);
++nIt; erasedHere = true;
} break;
} }
// eg SomeClass & (class & ~Unrelated)
if (!isSubclass(*nIt, hereTy))
nIt = negations.erase(nIt);
else
++nIt;
}
if (!erasedHere)
{
unionClasses(hereNegations, negations); unionClasses(hereNegations, negations);
break; ++it;
}
} }
else if (hereTy == thereTy) 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 // function arguments are options, then this function call
// is ok. // is ok.
const size_t firstUnsatisfiedArgument = argExprs->size(); const size_t firstUnsatisfiedArgument = args->head.size();
const auto [requiredHead, _requiredTail] = flatten(fn->argTypes); const auto [requiredHead, _requiredTail] = flatten(fn->argTypes);
// If too many arguments were supplied, this overload // If too many arguments were supplied, this overload

View File

@ -1159,13 +1159,7 @@ std::optional<TypeId> TypeSimplifier::basicIntersect(TypeId left, TypeId right)
for (const auto& [name, rightProp] : rt->props) for (const auto& [name, rightProp] : rt->props)
mergedProps[name] = rightProp; mergedProps[name] = rightProp;
return arena->addType(TableType{ return arena->addType(TableType{mergedProps, std::nullopt, TypeLevel{}, lt->scope, TableState::Sealed});
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}); return dest.addType(NegationType{a.ty});
else if constexpr (std::is_same_v<T, TypeFunctionInstanceType>) 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)); return dest.addType(std::move(clone));
} }
else else

View File

@ -639,6 +639,8 @@ SubtypingResult Subtyping::isCovariantWith(SubtypingEnvironment& env, TypeId sub
result = isCovariantWith(env, p); result = isCovariantWith(env, p);
else if (auto p = get2<ClassType, TableType>(subTy, superTy)) else if (auto p = get2<ClassType, TableType>(subTy, superTy))
result = isCovariantWith(env, subTy, p.first, superTy, p.second); 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)) else if (auto p = get2<PrimitiveType, TableType>(subTy, superTy))
result = isCovariantWith(env, p); result = isCovariantWith(env, p);
else if (auto p = get2<SingletonType, TableType>(subTy, superTy)) else if (auto p = get2<SingletonType, TableType>(subTy, superTy))
@ -1368,6 +1370,15 @@ SubtypingResult Subtyping::isCovariantWith(SubtypingEnvironment& env, const Func
return result; 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 Subtyping::isCovariantWith(SubtypingEnvironment& env, const PrimitiveType* subPrim, const TableType* superTable)
{ {
SubtypingResult result{false}; 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; return result;
} }

View File

@ -1036,6 +1036,9 @@ struct TypeStringifier
void operator()(TypeId, const TypeFunctionInstanceType& tfitv) 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(tfitv.function->name);
state.emit("<"); state.emit("<");

View File

@ -358,6 +358,9 @@ struct TypeFunctionReducer
if (tryGuessing(subject)) if (tryGuessing(subject))
return; return;
ctx.userFuncName = tfit->userFuncName;
ctx.userFuncBody = tfit->userFuncBody;
TypeFunctionReductionResult<TypeId> result = tfit->function->reducer(subject, tfit->typeArguments, tfit->packArguments, NotNull{&ctx}); TypeFunctionReductionResult<TypeId> result = tfit->function->reducer(subject, tfit->typeArguments, tfit->packArguments, NotNull{&ctx});
handleTypeFunctionReduction(subject, result); handleTypeFunctionReduction(subject, result);
} }
@ -567,6 +570,24 @@ static std::optional<TypeFunctionReductionResult<TypeId>> tryDistributeTypeFunct
return std::nullopt; 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( TypeFunctionReductionResult<TypeId> notTypeFunction(
TypeId instance, TypeId instance,
const std::vector<TypeId>& typeParams, const std::vector<TypeId>& typeParams,
@ -2253,7 +2274,8 @@ TypeFunctionReductionResult<TypeId> rawgetTypeFunction(
} }
BuiltinTypeFunctions::BuiltinTypeFunctions() BuiltinTypeFunctions::BuiltinTypeFunctions()
: notFunc{"not", notTypeFunction} : userFunc{"user", userDefinedTypeFunction}
, notFunc{"not", notTypeFunction}
, lenFunc{"len", lenTypeFunction} , lenFunc{"len", lenTypeFunction}
, unmFunc{"unm", unmTypeFunction} , unmFunc{"unm", unmTypeFunction}
, addFunc{"add", addTypeFunction} , 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' // where `binop' is any binary operator with a priority higher than `limit'
AstExpr* Parser::parseExpr(unsigned int limit) AstExpr* Parser::parseExpr(unsigned int limit)
{ {
// clang-format off
static const BinaryOpPriority binaryPriority[] = { static const BinaryOpPriority binaryPriority[] = {
{6, 6}, {6, 6}, {7, 7}, {7, 7}, {7, 7}, {7, 7}, // `+' `-' `*' `/' `//' `%' {6, 6}, // '+'
{10, 9}, {5, 4}, // power and concat (right associative) {6, 6}, // '-'
{3, 3}, {3, 3}, // equality and inequality {7, 7}, // '*'
{3, 3}, {3, 3}, {3, 3}, {3, 3}, // order {7, 7}, // '/'
{2, 2}, {1, 1} // logical (and/or) {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"); 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") TEST_CASE_FIXTURE(ATSFixture, "var_typepack_any_gen_table")
{ {
fileResolver.source["game/Gui/Modules/A"] = R"( fileResolver.source["game/Gui/Modules/A"] = R"(
@ -223,7 +270,10 @@ end
{ {
LUAU_ASSERT(module->ats.typeInfo.size() == 1); LUAU_ASSERT(module->ats.typeInfo.size() == 1);
LUAU_ASSERT(module->ats.typeInfo[0].code == Pattern::FuncArg); 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.size() == 11);
LUAU_ASSERT(module->ats.typeInfo[0].code == Pattern::FuncArg); 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.size() == 7);
LUAU_ASSERT(module->ats.typeInfo[0].code == Pattern::FuncArg); 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/Scope.h"
#include "Luau/ToString.h" #include "Luau/ToString.h"
#include "Luau/Type.h" #include "Luau/Type.h"
#include "Luau/TypeFunction.h"
#include "IostreamOptional.h" #include "IostreamOptional.h"
#include "ScopedFlags.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)?" == "(Parent | Unrelated | boolean | buffer | function | number | string | table | thread)?" ==
toString(normal("Not<cls & Not<Parent> & Not<Child> & Not<Unrelated>>")) 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") 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}; ScopedFastFlag sff{FFlag::DebugLuauDeferredConstraintResolution, true};
// { x: ~(false?) } // { x: ~(false?) }
TypeId t1 = arena.addType(TableType{ TypeId t1 = arena.addType(TableType{TableType::Props{{"x", builtinTypes->truthyType}}, std::nullopt, TypeLevel{}, TableState::Sealed});
TableType::Props{{"x", builtinTypes->truthyType}}, std::nullopt, TypeLevel{}, TableState::Sealed
});
// { x: number? }? // { x: number? }?
TypeId t2 = arena.addType(UnionType{{ TypeId t2 = arena.addType(UnionType{
arena.addType(TableType{ {arena.addType(TableType{TableType::Props{{"x", builtinTypes->optionalNumberType}}, std::nullopt, TypeLevel{}, TableState::Sealed}),
TableType::Props{{"x", builtinTypes->optionalNumberType}}, std::nullopt, TypeLevel{}, TableState::Sealed builtinTypes->nilType}
}), });
builtinTypes->nilType
}});
TypeId intersection = arena.addType(IntersectionType{{t2, t1}}); 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") TEST_CASE_FIXTURE(Fixture, "parse_variadics")
{ {
//clang-format off
AstStatBlock* stat = parseEx(R"( AstStatBlock* stat = parseEx(R"(
function foo(bar, ...: number): ...string function foo(bar, ...: number): ...string
end end
@ -2094,7 +2093,6 @@ TEST_CASE_FIXTURE(Fixture, "parse_variadics")
type Bar = () -> (number, ...boolean) type Bar = () -> (number, ...boolean)
)") )")
.root; .root;
//clang-format on
REQUIRE(stat); REQUIRE(stat);
REQUIRE_EQ(stat->body.size, 3); 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}})); 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 // Negated subtypes
TEST_IS_NOT_SUBTYPE(negate(builtinTypes->neverType), builtinTypes->stringType); TEST_IS_NOT_SUBTYPE(negate(builtinTypes->neverType), builtinTypes->stringType);
TEST_IS_SUBTYPE(negate(builtinTypes->unknownType), 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 // See https://github.com/luau-lang/luau/issues/767
TEST_CASE_FIXTURE(SubtypeFixture, "(...unknown) -> () <: <T>(T...) -> ()") 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}); TypeId genericTToAnys = arena.addType(FunctionType{genericAs, builtinTypes->emptyTypePack});
CHECK_MESSAGE(subtyping.isSubtype(unknownsToNothing, genericTToAnys).isSubtype, "(...unknown) -> () <: <T>(T...) -> ()"); 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") TEST_CASE_FIXTURE(SubtypeFixture, "bill")
{ {
TypeId a = arena.addType(TableType{ TypeId a = arena.addType(TableType{
{{"a", builtinTypes->stringType}}, {{"a", builtinTypes->stringType}}, TableIndexer{builtinTypes->stringType, builtinTypes->numberType}, TypeLevel{}, nullptr, TableState::Sealed
TableIndexer{
builtinTypes->stringType,
builtinTypes->numberType
},
TypeLevel{},
nullptr,
TableState::Sealed
}); });
TypeId b = arena.addType(TableType{ TypeId b = arena.addType(TableType{
{{"a", builtinTypes->stringType}}, {{"a", builtinTypes->stringType}}, TableIndexer{builtinTypes->stringType, builtinTypes->numberType}, TypeLevel{}, nullptr, TableState::Sealed
TableIndexer{
builtinTypes->stringType,
builtinTypes->numberType
},
TypeLevel{},
nullptr,
TableState::Sealed
}); });
CHECK(subtyping.isSubtype(a, b).isSubtype); 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, "({[string]: number, a: string}) -> () <: ({[string]: number, a: string}) -> ()")
TEST_CASE_FIXTURE(SubtypeFixture, "fred") TEST_CASE_FIXTURE(SubtypeFixture, "fred")
{ {
auto makeTheType = [&]() { auto makeTheType = [&]()
{
TypeId argType = arena.addType(TableType{ TypeId argType = arena.addType(TableType{
{{"a", builtinTypes->stringType}}, {{"a", builtinTypes->stringType}},
TableIndexer{ TableIndexer{builtinTypes->stringType, builtinTypes->numberType},
builtinTypes->stringType,
builtinTypes->numberType
},
TypeLevel{}, TypeLevel{},
nullptr, nullptr,
TableState::Sealed TableState::Sealed
}); });
return arena.addType(FunctionType { return arena.addType(FunctionType{arena.addTypePack({argType}), builtinTypes->emptyTypePack});
arena.addTypePack({argType}),
builtinTypes->emptyTypePack
});
}; };
TypeId a = makeTheType(); TypeId a = makeTheType();

View File

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

View File

@ -13,6 +13,7 @@ using namespace Luau;
LUAU_FASTFLAG(LuauRecursiveTypeParameterRestriction); LUAU_FASTFLAG(LuauRecursiveTypeParameterRestriction);
LUAU_FASTFLAG(DebugLuauDeferredConstraintResolution); LUAU_FASTFLAG(DebugLuauDeferredConstraintResolution);
LUAU_FASTFLAG(LuauAttributeSyntax); LUAU_FASTFLAG(LuauAttributeSyntax);
LUAU_FASTFLAG(LuauUserDefinedTypeFunctions)
TEST_SUITE_BEGIN("ToString"); 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"); CheckResult result = check("local a = nil local b = 44 local c = 'lalala' local d = true");
LUAU_REQUIRE_NO_ERRORS(result); 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' // A variable without an annotation and with a nil literal should infer as 'free', not 'nil'
CHECK_NE("nil", toString(requireType("a"))); CHECK_NE("nil", toString(requireType("a")));
}
CHECK_EQ("number", toString(requireType("b"))); CHECK_EQ("number", toString(requireType("b")));
CHECK_EQ("string", toString(requireType("c"))); CHECK_EQ("string", toString(requireType("c")));
@ -39,6 +45,8 @@ TEST_CASE_FIXTURE(Fixture, "bound_types")
TEST_CASE_FIXTURE(Fixture, "free_types") TEST_CASE_FIXTURE(Fixture, "free_types")
{ {
ScopedFastFlag sff{FFlag::DebugLuauDeferredConstraintResolution, false};
CheckResult result = check("local a"); CheckResult result = check("local a");
LUAU_REQUIRE_NO_ERRORS(result); LUAU_REQUIRE_NO_ERRORS(result);
@ -95,7 +103,6 @@ TEST_CASE_FIXTURE(Fixture, "table_respects_use_line_break")
ToStringOptions opts; ToStringOptions opts;
opts.useLineBreaks = true; opts.useLineBreaks = true;
//clang-format off
if (FFlag::DebugLuauDeferredConstraintResolution) if (FFlag::DebugLuauDeferredConstraintResolution)
CHECK_EQ( CHECK_EQ(
"{\n" "{\n"
@ -114,7 +121,6 @@ TEST_CASE_FIXTURE(Fixture, "table_respects_use_line_break")
"|}", "|}",
toString(requireType("a"), opts) toString(requireType("a"), opts)
); );
//clang-format on
} }
TEST_CASE_FIXTURE(Fixture, "nil_or_nil_is_nil_not_question_mark") 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") TEST_CASE_FIXTURE(BuiltinsFixture, "named_metatable_toStringNamedFunction")
{ {
ScopedFastFlag sff{FFlag::DebugLuauDeferredConstraintResolution, false};
CheckResult result = check(R"( CheckResult result = check(R"(
local function createTbl(): NamedMetatable local function createTbl(): NamedMetatable
return setmetatable({}, {}) 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("CYCLE"));
CHECK_EQ(std::string::npos, a.find("TRUNCATED")); 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( CHECK_EQ(
"t2 where " "t2 where "
"t1 = { __index: t1, __mul: ((t2, number) -> t2) & ((t2, t2) -> t2), new: () -> t2 } ; " "t1 = { __index: t1, __mul: ((t2, number) -> t2) & ((t2, t2) -> t2), new: () -> t2 } ; "
"t2 = { @metatable t1, {| x: number, y: number, z: number |} }", "t2 = { @metatable t1, {| x: number, y: number, z: number |} }",
a a
); );
//clang-format on }
} }
@ -263,14 +281,12 @@ TEST_CASE_FIXTURE(Fixture, "complex_intersections_printed_on_multiple_lines")
opts.useLineBreaks = true; opts.useLineBreaks = true;
opts.compositeTypesSingleLineLimit = 2; opts.compositeTypesSingleLineLimit = 2;
//clang-format off
CHECK_EQ( CHECK_EQ(
"boolean\n" "boolean\n"
"& number\n" "& number\n"
"& string", "& string",
toString(requireType("a"), opts) toString(requireType("a"), opts)
); );
//clang-format on
} }
TEST_CASE_FIXTURE(Fixture, "overloaded_functions_always_printed_on_multiple_lines") 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; ToStringOptions opts;
opts.useLineBreaks = true; opts.useLineBreaks = true;
//clang-format off
CHECK_EQ( CHECK_EQ(
"((number) -> number)\n" "((number) -> number)\n"
"& ((string) -> string)", "& ((string) -> string)",
toString(requireType("a"), opts) toString(requireType("a"), opts)
); );
//clang-format on
} }
TEST_CASE_FIXTURE(Fixture, "simple_unions_printed_on_one_line") 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.compositeTypesSingleLineLimit = 2;
opts.useLineBreaks = true; opts.useLineBreaks = true;
//clang-format off
CHECK_EQ( CHECK_EQ(
"boolean\n" "boolean\n"
"| number\n" "| number\n"
"| string", "| string",
toString(requireType("a"), opts) toString(requireType("a"), opts)
); );
//clang-format on
} }
TEST_CASE_FIXTURE(Fixture, "quit_stringifying_table_type_when_length_is_exceeded") 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") TEST_CASE_FIXTURE(Fixture, "toStringErrorPack")
{ {
ScopedFastFlag sff{FFlag::DebugLuauDeferredConstraintResolution, false};
CheckResult result = check(R"( CheckResult result = check(R"(
local function target(callback: nil) return callback(4, "hello") end 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); 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"))); 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 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"))); 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)); 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(); 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("a")) == "number");
CHECK(toString(requireType("b")) == "add<number, string>"); CHECK(toString(requireType("b")) == "add<number, string>");
CHECK(toString(requireType("c")) == "add<string, number>"); 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(
CHECK(toString(result.errors[1]) == "Operator '+' could not be applied to operands of types string and number; there is no corresponding overload for __add"); 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") 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); LUAU_REQUIRE_ERROR_COUNT(1, result);
CHECK( 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); 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") TEST_CASE_FIXTURE(Fixture, "user_defined_type_function_errors")
{ {
if (!FFlag::LuauUserDefinedTypeFunctions) if (!FFlag::LuauUserDefinedTypeFunctions)

View File

@ -692,16 +692,16 @@ TEST_CASE_FIXTURE(BuiltinsFixture, "bad_select_should_not_crash")
end end
)"); )");
LUAU_REQUIRE_ERROR_COUNT(2, result);
if (FFlag::DebugLuauDeferredConstraintResolution) if (FFlag::DebugLuauDeferredConstraintResolution)
{ {
// The argument count is the same, but the errors are currently cyclic type family instance ones. // Counterintuitively, the parametr l0 is unconstrained and therefore it is valid to pass nil.
// This isn't great, but the desired behavior here was that it didn't cause a crash and that is still true. // The new solver therefore considers that parameter to be optional.
// The larger fix for this behavior will likely be integration of egraph-based normalization throughout the new solver. LUAU_REQUIRE_ERROR_COUNT(1, result);
CHECK("Argument count mismatch. Function expects 1 argument, but none are specified" == toString(result.errors[0]));
} }
else 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 '_' 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])); 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); LUAU_REQUIRE_ERROR_COUNT(1, result);
if (FFlag::DebugLuauDeferredConstraintResolution) 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][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))", type X & Y & Z[0][2] (Z) is not a subtype of number[0] (number))",
toString(result.errors[0])); toString(result.errors[0])
);
} }
else else
CHECK_EQ( 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") 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); LUAU_REQUIRE_ERROR_COUNT(1, result);
if (FFlag::DebugLuauDeferredConstraintResolution) 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))", type boolean & false[1] (false) is not a subtype of true (true))",
toString(result.errors[0])); toString(result.errors[0])
);
} }
else else
CHECK_EQ( 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") 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) 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[1] (boolean) is not a subtype of true (true)
type boolean & false & false[2] (false) 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 else
CHECK_EQ(toString(result.errors[0]), CHECK_EQ(
"Type 'boolean & false & false' could not be converted into 'true'; none of the intersection parts are compatible"); 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") TEST_CASE_FIXTURE(Fixture, "intersect_saturate_overloaded_functions")

View File

@ -55,11 +55,26 @@ TEST_CASE_FIXTURE(Fixture, "typeguard_inference_incomplete")
end 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)); CHECK_EQ(expected, decorateWithTypes(code));
} }
TEST_CASE_FIXTURE(BuiltinsFixture, "luau-polyfill.Array.filter") 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 // This test exercises the fact that we should reduce sealed/unsealed/free tables
// res is a unsealed table with type {((T & ~nil)?) & any} // 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} // 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. // For now, infer it as just a free table.
TEST_CASE_FIXTURE(BuiltinsFixture, "setmetatable_constrains_free_type_into_free_table") TEST_CASE_FIXTURE(BuiltinsFixture, "setmetatable_constrains_free_type_into_free_table")
{ {
ScopedFastFlag sff{FFlag::DebugLuauDeferredConstraintResolution, false};
CheckResult result = check(R"( CheckResult result = check(R"(
local a = {} local a = {}
local b 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. // Luau currently doesn't yet know how to allow assignments when the binding was refined.
TEST_CASE_FIXTURE(Fixture, "while_body_are_also_refined") TEST_CASE_FIXTURE(Fixture, "while_body_are_also_refined")
{ {
ScopedFastFlag sff{FFlag::DebugLuauDeferredConstraintResolution, false};
CheckResult result = check(R"( CheckResult result = check(R"(
type Node<T> = { value: T, child: Node<T>? } 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. // 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") TEST_CASE_FIXTURE(BuiltinsFixture, "error_on_eq_metamethod_returning_a_type_other_than_boolean")
{ {
ScopedFastFlag sff{FFlag::DebugLuauDeferredConstraintResolution, false};
CheckResult result = check(R"( CheckResult result = check(R"(
local tab = {a = 1} local tab = {a = 1}
setmetatable(tab, {__eq = function(a, b): number 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) if (FFlag::DebugLuauDeferredConstraintResolution)
{ {
CHECK_EQ("{ x: string, y: number }", toString(requireTypeAtPosition({5, 28}))); CHECK_EQ("{ x: string, y: number }", toString(requireTypeAtPosition({5, 28})));
CHECK_EQ("{ x: nil, y: nil }", toString(requireTypeAtPosition({7, 28})));
// Should be { x: nil, y: nil }
CHECK_EQ("{ x: nil, y: nil } | { x: string, y: number }", toString(requireTypeAtPosition({7, 28})));
} }
else else
{ {
@ -341,11 +359,20 @@ TEST_CASE_FIXTURE(Fixture, "do_not_ice_when_trying_to_pick_first_of_generic_type
LUAU_REQUIRE_NO_ERRORS(result); 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 () -> () // f and g should have the type () -> ()
CHECK_EQ("() -> (a...)", toString(requireType("f"))); CHECK_EQ("() -> (a...)", toString(requireType("f")));
CHECK_EQ("<a...>() -> (a...)", toString(requireType("g"))); CHECK_EQ("<a...>() -> (a...)", toString(requireType("g")));
CHECK_EQ("any", toString(requireType("x"))); // any is returned instead of ICE for now CHECK_EQ("any", toString(requireType("x"))); // any is returned instead of ICE for now
} }
}
TEST_CASE_FIXTURE(Fixture, "specialization_binds_with_prototypes_too_early") 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 local s2s: (string) -> string = id
)"); )");
if (FFlag::DebugLuauDeferredConstraintResolution)
LUAU_REQUIRE_NO_ERRORS(result);
else
LUAU_REQUIRE_ERRORS(result); // Should not have any errors. 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") TEST_CASE_FIXTURE(Fixture, "free_options_cannot_be_unified_together")
{ {
ScopedFastFlag sff{FFlag::DebugLuauDeferredConstraintResolution, false};
TypeArena arena; TypeArena arena;
TypeId nilType = builtinTypes->nilType; TypeId nilType = builtinTypes->nilType;
@ -569,7 +601,7 @@ return wrapStrictTable(Constants, "Constants")
std::optional<TypeId> result = first(m->returnType); std::optional<TypeId> result = first(m->returnType);
REQUIRE(result); REQUIRE(result);
if (FFlag::DebugLuauDeferredConstraintResolution) if (FFlag::DebugLuauDeferredConstraintResolution)
CHECK_EQ("(any & ~(*error-type* | table))?", toString(*result)); CHECK_EQ("unknown", toString(*result));
else else
CHECK_MESSAGE(get<AnyType>(*result), *result); CHECK_MESSAGE(get<AnyType>(*result), *result);
} }
@ -610,7 +642,11 @@ return wrapStrictTable(Constants, "Constants")
std::optional<TypeId> result = first(m->returnType); std::optional<TypeId> result = first(m->returnType);
REQUIRE(result); REQUIRE(result);
CHECK(get<AnyType>(*result));
if (FFlag::DebugLuauDeferredConstraintResolution)
CHECK("unknown" == toString(*result));
else
CHECK("any" == toString(*result));
} }
namespace namespace
@ -793,6 +829,10 @@ TEST_CASE_FIXTURE(Fixture, "assign_table_with_refined_property_with_a_similar_ty
end 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); LUAU_REQUIRE_ERROR_COUNT(1, result);
const std::string expected = R"(Type const std::string expected = R"(Type
'{| x: number? |}' '{| x: number? |}'
@ -803,9 +843,12 @@ caused by:
Type 'number?' could not be converted into 'number' in an invariant context)"; Type 'number?' could not be converted into 'number' in an invariant context)";
CHECK_EQ(expected, toString(result.errors[0])); CHECK_EQ(expected, toString(result.errors[0]));
} }
}
TEST_CASE_FIXTURE(BuiltinsFixture, "table_insert_with_a_singleton_argument") TEST_CASE_FIXTURE(BuiltinsFixture, "table_insert_with_a_singleton_argument")
{ {
ScopedFastFlag sff{FFlag::DebugLuauDeferredConstraintResolution, false};
CheckResult result = check(R"( CheckResult result = check(R"(
local function foo(t, x) local function foo(t, x)
if x == "hi" or x == "bye" then 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) 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 // This is actually wrong! Sort of. It's doing the wrong thing, it's actually asking whether
// `{| val: number? |} <: {| val: nil |}` // `{| val: number? |} <: {| val: nil |}`
// instead of the correct way, which is // instead of the correct way, which is
// `{| val: nil |} <: {| val: number? |}` // `{| val: nil |} <: {| val: number? |}`
LUAU_REQUIRE_NO_ERRORS(result); LUAU_REQUIRE_NO_ERRORS(result);
} }
}
TEST_CASE_FIXTURE(Fixture, "floating_generics_should_not_be_allowed") TEST_CASE_FIXTURE(Fixture, "floating_generics_should_not_be_allowed")
{ {
ScopedFastFlag sff{FFlag::DebugLuauDeferredConstraintResolution, false};
CheckResult result = check(R"( 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) 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") TEST_CASE_FIXTURE(Fixture, "free_options_can_be_unified_together")
{ {
ScopedFastFlag sff{FFlag::DebugLuauDeferredConstraintResolution, false};
TypeArena arena; TypeArena arena;
TypeId nilType = builtinTypes->nilType; TypeId nilType = builtinTypes->nilType;
@ -935,8 +990,10 @@ TEST_CASE_FIXTURE(Fixture, "unify_more_complex_unions_that_include_nil")
LUAU_REQUIRE_NO_ERRORS(result); 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); createSomeClasses(&frontend);
CheckResult result = check(R"( CheckResult result = check(R"(
@ -951,6 +1008,24 @@ TEST_CASE_FIXTURE(Fixture, "optional_class_instances_are_invariant")
LUAU_REQUIRE_NO_ERRORS(result); 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") 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 // 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") 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) #if defined(_NOOPT) || defined(_DEBUG)
ScopedFastInt LuauTypeInferRecursionLimit{FInt::LuauTypeInferRecursionLimit, 100}; ScopedFastInt LuauTypeInferRecursionLimit{FInt::LuauTypeInferRecursionLimit, 100};
#endif #endif
@ -1027,17 +1105,9 @@ local tbl = require(game.A)
tbl:f3() 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"); CheckResult result = frontend.check("game/B");
LUAU_REQUIRE_ERROR_COUNT(1, result); LUAU_REQUIRE_ERROR_COUNT(1, result);
} }
}
// Ideally, unification with any will not cause a 2^n normalization of a function overload // Ideally, unification with any will not cause a 2^n normalization of a function overload
TEST_CASE_FIXTURE(BuiltinsFixture, "normalization_limit_in_unify_with_any") TEST_CASE_FIXTURE(BuiltinsFixture, "normalization_limit_in_unify_with_any")
@ -1148,7 +1218,8 @@ TEST_CASE_FIXTURE(BuiltinsFixture, "luau_roact_useState_minimization")
update("hello") 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") 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") TEST_CASE_FIXTURE(BuiltinsFixture, "typeguard_in_assert_position")
{ {
CheckResult result = check(R"( CheckResult result = check(R"(
local a function f(a)
assert(type(a) == "number") assert(type(a) == "number")
local b = a local b = a
return b
end
)"); )");
LUAU_REQUIRE_NO_ERRORS(result); 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") 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 end
)"); )");
if (FFlag::DebugLuauDeferredConstraintResolution)
LUAU_REQUIRE_ERROR_COUNT(1, result);
else
LUAU_REQUIRE_ERROR_COUNT(2, result); 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("Type 'string' could not be converted into 'number'" == toString(result.errors[1]));
CHECK_EQ("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") 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) 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}))); CHECK("number" == toString(requireTypeAtPosition({5, 26})));
} }
@ -699,6 +703,9 @@ TEST_CASE_FIXTURE(BuiltinsFixture, "type_narrow_to_vector")
LUAU_REQUIRE_NO_ERRORS(result); LUAU_REQUIRE_NO_ERRORS(result);
if (FFlag::DebugLuauDeferredConstraintResolution)
CHECK_EQ("never", toString(requireTypeAtPosition({3, 28})));
else
CHECK_EQ("*error-type*", toString(requireTypeAtPosition({3, 28}))); CHECK_EQ("*error-type*", toString(requireTypeAtPosition({3, 28})));
} }
@ -722,12 +729,24 @@ TEST_CASE_FIXTURE(BuiltinsFixture, "nonoptional_type_can_narrow_to_nil_if_sense_
LUAU_REQUIRE_NO_ERRORS(result); 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("nil", toString(requireTypeAtPosition({4, 24}))); // type(v) == "nil"
CHECK_EQ("string", toString(requireTypeAtPosition({6, 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("nil", toString(requireTypeAtPosition({10, 24}))); // equivalent to type(v) == "nil"
CHECK_EQ("string", toString(requireTypeAtPosition({12, 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") 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); 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}))); 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); LUAU_REQUIRE_NO_ERRORS(result);
if (FFlag::DebugLuauDeferredConstraintResolution) 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 else
CHECK_EQ("{| x: boolean |}?", toString(requireTypeAtPosition({3, 28}))); CHECK_EQ("{| x: boolean |}?", toString(requireTypeAtPosition({3, 28})));
} }
@ -1196,12 +1224,18 @@ TEST_CASE_FIXTURE(Fixture, "discriminate_from_truthiness_of_x")
LUAU_REQUIRE_NO_ERRORS(result); LUAU_REQUIRE_NO_ERRORS(result);
CHECK_EQ(R"({| tag: "exists", x: string |})", toString(requireTypeAtPosition({5, 28})));
if (FFlag::DebugLuauDeferredConstraintResolution) 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 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}))); CHECK_EQ(R"({| tag: "exists", x: string |} | {| tag: "missing", x: nil |})", toString(requireTypeAtPosition({7, 28})));
} }
}
TEST_CASE_FIXTURE(Fixture, "discriminate_tag") TEST_CASE_FIXTURE(Fixture, "discriminate_tag")
{ {
@ -1328,8 +1362,8 @@ TEST_CASE_FIXTURE(RefinementClassFixture, "discriminate_from_isa_of_x")
if (FFlag::DebugLuauDeferredConstraintResolution) if (FFlag::DebugLuauDeferredConstraintResolution)
{ {
CHECK_EQ(R"({ tag: "Part", x: Part })", toString(requireTypeAtPosition({5, 28}))); CHECK(R"({ tag: "Part", x: Part })" == toString(requireTypeAtPosition({5, 28})));
CHECK_EQ(R"({ tag: "Folder", x: Folder })", toString(requireTypeAtPosition({7, 28}))); CHECK(R"({ tag: "Folder", x: Folder })" == toString(requireTypeAtPosition({7, 28})));
} }
else else
{ {
@ -1340,6 +1374,9 @@ TEST_CASE_FIXTURE(RefinementClassFixture, "discriminate_from_isa_of_x")
TEST_CASE_FIXTURE(RefinementClassFixture, "typeguard_cast_free_table_to_vector") 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"( CheckResult result = check(R"(
local function f(vec) local function f(vec)
local X, Y, Z = vec.X, vec.Y, vec.Z 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") 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"( CheckResult result = check(R"(
local function f(x): Instance local function f(x): Instance
if x:IsA("Folder") then 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); 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 end
if typeof(s) == "nil" then if typeof(s) == "nil" then
local foo = s local foo = s -- line 18
else else
local foo = s local foo = s -- line 20
end end
end end
)"); )");
@ -1860,7 +1901,8 @@ TEST_CASE_FIXTURE(BuiltinsFixture, "dataflow_analysis_can_tell_refinements_when_
if (FFlag::DebugLuauDeferredConstraintResolution) 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}))); CHECK_EQ("string", toString(requireTypeAtPosition({20, 28})));
} }
else else
@ -1948,7 +1990,10 @@ TEST_CASE_FIXTURE(BuiltinsFixture, "type_annotations_arent_relevant_when_doing_d
CHECK_EQ("nil", toString(requireTypeAtPosition({8, 28}))); CHECK_EQ("nil", toString(requireTypeAtPosition({8, 28})));
if (FFlag::DebugLuauDeferredConstraintResolution) if (FFlag::DebugLuauDeferredConstraintResolution)
CHECK_EQ("never", toString(requireTypeAtPosition({9, 28}))); {
// CLI-115478 - This should be never
CHECK_EQ("nil", toString(requireTypeAtPosition({9, 28})));
}
else else
CHECK_EQ("nil", toString(requireTypeAtPosition({9, 28}))); 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") TEST_CASE_FIXTURE(BuiltinsFixture, "conditional_refinement_should_stay_error_suppressing")
{ {
ScopedFastFlag sff{FFlag::DebugLuauDeferredConstraintResolution, true}; 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"( CheckResult result = check(R"(
local function test(element: any?) local function test(element: any?)
@ -2065,6 +2109,12 @@ TEST_CASE_FIXTURE(BuiltinsFixture, "globals_can_be_narrowed_too")
end end
)"); )");
if (FFlag::DebugLuauDeferredConstraintResolution)
{
// CLI-114134
CHECK("string & typeof(string)" == toString(requireTypeAtPosition(Position{2, 24})));
}
else
CHECK("never" == toString(requireTypeAtPosition(Position{2, 24}))); 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); 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") TEST_CASE_FIXTURE(BuiltinsFixture, "ex")
{ {
CheckResult result = check(R"( CheckResult result = check(R"(

View File

@ -2536,7 +2536,10 @@ local y: number = tmp.p.y
LUAU_REQUIRE_ERROR_COUNT(1, result); LUAU_REQUIRE_ERROR_COUNT(1, result);
if (FFlag::DebugLuauDeferredConstraintResolution) 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 else
{ {
const std::string expected = R"(Type 'tmp' could not be converted into 'HasSuper' 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") 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"( CheckResult result = check(R"(
local count = 0 local count = 0
function most_of_the_natural_numbers(): number? function most_of_the_natural_numbers(): number?
@ -51,6 +55,27 @@ TEST_CASE_FIXTURE(Fixture, "return_types_can_be_disjoint")
REQUIRE(utv != nullptr); 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") TEST_CASE_FIXTURE(Fixture, "allow_specific_assign")
{ {
CheckResult result = check(R"( CheckResult result = check(R"(
@ -95,6 +120,9 @@ TEST_CASE_FIXTURE(Fixture, "optional_arguments")
TEST_CASE_FIXTURE(Fixture, "optional_arguments_table") 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"( CheckResult result = check(R"(
local a:{a:string, b:string?} local a:{a:string, b:string?}
a = {a="ok"} 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])); CHECK_EQ("Key 'x' is missing from 'B' in the type 'A | B'", toString(result.errors[0]));
if (FFlag::DebugLuauDeferredConstraintResolution) if (FFlag::DebugLuauDeferredConstraintResolution)
CHECK_EQ("(A | B) -> number | *error-type*", toString(requireType("f"))); CHECK_EQ("(A | B) -> number", toString(requireType("f")));
else else
CHECK_EQ("(A | B) -> *error-type*", toString(requireType("f"))); 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); LUAU_REQUIRE_ERROR_COUNT(1, result);
CHECK_EQ("Value of type 'A?' could be nil", toString(result.errors[0])); 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"))); CHECK_EQ("(A?) -> number", toString(requireType("f")));
} }
@ -282,10 +306,6 @@ TEST_CASE_FIXTURE(Fixture, "optional_union_functions")
LUAU_REQUIRE_ERROR_COUNT(1, result); LUAU_REQUIRE_ERROR_COUNT(1, result);
CHECK_EQ("Value of type 'A?' could be nil", toString(result.errors[0])); 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"))); CHECK_EQ("(A?) -> number", toString(requireType("f")));
} }
@ -303,10 +323,6 @@ TEST_CASE_FIXTURE(Fixture, "optional_union_methods")
LUAU_REQUIRE_ERROR_COUNT(1, result); LUAU_REQUIRE_ERROR_COUNT(1, result);
CHECK_EQ("Value of type 'A?' could be nil", toString(result.errors[0])); 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"))); CHECK_EQ("(A?) -> number", toString(requireType("f")));
} }
@ -456,6 +472,8 @@ end
TEST_CASE_FIXTURE(Fixture, "unify_unsealed_table_union_check") TEST_CASE_FIXTURE(Fixture, "unify_unsealed_table_union_check")
{ {
ScopedFastFlag sff{FFlag::DebugLuauDeferredConstraintResolution, false};
CheckResult result = check(R"( CheckResult result = check(R"(
local x = { x = 3 } local x = { x = 3 }
type A = number? type A = number?
@ -547,6 +565,9 @@ local a: XYZ = { w = 4 }
)"); )");
LUAU_REQUIRE_ERROR_COUNT(1, result); 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)"); 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,12 +580,17 @@ local a: X? = { w = 4 }
)"); )");
LUAU_REQUIRE_ERROR_COUNT(1, result); 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?' const std::string expected = R"(Type 'a' could not be converted into 'X?'
caused by: caused by:
None of the union options are compatible. For example: None of the union options are compatible. For example:
Table type 'a' not compatible with type 'X' because the former is missing field 'x')"; Table type 'a' not compatible with type 'X' because the former is missing field 'x')";
CHECK_EQ(expected, toString(result.errors[0])); CHECK_EQ(expected, toString(result.errors[0]));
} }
}
// We had a bug where a cyclic union caused a stack overflow. // We had a bug where a cyclic union caused a stack overflow.
// ex type U = number | U // ex type U = number | U
@ -615,6 +641,7 @@ TEST_CASE_FIXTURE(Fixture, "indexing_into_a_cyclic_union_doesnt_crash")
TEST_CASE_FIXTURE(BuiltinsFixture, "table_union_write_indirect") TEST_CASE_FIXTURE(BuiltinsFixture, "table_union_write_indirect")
{ {
ScopedFastFlag sff{FFlag::DebugLuauDeferredConstraintResolution, false};
CheckResult result = check(R"( CheckResult result = check(R"(
type A = { x: number, y: (number) -> string } | { z: number, y: (number) -> string } 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") TEST_CASE_FIXTURE(Fixture, "union_of_functions_mentioning_generics")
{ {
ScopedFastFlag sff{FFlag::DebugLuauDeferredConstraintResolution, false};
CheckResult result = check(R"( CheckResult result = check(R"(
function f<a,b>() function f<a,b>()
function g(x : (a) -> a?) 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") TEST_CASE_FIXTURE(Fixture, "union_of_functions_mentioning_generic_typepacks")
{ {
ScopedFastFlag sff{FFlag::DebugLuauDeferredConstraintResolution, false};
CheckResult result = check(R"( CheckResult result = check(R"(
function f<a...>() function f<a...>()
function g(x : (number, a...) -> (number?, 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") TEST_CASE_FIXTURE(Fixture, "union_of_functions_with_mismatching_arg_arities")
{ {
ScopedFastFlag sff{FFlag::DebugLuauDeferredConstraintResolution, false};
CheckResult result = check(R"( CheckResult result = check(R"(
function f(x : (number) -> number?) function f(x : (number) -> number?)
local y : ((number?) -> number) | ((number | string) -> nil) = x -- OK 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") TEST_CASE_FIXTURE(Fixture, "union_of_functions_with_mismatching_result_arities")
{ {
ScopedFastFlag sff{FFlag::DebugLuauDeferredConstraintResolution, false};
CheckResult result = check(R"( CheckResult result = check(R"(
function f(x : () -> (number | string)) function f(x : () -> (number | string))
local y : (() -> number) | (() -> string) = x -- OK local y : (() -> number) | (() -> string) = x -- OK
@ -761,6 +796,8 @@ could not be converted into
TEST_CASE_FIXTURE(Fixture, "union_of_functions_with_variadics") TEST_CASE_FIXTURE(Fixture, "union_of_functions_with_variadics")
{ {
ScopedFastFlag sff{FFlag::DebugLuauDeferredConstraintResolution, false};
CheckResult result = check(R"( CheckResult result = check(R"(
function f(x : (...nil) -> (...number?)) function f(x : (...nil) -> (...number?))
local y : ((...string?) -> (...number)) | ((...number?) -> nil) = x -- OK 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); 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 const std::string expected = R"(Type
'(number) -> ()' '(number) -> ()'
could not be converted into could not be converted into
'((...number?) -> ()) | ((number?) -> ())'; none of the union options are compatible)"; '((...number?) -> ()) | ((number?) -> ())'; none of the union options are compatible)";
CHECK_EQ(expected, toString(result.errors[0])); CHECK_EQ(expected, toString(result.errors[0]));
} }
}
TEST_CASE_FIXTURE(Fixture, "union_of_functions_with_mismatching_result_variadics") TEST_CASE_FIXTURE(Fixture, "union_of_functions_with_mismatching_result_variadics")
{ {
ScopedFastFlag sff{FFlag::DebugLuauDeferredConstraintResolution, false};
CheckResult result = check(R"( CheckResult result = check(R"(
function f(x : () -> (number?, ...number)) function f(x : () -> (number?, ...number))
local y : (() -> (...number)) | (() -> nil) = x -- OK 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); 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") 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); LUAU_REQUIRE_NO_ERRORS(result);
if (FFlag::DebugLuauDeferredConstraintResolution)
CHECK_EQ("({ x: number } | { x: string }) -> number | string", toString(requireType("f"))); 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") 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") TEST_CASE_FIXTURE(Fixture, "union_function_any_args")
{ {
ScopedFastFlag sff{FFlag::DebugLuauDeferredConstraintResolution, false};
CheckResult result = check(R"( CheckResult result = check(R"(
function f(sup : ((...any) -> (...any))?, sub : ((number) -> (...any))) function f(sup : ((...any) -> (...any))?, sub : ((number) -> (...any)))
sup = sub sup = sub
@ -886,6 +934,8 @@ TEST_CASE_FIXTURE(Fixture, "optional_any")
TEST_CASE_FIXTURE(Fixture, "generic_function_with_optional_arg") TEST_CASE_FIXTURE(Fixture, "generic_function_with_optional_arg")
{ {
ScopedFastFlag sff{FFlag::DebugLuauDeferredConstraintResolution, false};
CheckResult result = check(R"( CheckResult result = check(R"(
function f<T>(x : T?) : {T} function f<T>(x : T?) : {T}
local result = {} local result = {}

View File

@ -63,53 +63,9 @@ Negations.cofinite_strings_can_be_compared_for_equality
Normalize.higher_order_function_with_annotation Normalize.higher_order_function_with_annotation
Normalize.negations_of_tables Normalize.negations_of_tables
Normalize.specific_functions_cannot_be_negated 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_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_as_any_if_x_is_instance_elseif_x_is_table
RefinementTest.x_is_not_instance_or_else_not_part 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.members_of_failed_typepack_unification_are_unified_with_errorType
TryUnifyTests.result_of_failed_typepack_unification_is_constrained TryUnifyTests.result_of_failed_typepack_unification_is_constrained
TryUnifyTests.uninhabited_table_sub_anything TryUnifyTests.uninhabited_table_sub_anything
@ -261,24 +217,4 @@ TypeSingletons.return_type_of_f_is_not_widened
TypeSingletons.table_properties_type_error_escapes TypeSingletons.table_properties_type_error_escapes
TypeSingletons.widen_the_supertype_if_it_is_free_and_subtype_has_singleton TypeSingletons.widen_the_supertype_if_it_is_free_and_subtype_has_singleton
TypeStatesTest.typestates_preserve_error_suppression_properties 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 VisitType.throw_when_limit_is_exceeded