Sync to upstream/release/597

This commit is contained in:
Alexander McCord 2023-09-29 17:22:06 -07:00
parent 81681e2948
commit 3bfc864280
49 changed files with 2006 additions and 316 deletions

View file

@ -106,6 +106,14 @@ struct ConstraintGraphBuilder
std::function<void(const ModuleName&, const ScopePtr&)> prepareModuleScope, DcrLogger* logger, NotNull<DataFlowGraph> dfg,
std::vector<RequireCycle> requireCycles);
/**
* The entry point to the ConstraintGraphBuilder. This will construct a set
* of scopes, constraints, and free types that can be solved later.
* @param block the root block to generate constraints for.
*/
void visitModuleRoot(AstStatBlock* block);
private:
/**
* Fabricates a new free type belonging to a given scope.
* @param scope the scope the free type belongs to.
@ -143,13 +151,6 @@ struct ConstraintGraphBuilder
void applyRefinements(const ScopePtr& scope, Location location, RefinementId refinement);
/**
* The entry point to the ConstraintGraphBuilder. This will construct a set
* of scopes, constraints, and free types that can be solved later.
* @param block the root block to generate constraints for.
*/
void visit(AstStatBlock* block);
ControlFlow visitBlockWithoutChildScope(const ScopePtr& scope, AstStatBlock* block);
ControlFlow visit(const ScopePtr& scope, AstStat* stat);
@ -172,7 +173,8 @@ struct ConstraintGraphBuilder
ControlFlow visit(const ScopePtr& scope, AstStatError* error);
InferencePack checkPack(const ScopePtr& scope, AstArray<AstExpr*> exprs, const std::vector<std::optional<TypeId>>& expectedTypes = {});
InferencePack checkPack(const ScopePtr& scope, AstExpr* expr, const std::vector<std::optional<TypeId>>& expectedTypes = {});
InferencePack checkPack(
const ScopePtr& scope, AstExpr* expr, const std::vector<std::optional<TypeId>>& expectedTypes = {}, bool generalize = true);
InferencePack checkPack(const ScopePtr& scope, AstExprCall* call);
@ -182,10 +184,11 @@ struct ConstraintGraphBuilder
* @param expr the expression to check.
* @param expectedType the type of the expression that is expected from its
* surrounding context. Used to implement bidirectional type checking.
* @param generalize If true, generalize any lambdas that are encountered.
* @return the type of the expression.
*/
Inference check(const ScopePtr& scope, AstExpr* expr, ValueContext context = ValueContext::RValue, std::optional<TypeId> expectedType = {},
bool forceSingleton = false);
bool forceSingleton = false, bool generalize = true);
Inference check(const ScopePtr& scope, AstExprConstantString* string, std::optional<TypeId> expectedType, bool forceSingleton);
Inference check(const ScopePtr& scope, AstExprConstantBool* bool_, std::optional<TypeId> expectedType, bool forceSingleton);
@ -193,7 +196,7 @@ struct ConstraintGraphBuilder
Inference check(const ScopePtr& scope, AstExprGlobal* global);
Inference check(const ScopePtr& scope, AstExprIndexName* indexName);
Inference check(const ScopePtr& scope, AstExprIndexExpr* indexExpr);
Inference check(const ScopePtr& scope, AstExprFunction* func, std::optional<TypeId> expectedType);
Inference check(const ScopePtr& scope, AstExprFunction* func, std::optional<TypeId> expectedType, bool generalize);
Inference check(const ScopePtr& scope, AstExprUnary* unary);
Inference check(const ScopePtr& scope, AstExprBinary* binary, std::optional<TypeId> expectedType);
Inference check(const ScopePtr& scope, AstExprIfElse* ifElse, std::optional<TypeId> expectedType);

View file

@ -14,8 +14,8 @@ enum class ControlFlow
None = 0b00001,
Returns = 0b00010,
Throws = 0b00100,
Break = 0b01000, // Currently unused.
Continue = 0b10000, // Currently unused.
Breaks = 0b01000,
Continues = 0b10000,
};
inline ControlFlow operator&(ControlFlow a, ControlFlow b)

View file

@ -0,0 +1,15 @@
// This file is part of the Luau programming language and is licensed under MIT License; see LICENSE.txt for details
#pragma once
#include "Luau/Module.h"
#include "Luau/NotNull.h"
namespace Luau
{
struct BuiltinTypes;
void checkNonStrict(NotNull<BuiltinTypes> builtinTypes, Module* module);
} // namespace Luau

View file

@ -8,7 +8,6 @@
#include "Luau/TypePack.h"
#include "Luau/Unifiable.h"
LUAU_FASTFLAG(DebugLuauCopyBeforeNormalizing)
LUAU_FASTFLAG(DebugLuauReadWriteProperties)
LUAU_FASTFLAG(DebugLuauDeferredConstraintResolution)
@ -253,8 +252,10 @@ private:
void cloneChildren(FreeType* t)
{
// TODO: clone lower and upper bounds.
// TODO: In the new solver, we should ice.
if (t->lowerBound)
t->lowerBound = shallowClone(t->lowerBound);
if (t->upperBound)
t->upperBound = shallowClone(t->upperBound);
}
void cloneChildren(GenericType* t)
@ -376,7 +377,11 @@ private:
void cloneChildren(TypeFamilyInstanceType* t)
{
// TODO: In the new solver, we should ice.
for (TypeId& ty : t->typeArguments)
ty = shallowClone(ty);
for (TypePackId& tp : t->packArguments)
tp = shallowClone(tp);
}
void cloneChildren(FreeTypePack* t)
@ -416,7 +421,11 @@ private:
void cloneChildren(TypeFamilyInstanceTypePack* t)
{
// TODO: In the new solver, we should ice.
for (TypeId& ty : t->typeArguments)
ty = shallowClone(ty);
for (TypePackId& tp : t->packArguments)
tp = shallowClone(tp);
}
};
@ -560,8 +569,6 @@ struct TypePackCloner
void operator()(const Unifiable::Bound<TypePackId>& t)
{
TypePackId cloned = clone(t.boundTo, dest, cloneState);
if (FFlag::DebugLuauCopyBeforeNormalizing)
cloned = dest.addTypePack(TypePackVar{BoundTypePack{cloned}});
seenTypePacks[typePackId] = cloned;
}
@ -629,8 +636,6 @@ void TypeCloner::operator()(const GenericType& t)
void TypeCloner::operator()(const Unifiable::Bound<TypeId>& t)
{
TypeId boundTo = clone(t.boundTo, dest, cloneState);
if (FFlag::DebugLuauCopyBeforeNormalizing)
boundTo = dest.addType(BoundType{boundTo});
seenTypes[typeId] = boundTo;
}
@ -701,7 +706,7 @@ void TypeCloner::operator()(const FunctionType& t)
void TypeCloner::operator()(const TableType& t)
{
// If table is now bound to another one, we ignore the content of the original
if (!FFlag::DebugLuauCopyBeforeNormalizing && t.boundTo)
if (t.boundTo)
{
TypeId boundTo = clone(*t.boundTo, dest, cloneState);
seenTypes[typeId] = boundTo;
@ -718,9 +723,6 @@ void TypeCloner::operator()(const TableType& t)
ttv->level = TypeLevel{0, 0};
if (FFlag::DebugLuauCopyBeforeNormalizing && t.boundTo)
ttv->boundTo = clone(*t.boundTo, dest, cloneState);
for (const auto& [name, prop] : t.props)
ttv->props[name] = clone(prop, dest, cloneState);

View file

@ -24,6 +24,7 @@ LUAU_FASTINT(LuauCheckRecursionLimit);
LUAU_FASTFLAG(DebugLuauLogSolverToJson);
LUAU_FASTFLAG(DebugLuauMagicTypes);
LUAU_FASTFLAG(LuauParseDeclareClassIndexer);
LUAU_FASTFLAG(LuauLoopControlFlowAnalysis);
LUAU_FASTFLAG(LuauFloorDivision);
namespace Luau
@ -159,6 +160,25 @@ ConstraintGraphBuilder::ConstraintGraphBuilder(ModulePtr module, NotNull<Normali
LUAU_ASSERT(module);
}
void ConstraintGraphBuilder::visitModuleRoot(AstStatBlock* block)
{
LUAU_ASSERT(scopes.empty());
LUAU_ASSERT(rootScope == nullptr);
ScopePtr scope = std::make_shared<Scope>(globalScope);
rootScope = scope.get();
scopes.emplace_back(block->location, scope);
module->astScopes[block] = NotNull{scope.get()};
rootScope->returnType = freshTypePack(scope);
prepopulateGlobalScope(scope, block);
visitBlockWithoutChildScope(scope, block);
if (logger)
logger->captureGenerationModule(module);
}
TypeId ConstraintGraphBuilder::freshType(const ScopePtr& scope)
{
return Luau::freshType(arena, builtinTypes, scope.get());
@ -443,25 +463,6 @@ void ConstraintGraphBuilder::applyRefinements(const ScopePtr& scope, Location lo
addConstraint(scope, location, c);
}
void ConstraintGraphBuilder::visit(AstStatBlock* block)
{
LUAU_ASSERT(scopes.empty());
LUAU_ASSERT(rootScope == nullptr);
ScopePtr scope = std::make_shared<Scope>(globalScope);
rootScope = scope.get();
scopes.emplace_back(block->location, scope);
module->astScopes[block] = NotNull{scope.get()};
rootScope->returnType = freshTypePack(scope);
prepopulateGlobalScope(scope, block);
visitBlockWithoutChildScope(scope, block);
if (logger)
logger->captureGenerationModule(module);
}
ControlFlow ConstraintGraphBuilder::visitBlockWithoutChildScope(const ScopePtr& scope, AstStatBlock* block)
{
RecursionCounter counter{&recursionCount};
@ -537,11 +538,10 @@ ControlFlow ConstraintGraphBuilder::visit(const ScopePtr& scope, AstStat* stat)
return visit(scope, s);
else if (auto s = stat->as<AstStatRepeat>())
return visit(scope, s);
else if (stat->is<AstStatBreak>() || stat->is<AstStatContinue>())
{
// Nothing
return ControlFlow::None; // TODO: ControlFlow::Break/Continue
}
else if (stat->is<AstStatBreak>())
return FFlag::LuauLoopControlFlowAnalysis ? ControlFlow::Breaks : ControlFlow::None;
else if (stat->is<AstStatContinue>())
return FFlag::LuauLoopControlFlowAnalysis ? ControlFlow::Continues : ControlFlow::None;
else if (auto r = stat->as<AstStatReturn>())
return visit(scope, r);
else if (auto e = stat->as<AstStatExpr>())
@ -616,7 +616,7 @@ ControlFlow ConstraintGraphBuilder::visit(const ScopePtr& scope, AstStatLocal* l
// See the test TypeInfer/infer_locals_with_nil_value. Better flow
// awareness should make this obsolete.
if (!varTypes[i])
if (i < varTypes.size() && !varTypes[i])
varTypes[i] = freshType(scope);
}
// Only function calls and vararg expressions can produce packs. All
@ -627,7 +627,7 @@ ControlFlow ConstraintGraphBuilder::visit(const ScopePtr& scope, AstStatLocal* l
if (hasAnnotation)
expectedType = varTypes.at(i);
TypeId exprType = check(scope, value, ValueContext::RValue, expectedType).ty;
TypeId exprType = check(scope, value, ValueContext::RValue, expectedType, /*forceSingleton*/ false, /*generalize*/ true).ty;
if (i < varTypes.size())
{
if (varTypes[i])
@ -645,7 +645,7 @@ ControlFlow ConstraintGraphBuilder::visit(const ScopePtr& scope, AstStatLocal* l
if (hasAnnotation)
expectedTypes.insert(begin(expectedTypes), begin(varTypes) + i, end(varTypes));
TypePackId exprPack = checkPack(scope, value, expectedTypes).tp;
TypePackId exprPack = checkPack(scope, value, expectedTypes, /*generalize*/ true).tp;
if (i < local->vars.size)
{
@ -1072,12 +1072,14 @@ ControlFlow ConstraintGraphBuilder::visit(const ScopePtr& scope, AstStatIf* ifSt
if (ifStatement->elsebody)
elsecf = visit(elseScope, ifStatement->elsebody);
if (matches(thencf, ControlFlow::Returns | ControlFlow::Throws) && elsecf == ControlFlow::None)
if (thencf != ControlFlow::None && elsecf == ControlFlow::None)
scope->inheritRefinements(elseScope);
else if (thencf == ControlFlow::None && matches(elsecf, ControlFlow::Returns | ControlFlow::Throws))
else if (thencf == ControlFlow::None && elsecf != ControlFlow::None)
scope->inheritRefinements(thenScope);
if (matches(thencf, ControlFlow::Returns | ControlFlow::Throws) && matches(elsecf, ControlFlow::Returns | ControlFlow::Throws))
if (FFlag::LuauLoopControlFlowAnalysis && thencf == elsecf)
return thencf;
else if (matches(thencf, ControlFlow::Returns | ControlFlow::Throws) && matches(elsecf, ControlFlow::Returns | ControlFlow::Throws))
return ControlFlow::Returns;
else
return ControlFlow::None;
@ -1378,7 +1380,8 @@ InferencePack ConstraintGraphBuilder::checkPack(
return InferencePack{arena->addTypePack(TypePack{std::move(head), tail})};
}
InferencePack ConstraintGraphBuilder::checkPack(const ScopePtr& scope, AstExpr* expr, const std::vector<std::optional<TypeId>>& expectedTypes)
InferencePack ConstraintGraphBuilder::checkPack(
const ScopePtr& scope, AstExpr* expr, const std::vector<std::optional<TypeId>>& expectedTypes, bool generalize)
{
RecursionCounter counter{&recursionCount};
@ -1404,7 +1407,7 @@ InferencePack ConstraintGraphBuilder::checkPack(const ScopePtr& scope, AstExpr*
std::optional<TypeId> expectedType;
if (!expectedTypes.empty())
expectedType = expectedTypes[0];
TypeId t = check(scope, expr, ValueContext::RValue, expectedType).ty;
TypeId t = check(scope, expr, ValueContext::RValue, expectedType, /*forceSingletons*/ false, generalize).ty;
result = InferencePack{arena->addTypePack({t})};
}
@ -1452,51 +1455,25 @@ InferencePack ConstraintGraphBuilder::checkPack(const ScopePtr& scope, AstExprCa
discriminantTypes.push_back(std::nullopt);
}
Checkpoint startCheckpoint = checkpoint(this);
TypeId fnType = check(scope, call->func).ty;
Checkpoint fnEndCheckpoint = checkpoint(this);
std::vector<std::optional<TypeId>> expectedTypesForCall = getExpectedCallTypesForFunctionOverloads(fnType);
module->astOriginalCallTypes[call->func] = fnType;
module->astOriginalCallTypes[call] = fnType;
TypePackId expectedArgPack = arena->freshTypePack(scope.get());
TypePackId expectedRetPack = arena->freshTypePack(scope.get());
TypeId expectedFunctionType = arena->addType(FunctionType{expectedArgPack, expectedRetPack, std::nullopt, call->self});
TypeId instantiatedFnType = arena->addType(BlockedType{});
addConstraint(scope, call->location, InstantiationConstraint{instantiatedFnType, fnType});
NotNull<Constraint> extractArgsConstraint = addConstraint(scope, call->location, SubtypeConstraint{instantiatedFnType, expectedFunctionType});
// Fully solve fnType, then extract its argument list as expectedArgPack.
forEachConstraint(startCheckpoint, fnEndCheckpoint, this, [extractArgsConstraint](const ConstraintPtr& constraint) {
extractArgsConstraint->dependencies.emplace_back(constraint.get());
});
const AstExpr* lastArg = exprArgs.size() ? exprArgs[exprArgs.size() - 1] : nullptr;
const bool needTail = lastArg && (lastArg->is<AstExprCall>() || lastArg->is<AstExprVarargs>());
TypePack expectedArgs;
if (!needTail)
expectedArgs = extendTypePack(*arena, builtinTypes, expectedArgPack, exprArgs.size(), expectedTypesForCall);
else
expectedArgs = extendTypePack(*arena, builtinTypes, expectedArgPack, exprArgs.size() - 1, expectedTypesForCall);
Checkpoint argBeginCheckpoint = checkpoint(this);
std::vector<TypeId> args;
std::optional<TypePackId> argTail;
std::vector<RefinementId> argumentRefinements;
Checkpoint argCheckpoint = checkpoint(this);
for (size_t i = 0; i < exprArgs.size(); ++i)
{
AstExpr* arg = exprArgs[i];
std::optional<TypeId> expectedType;
if (i < expectedArgs.head.size())
expectedType = expectedArgs.head[i];
if (i == 0 && call->self)
{
@ -1512,7 +1489,8 @@ InferencePack ConstraintGraphBuilder::checkPack(const ScopePtr& scope, AstExprCa
}
else if (i < exprArgs.size() - 1 || !(arg->is<AstExprCall>() || arg->is<AstExprVarargs>()))
{
auto [ty, refinement] = check(scope, arg, ValueContext::RValue, expectedType);
auto [ty, refinement] =
check(scope, arg, ValueContext::RValue, /*expectedType*/ std::nullopt, /*forceSingleton*/ false, /*generalize*/ false);
args.push_back(ty);
argumentRefinements.push_back(refinement);
}
@ -1526,12 +1504,6 @@ InferencePack ConstraintGraphBuilder::checkPack(const ScopePtr& scope, AstExprCa
Checkpoint argEndCheckpoint = checkpoint(this);
// Do not solve argument constraints until after we have extracted the
// expected types from the callable.
forEachConstraint(argCheckpoint, argEndCheckpoint, this, [extractArgsConstraint](const ConstraintPtr& constraint) {
constraint->dependencies.push_back(extractArgsConstraint);
});
if (matchSetmetatable(*call))
{
TypePack argTailPack;
@ -1607,8 +1579,8 @@ InferencePack ConstraintGraphBuilder::checkPack(const ScopePtr& scope, AstExprCa
// This ensures, for instance, that we start inferring the contents of
// lambdas under the assumption that their arguments and return types
// will be compatible with the enclosing function call.
forEachConstraint(fnEndCheckpoint, argEndCheckpoint, this, [fcc](const ConstraintPtr& constraint) {
fcc->dependencies.emplace_back(constraint.get());
forEachConstraint(argBeginCheckpoint, argEndCheckpoint, this, [fcc](const ConstraintPtr& constraint) {
constraint->dependencies.emplace_back(fcc);
});
return InferencePack{rets, {refinementArena.variadic(returnRefinements)}};
@ -1616,7 +1588,7 @@ InferencePack ConstraintGraphBuilder::checkPack(const ScopePtr& scope, AstExprCa
}
Inference ConstraintGraphBuilder::check(
const ScopePtr& scope, AstExpr* expr, ValueContext context, std::optional<TypeId> expectedType, bool forceSingleton)
const ScopePtr& scope, AstExpr* expr, ValueContext context, std::optional<TypeId> expectedType, bool forceSingleton, bool generalize)
{
RecursionCounter counter{&recursionCount};
@ -1647,7 +1619,7 @@ Inference ConstraintGraphBuilder::check(
else if (auto call = expr->as<AstExprCall>())
result = flattenPack(scope, expr->location, checkPack(scope, call)); // TODO: needs predicates too
else if (auto a = expr->as<AstExprFunction>())
result = check(scope, a, expectedType);
result = check(scope, a, expectedType, generalize);
else if (auto indexName = expr->as<AstExprIndexName>())
result = check(scope, indexName);
else if (auto indexExpr = expr->as<AstExprIndexExpr>())
@ -1815,13 +1787,15 @@ Inference ConstraintGraphBuilder::check(const ScopePtr& scope, AstExprIndexExpr*
return Inference{result};
}
Inference ConstraintGraphBuilder::check(const ScopePtr& scope, AstExprFunction* func, std::optional<TypeId> expectedType)
Inference ConstraintGraphBuilder::check(const ScopePtr& scope, AstExprFunction* func, std::optional<TypeId> expectedType, bool generalize)
{
Checkpoint startCheckpoint = checkpoint(this);
FunctionSignature sig = checkFunctionSignature(scope, func, expectedType);
checkFunctionBody(sig.bodyScope, func);
Checkpoint endCheckpoint = checkpoint(this);
if (generalize)
{
TypeId generalizedTy = arena->addType(BlockedType{});
NotNull<Constraint> gc = addConstraint(sig.signatureScope, func->location, GeneralizationConstraint{generalizedTy, sig.signature});
@ -1840,6 +1814,11 @@ Inference ConstraintGraphBuilder::check(const ScopePtr& scope, AstExprFunction*
return Inference{generalizedTy};
}
else
{
return Inference{sig.signature};
}
}
Inference ConstraintGraphBuilder::check(const ScopePtr& scope, AstExprUnary* unary)
{
@ -2379,10 +2358,11 @@ ConstraintGraphBuilder::FunctionSignature ConstraintGraphBuilder::checkFunctionS
argTy = resolveType(signatureScope, local->annotation, /* inTypeArguments */ false, /* replaceErrorWithFresh*/ true);
else
{
argTy = freshType(signatureScope);
if (i < expectedArgPack.head.size())
addConstraint(signatureScope, local->location, SubtypeConstraint{argTy, expectedArgPack.head[i]});
argTy = expectedArgPack.head[i];
else
argTy = freshType(signatureScope);
}
argTypes.push_back(argTy);

View file

@ -1380,6 +1380,44 @@ bool ConstraintSolver::tryDispatch(const FunctionCallConstraint& c, NotNull<cons
*asMutable(follow(*ty)) = BoundType{builtinTypes->anyType};
}
// We know the type of the function and the arguments it expects to receive.
// We also know the TypeIds of the actual arguments that will be passed.
//
// Bidirectional type checking: Force those TypeIds to be the expected
// arguments. If something is incoherent, we'll spot it in type checking.
//
// Most important detail: If a function argument is a lambda, we also want
// to force unannotated argument types of that lambda to be the expected
// types.
// FIXME: Bidirectional type checking of overloaded functions is not yet supported.
if (auto ftv = get<FunctionType>(fn))
{
const std::vector<TypeId> expectedArgs = flatten(ftv->argTypes).first;
const std::vector<TypeId> argPackHead = flatten(argsPack).first;
for (size_t i = 0; i < c.callSite->args.size && i < expectedArgs.size() && i < argPackHead.size(); ++i)
{
const FunctionType* expectedLambdaTy = get<FunctionType>(follow(expectedArgs[i]));
const FunctionType* lambdaTy = get<FunctionType>(follow(argPackHead[i]));
const AstExprFunction* lambdaExpr = c.callSite->args.data[i]->as<AstExprFunction>();
if (expectedLambdaTy && lambdaTy && lambdaExpr)
{
const std::vector<TypeId> expectedLambdaArgTys = flatten(expectedLambdaTy->argTypes).first;
const std::vector<TypeId> lambdaArgTys = flatten(lambdaTy->argTypes).first;
for (size_t j = 0; j < expectedLambdaArgTys.size() && j < lambdaArgTys.size() && j < lambdaExpr->args.size; ++j)
{
if (!lambdaExpr->args.data[j]->annotation && get<FreeType>(follow(lambdaArgTys[j])))
{
asMutable(lambdaArgTys[j])->ty.emplace<BoundType>(expectedLambdaArgTys[j]);
}
}
}
}
}
TypeId inferredTy = arena->addType(FunctionType{TypeLevel{}, constraint->scope.get(), argsPack, c.result});
Unifier2 u2{NotNull{arena}, builtinTypes, constraint->scope, NotNull{&iceReporter}};

View file

@ -37,6 +37,7 @@ LUAU_FASTFLAGVARIABLE(DebugLuauLogSolverToJson, false)
LUAU_FASTFLAGVARIABLE(DebugLuauReadWriteProperties, false)
LUAU_FASTFLAGVARIABLE(LuauTypecheckLimitControls, false)
LUAU_FASTFLAGVARIABLE(CorrectEarlyReturnInMarkDirty, false)
LUAU_FASTFLAGVARIABLE(DebugLuauNewNonStrictMode, false)
namespace Luau
{
@ -1257,7 +1258,7 @@ ModulePtr check(const SourceModule& sourceModule, const std::vector<RequireCycle
ConstraintGraphBuilder cgb{result, NotNull{&normalizer}, moduleResolver, builtinTypes, iceHandler, parentScope, std::move(prepareModuleScope),
logger.get(), NotNull{&dfg}, requireCycles};
cgb.visit(sourceModule.root);
cgb.visitModuleRoot(sourceModule.root);
result->errors = std::move(cgb.errors);
ConstraintSolver cs{NotNull{&normalizer}, NotNull(cgb.rootScope), borrowConstraints(cgb.constraints), result->humanReadableName, moduleResolver,

View file

@ -0,0 +1,87 @@
// This file is part of the Luau programming language and is licensed under MIT License; see LICENSE.txt for details
#include "Luau/NonStrictTypeChecker.h"
#include "Luau/Type.h"
#include "Luau/Subtyping.h"
#include "Luau/Normalize.h"
#include "Luau/Error.h"
#include "Luau/TypeArena.h"
#include "Luau/Def.h"
namespace Luau
{
struct NonStrictContext
{
std::unordered_map<DefId, TypeId> context;
NonStrictContext() = default;
NonStrictContext(const NonStrictContext&) = delete;
NonStrictContext& operator=(const NonStrictContext&) = delete;
NonStrictContext(NonStrictContext&&) = default;
NonStrictContext& operator=(NonStrictContext&&) = default;
void unionContexts(const NonStrictContext& other)
{
// TODO: unimplemented
}
void intersectContexts(const NonStrictContext& other)
{
// TODO: unimplemented
}
void removeFromContext(const std::vector<DefId>& defs)
{
// TODO: unimplemented
}
std::optional<TypeId> find(const DefId& def)
{
// TODO: unimplemented
return {};
}
// Satisfies means that for a given DefId n, and an actual type t for `n`, t satisfies the context if t <: context[n]
// ice if the DefId is not in the context
bool satisfies(const DefId& def, TypeId inferredType)
{
// TODO: unimplemented
return false;
}
bool willRunTimeError(const DefId& def, TypeId inferredType)
{
return satisfies(def, inferredType);
}
};
struct NonStrictTypeChecker
{
NotNull<BuiltinTypes> builtinTypes;
const NotNull<InternalErrorReporter> ice;
TypeArena arena;
Module* module;
Normalizer normalizer;
Subtyping subtyping;
NonStrictTypeChecker(NotNull<BuiltinTypes> builtinTypes, Subtyping subtyping, const NotNull<InternalErrorReporter> ice,
NotNull<UnifierSharedState> unifierState, Module* module)
: builtinTypes(builtinTypes)
, ice(ice)
, module(module)
, normalizer{&arena, builtinTypes, unifierState, /* cache inhabitance */ true}
, subtyping{builtinTypes, NotNull{&arena}, NotNull(&normalizer), ice, NotNull{module->getModuleScope().get()}}
{
}
};
void checkNonStrict(NotNull<BuiltinTypes> builtinTypes, Module* module)
{
// TODO: unimplemented
}
} // namespace Luau

View file

@ -11,7 +11,6 @@
#include "Luau/Type.h"
#include "Luau/Unifier.h"
LUAU_FASTFLAGVARIABLE(DebugLuauCopyBeforeNormalizing, false)
LUAU_FASTFLAGVARIABLE(DebugLuauCheckNormalizeInvariant, false)
// This could theoretically be 2000 on amd64, but x86 requires this.

View file

@ -696,16 +696,33 @@ struct TypeStringifier
std::string openbrace = "@@@";
std::string closedbrace = "@@@?!";
switch (state.opts.hideTableKind ? TableState::Unsealed : ttv.state)
switch (state.opts.hideTableKind ? (FFlag::DebugLuauDeferredConstraintResolution ? TableState::Sealed : TableState::Unsealed) : ttv.state)
{
case TableState::Sealed:
if (FFlag::DebugLuauDeferredConstraintResolution)
{
openbrace = "{";
closedbrace = "}";
}
else
{
state.result.invalid = true;
openbrace = "{|";
closedbrace = "|}";
}
break;
case TableState::Unsealed:
if (FFlag::DebugLuauDeferredConstraintResolution)
{
state.result.invalid = true;
openbrace = "{|";
closedbrace = "|}";
}
else
{
openbrace = "{";
closedbrace = "}";
}
break;
case TableState::Free:
state.result.invalid = true;

View file

@ -38,6 +38,7 @@ LUAU_FASTFLAG(LuauInstantiateInSubtyping)
LUAU_FASTFLAGVARIABLE(LuauAllowIndexClassParameters, false)
LUAU_FASTFLAG(LuauOccursIsntAlwaysFailure)
LUAU_FASTFLAGVARIABLE(LuauTinyControlFlowAnalysis, false)
LUAU_FASTFLAGVARIABLE(LuauLoopControlFlowAnalysis, false)
LUAU_FASTFLAGVARIABLE(LuauVariadicOverloadFix, false)
LUAU_FASTFLAGVARIABLE(LuauAlwaysCommitInferencesOfFunctionCalls, false)
LUAU_FASTFLAG(LuauParseDeclareClassIndexer)
@ -350,11 +351,10 @@ ControlFlow TypeChecker::check(const ScopePtr& scope, const AstStat& program)
return check(scope, *while_);
else if (auto repeat = program.as<AstStatRepeat>())
return check(scope, *repeat);
else if (program.is<AstStatBreak>() || program.is<AstStatContinue>())
{
// Nothing to do
return ControlFlow::None;
}
else if (program.is<AstStatBreak>())
return FFlag::LuauLoopControlFlowAnalysis ? ControlFlow::Breaks : ControlFlow::None;
else if (program.is<AstStatContinue>())
return FFlag::LuauLoopControlFlowAnalysis ? ControlFlow::Continues : ControlFlow::None;
else if (auto return_ = program.as<AstStatReturn>())
return check(scope, *return_);
else if (auto expr = program.as<AstStatExpr>())
@ -752,12 +752,14 @@ ControlFlow TypeChecker::check(const ScopePtr& scope, const AstStatIf& statement
if (statement.elsebody)
elsecf = check(elseScope, *statement.elsebody);
if (matches(thencf, ControlFlow::Returns | ControlFlow::Throws) && elsecf == ControlFlow::None)
if (thencf != ControlFlow::None && elsecf == ControlFlow::None)
scope->inheritRefinements(elseScope);
else if (thencf == ControlFlow::None && matches(elsecf, ControlFlow::Returns | ControlFlow::Throws))
else if (thencf == ControlFlow::None && elsecf != ControlFlow::None)
scope->inheritRefinements(thenScope);
if (matches(thencf, ControlFlow::Returns | ControlFlow::Throws) && matches(elsecf, ControlFlow::Returns | ControlFlow::Throws))
if (FFlag::LuauLoopControlFlowAnalysis && thencf == elsecf)
return thencf;
else if (matches(thencf, ControlFlow::Returns | ControlFlow::Throws) && matches(elsecf, ControlFlow::Returns | ControlFlow::Throws))
return ControlFlow::Returns;
else
return ControlFlow::None;

View file

@ -320,14 +320,26 @@ bool Unifier2::unify(TypePackId subTp, TypePackId superTp)
for (size_t i = 0; i < maxLength; ++i)
unify(subTypes[i], superTypes[i]);
if (!subTail || !superTail)
return true;
if (subTail && superTail)
{
TypePackId followedSubTail = follow(*subTail);
TypePackId followedSuperTail = follow(*superTail);
if (get<FreeTypePack>(followedSubTail) || get<FreeTypePack>(followedSuperTail))
return unify(followedSubTail, followedSuperTail);
}
else if (subTail)
{
TypePackId followedSubTail = follow(*subTail);
if (get<FreeTypePack>(followedSubTail))
asMutable(followedSubTail)->ty.emplace<BoundTypePack>(builtinTypes->emptyTypePack);
}
else if (superTail)
{
TypePackId followedSuperTail = follow(*superTail);
if (get<FreeTypePack>(followedSuperTail))
asMutable(followedSuperTail)->ty.emplace<BoundTypePack>(builtinTypes->emptyTypePack);
}
return true;
}
@ -582,13 +594,6 @@ struct MutatingGeneralizer : TypeOnceVisitor
TableType* tt = getMutable<TableType>(ty);
LUAU_ASSERT(tt);
// We only unseal tables if they occur within function argument or
// return lists. In principle, we could always seal tables when
// generalizing, but that would mean that we'd lose the ability to
// report the existence of unsealed tables via things like hovertype.
if (tt->state == TableState::Unsealed && !isWithinFunction)
return true;
tt->state = TableState::Sealed;
return true;
@ -611,10 +616,7 @@ std::optional<TypeId> Unifier2::generalize(TypeId ty)
{
ty = follow(ty);
if (ty->owningArena != arena)
return ty;
if (ty->persistent)
if (ty->owningArena != arena || ty->persistent)
return ty;
if (const FunctionType* ft = get<FunctionType>(ty); ft && (!ft->generics.empty() || !ft->genericPacks.empty()))
@ -627,16 +629,23 @@ std::optional<TypeId> Unifier2::generalize(TypeId ty)
gen.traverse(ty);
std::optional<TypeId> res = ty;
/* MutatingGeneralizer mutates types in place, so it is possible that ty has
* been transmuted to a BoundType. We must follow it again and verify that
* we are allowed to mutate it before we attach generics to it.
*/
ty = follow(ty);
FunctionType* ftv = getMutable<FunctionType>(follow(*res));
if (ty->owningArena != arena || ty->persistent)
return ty;
FunctionType* ftv = getMutable<FunctionType>(ty);
if (ftv)
{
ftv->generics = std::move(gen.generics);
ftv->genericPacks = std::move(gen.genericPacks);
}
return res;
return ty;
}
TypeId Unifier2::mkUnion(TypeId left, TypeId right)

View file

@ -477,17 +477,9 @@ const Instruction* executeSETTABLEKS(lua_State* L, const Instruction* pc, StkId
{
Table* h = hvalue(rb);
int slot = LUAU_INSN_C(insn) & h->nodemask8;
LuaNode* n = &h->node[slot];
// we ignore the fast path that checks for the cached slot since IrTranslation already checks for it.
// fast-path: value is in expected slot
if (LUAU_LIKELY(ttisstring(gkey(n)) && tsvalue(gkey(n)) == tsvalue(kv) && !ttisnil(gval(n)) && !h->readonly))
{
setobj2t(L, gval(n), ra);
luaC_barriert(L, h, ra);
return pc;
}
else if (fastnotm(h->metatable, TM_NEWINDEX) && !h->readonly)
if (fastnotm(h->metatable, TM_NEWINDEX) && !h->readonly)
{
VM_PROTECT_PC(); // set may fail
@ -502,6 +494,7 @@ const Instruction* executeSETTABLEKS(lua_State* L, const Instruction* pc, StkId
else
{
// slow-path, may invoke Lua calls via __newindex metamethod
int slot = LUAU_INSN_C(insn) & h->nodemask8;
L->cachedslot = slot;
VM_PROTECT(luaV_settable(L, rb, kv, ra));
// save cachedslot to accelerate future lookups; patches currently executing instruction since pc-2 rolls back two pc++

View file

@ -18,6 +18,7 @@ LUAU_FASTFLAGVARIABLE(DebugLuauAbortingChecks, false)
LUAU_FASTFLAGVARIABLE(LuauReuseHashSlots2, false)
LUAU_FASTFLAGVARIABLE(LuauKeepVmapLinear, false)
LUAU_FASTFLAGVARIABLE(LuauMergeTagLoads, false)
LUAU_FASTFLAGVARIABLE(LuauReuseArrSlots, false)
namespace Luau
{
@ -174,14 +175,32 @@ struct ConstPropState
}
}
// Value propagation extends the live range of an SSA register
// In some cases we can't propagate earlier values because we can't guarantee that we will be able to find a storage/restore location
// As an example, when Luau call is performed, both volatile registers and stack slots might be overwritten
void invalidateValuePropagation()
{
valueMap.clear();
tryNumToIndexCache.clear();
}
// If table memory has changed, we can't reuse previously computed and validated table slot lookups
// Same goes for table array elements as well
void invalidateHeapTableData()
{
getSlotNodeCache.clear();
checkSlotMatchCache.clear();
getArrAddrCache.clear();
checkArraySizeCache.clear();
}
void invalidateHeap()
{
for (int i = 0; i <= maxReg; ++i)
invalidateHeap(regs[i]);
// If table memory has changed, we can't reuse previously computed and validated table slot lookups
getSlotNodeCache.clear();
checkSlotMatchCache.clear();
invalidateHeapTableData();
}
void invalidateHeap(RegisterInfo& reg)
@ -203,9 +222,7 @@ struct ConstPropState
for (int i = 0; i <= maxReg; ++i)
invalidateTableArraySize(regs[i]);
// If table memory has changed, we can't reuse previously computed and validated table slot lookups
getSlotNodeCache.clear();
checkSlotMatchCache.clear();
invalidateHeapTableData();
}
void invalidateTableArraySize(RegisterInfo& reg)
@ -389,9 +406,9 @@ struct ConstPropState
checkedGc = false;
instLink.clear();
valueMap.clear();
getSlotNodeCache.clear();
checkSlotMatchCache.clear();
invalidateValuePropagation();
invalidateHeapTableData();
}
IrFunction& function;
@ -410,8 +427,15 @@ struct ConstPropState
DenseHashMap<IrInst, uint32_t, IrInstHash, IrInstEq> valueMap;
std::vector<uint32_t> getSlotNodeCache;
std::vector<uint32_t> checkSlotMatchCache;
// Some instruction re-uses can't be stored in valueMap because of extra requirements
std::vector<uint32_t> tryNumToIndexCache; // Fallback block argument might be different
// Heap changes might affect table state
std::vector<uint32_t> getSlotNodeCache; // Additionally, pcpos argument might be different
std::vector<uint32_t> checkSlotMatchCache; // Additionally, fallback block argument might be different
std::vector<uint32_t> getArrAddrCache;
std::vector<uint32_t> checkArraySizeCache; // Additionally, fallback block argument might be different
};
static void handleBuiltinEffects(ConstPropState& state, LuauBuiltinFunction bfid, uint32_t firstReturnReg, int nresults)
@ -873,7 +897,24 @@ static void constPropInInst(ConstPropState& state, IrBuilder& build, IrFunction&
// These instructions don't have an effect on register/memory state we are tracking
case IrCmd::NOP:
case IrCmd::LOAD_ENV:
break;
case IrCmd::GET_ARR_ADDR:
if (!FFlag::LuauReuseArrSlots)
break;
for (uint32_t prevIdx : state.getArrAddrCache)
{
const IrInst& prev = function.instructions[prevIdx];
if (prev.a == inst.a && prev.b == inst.b)
{
substitute(function, inst, IrOp{IrOpKind::Inst, prevIdx});
return; // Break out from both the loop and the switch
}
}
if (int(state.getArrAddrCache.size()) < FInt::LuauCodeGenReuseSlotLimit)
state.getArrAddrCache.push_back(index);
break;
case IrCmd::GET_SLOT_NODE_ADDR:
if (!FFlag::LuauReuseHashSlots2)
@ -929,7 +970,25 @@ static void constPropInInst(ConstPropState& state, IrBuilder& build, IrFunction&
case IrCmd::STRING_LEN:
case IrCmd::NEW_TABLE:
case IrCmd::DUP_TABLE:
break;
case IrCmd::TRY_NUM_TO_INDEX:
if (!FFlag::LuauReuseArrSlots)
break;
for (uint32_t prevIdx : state.tryNumToIndexCache)
{
const IrInst& prev = function.instructions[prevIdx];
if (prev.a == inst.a)
{
substitute(function, inst, IrOp{IrOpKind::Inst, prevIdx});
return; // Break out from both the loop and the switch
}
}
if (int(state.tryNumToIndexCache.size()) < FInt::LuauCodeGenReuseSlotLimit)
state.tryNumToIndexCache.push_back(index);
break;
case IrCmd::TRY_CALL_FASTGETTM:
break;
case IrCmd::INT_TO_NUM:
@ -967,8 +1026,42 @@ static void constPropInInst(ConstPropState& state, IrBuilder& build, IrFunction&
{
replace(function, block, index, {IrCmd::JUMP, inst.c});
}
return; // Break out from both the loop and the switch
}
}
if (!FFlag::LuauReuseArrSlots)
break;
for (uint32_t prevIdx : state.checkArraySizeCache)
{
const IrInst& prev = function.instructions[prevIdx];
if (prev.a != inst.a)
continue;
bool sameBoundary = prev.b == inst.b;
// If arguments are different, in case they are both constant, we can check if a larger bound was already tested
if (!sameBoundary && inst.b.kind == IrOpKind::Constant && prev.b.kind == IrOpKind::Constant &&
function.intOp(inst.b) < function.intOp(prev.b))
sameBoundary = true;
if (sameBoundary)
{
if (FFlag::DebugLuauAbortingChecks)
replace(function, inst.c, build.undef());
else
kill(function, inst);
return; // Break out from both the loop and the switch
}
// TODO: it should be possible to update previous check with a higher bound if current and previous checks are against a constant
}
if (int(state.checkArraySizeCache.size()) < FInt::LuauCodeGenReuseSlotLimit)
state.checkArraySizeCache.push_back(index);
break;
}
case IrCmd::CHECK_SLOT_MATCH:
@ -1053,9 +1146,8 @@ static void constPropInInst(ConstPropState& state, IrBuilder& build, IrFunction&
replace(function, inst.f, build.constUint(info->knownTableArraySize));
// TODO: this can be relaxed when x64 emitInstSetList becomes aware of register allocator
state.valueMap.clear();
state.getSlotNodeCache.clear();
state.checkSlotMatchCache.clear();
state.invalidateValuePropagation();
state.invalidateHeapTableData();
break;
case IrCmd::CALL:
state.invalidateRegistersFrom(vmRegOp(inst.a));
@ -1064,15 +1156,14 @@ static void constPropInInst(ConstPropState& state, IrBuilder& build, IrFunction&
// We cannot guarantee right now that all live values can be rematerialized from non-stack memory locations
// To prevent earlier values from being propagated to after the call, we have to clear the map
// TODO: remove only the values that don't have a guaranteed restore location
state.valueMap.clear();
state.invalidateValuePropagation();
break;
case IrCmd::FORGLOOP:
state.invalidateRegistersFrom(vmRegOp(inst.a) + 2); // Rn and Rn+1 are not modified
// TODO: this can be relaxed when x64 emitInstForGLoop becomes aware of register allocator
state.valueMap.clear();
state.getSlotNodeCache.clear();
state.checkSlotMatchCache.clear();
state.invalidateValuePropagation();
state.invalidateHeapTableData();
break;
case IrCmd::FORGLOOP_FALLBACK:
state.invalidateRegistersFrom(vmRegOp(inst.a) + 2); // Rn and Rn+1 are not modified
@ -1139,11 +1230,10 @@ static void constPropInBlock(IrBuilder& build, IrBlock& block, ConstPropState& s
if (!FFlag::LuauKeepVmapLinear)
{
// Value numbering and load/store propagation is not performed between blocks
state.valueMap.clear();
state.invalidateValuePropagation();
// Same for table slot data propagation
state.getSlotNodeCache.clear();
state.checkSlotMatchCache.clear();
state.invalidateHeapTableData();
}
}
@ -1168,11 +1258,10 @@ static void constPropInBlockChain(IrBuilder& build, std::vector<uint8_t>& visite
{
// Value numbering and load/store propagation is not performed between blocks right now
// This is because cross-block value uses limit creation of linear block (restriction in collectDirectBlockJumpPath)
state.valueMap.clear();
state.invalidateValuePropagation();
// Same for table slot data propagation
state.getSlotNodeCache.clear();
state.checkSlotMatchCache.clear();
state.invalidateHeapTableData();
}
// Blocks in a chain are guaranteed to follow each other

View file

@ -178,6 +178,7 @@ target_sources(Luau.Analysis PRIVATE
Analysis/include/Luau/Metamethods.h
Analysis/include/Luau/Module.h
Analysis/include/Luau/ModuleResolver.h
Analysis/include/Luau/NonStrictTypeChecker.h
Analysis/include/Luau/Normalize.h
Analysis/include/Luau/Predicate.h
Analysis/include/Luau/Quantify.h
@ -235,6 +236,7 @@ target_sources(Luau.Analysis PRIVATE
Analysis/src/Linter.cpp
Analysis/src/LValue.cpp
Analysis/src/Module.cpp
Analysis/src/NonStrictTypeChecker.cpp
Analysis/src/Normalize.cpp
Analysis/src/Quantify.cpp
Analysis/src/Refinement.cpp
@ -398,6 +400,7 @@ if(TARGET Luau.UnitTest)
tests/LValue.test.cpp
tests/Module.test.cpp
tests/NonstrictMode.test.cpp
tests/NonStrictTypeChecker.test.cpp
tests/Normalize.test.cpp
tests/NotNull.test.cpp
tests/Parser.test.cpp

View file

@ -22,27 +22,6 @@ static time_t timegm(struct tm* timep)
{
return _mkgmtime(timep);
}
#elif defined(__FreeBSD__)
static tm* gmtime_r(const time_t* timep, tm* result)
{
// Note: return is reversed from Windows (0 is success on Windows, but 0/null is failure elsewhere)
// Windows https://learn.microsoft.com/en-us/cpp/c-runtime-library/reference/gmtime-s-gmtime32-s-gmtime64-s?view=msvc-170#return-value
// Everyone else https://en.cppreference.com/w/c/chrono/gmtime
return gmtime_s(timep, result);
}
static tm* localtime_r(const time_t* timep, tm* result)
{
// Note: return is reversed from Windows (0 is success on Windows, but 0/null is failure elsewhere)
// Windows https://learn.microsoft.com/en-us/cpp/c-runtime-library/reference/localtime-s-localtime32-s-localtime64-s?view=msvc-170#return-value
// Everyone else https://en.cppreference.com/w/c/chrono/localtime
return localtime_s(timep, result);
}
static time_t timegm(struct tm* timep)
{
return mktime(timep);
}
#endif
static int os_clock(lua_State* L)

6
extern/doctest.h vendored
View file

@ -3139,7 +3139,11 @@ DOCTEST_MAKE_STD_HEADERS_CLEAN_FROM_WARNINGS_ON_WALL_BEGIN
#include <unordered_set>
#include <exception>
#include <stdexcept>
#if !defined(DOCTEST_CONFIG_NO_POSIX_SIGNALS)
#include <csignal>
#endif
#include <cfloat>
#include <cctype>
#include <cstdint>
@ -5667,6 +5671,8 @@ namespace {
std::tm timeInfo;
#ifdef DOCTEST_PLATFORM_WINDOWS
gmtime_s(&timeInfo, &rawtime);
#elif defined(DOCTEST_CONFIG_USE_GMTIME_S)
gmtime_s(&rawtime, &timeInfo);
#else // DOCTEST_PLATFORM_WINDOWS
gmtime_r(&rawtime, &timeInfo);
#endif // DOCTEST_PLATFORM_WINDOWS

View file

@ -2162,6 +2162,9 @@ local fp: @1= f
auto ac = autocomplete('1');
if (FFlag::DebugLuauDeferredConstraintResolution)
REQUIRE_EQ("({ x: number, y: number }) -> number", toString(requireType("f")));
else
REQUIRE_EQ("({| x: number, y: number |}) -> number", toString(requireType("f")));
CHECK(ac.entryMap.count("({ x: number, y: number }) -> number"));
}

View file

@ -274,6 +274,9 @@ constexpr X64::RegisterX64 rNonVol4 = X64::r14;
TEST_CASE("GeneratedCodeExecutionX64")
{
if (!Luau::CodeGen::isSupported())
return;
using namespace X64;
AssemblyBuilderX64 build(/* logText= */ false);
@ -315,6 +318,9 @@ static void nonthrowing(int64_t arg)
TEST_CASE("GeneratedCodeExecutionWithThrowX64")
{
if (!Luau::CodeGen::isSupported())
return;
using namespace X64;
AssemblyBuilderX64 build(/* logText= */ false);
@ -513,6 +519,9 @@ TEST_CASE("GeneratedCodeExecutionWithThrowX64Simd")
TEST_CASE("GeneratedCodeExecutionMultipleFunctionsWithThrowX64")
{
if (!Luau::CodeGen::isSupported())
return;
using namespace X64;
AssemblyBuilderX64 build(/* logText= */ false);
@ -650,6 +659,9 @@ TEST_CASE("GeneratedCodeExecutionMultipleFunctionsWithThrowX64")
TEST_CASE("GeneratedCodeExecutionWithThrowOutsideTheGateX64")
{
if (!Luau::CodeGen::isSupported())
return;
using namespace X64;
AssemblyBuilderX64 build(/* logText= */ false);

View file

@ -21,7 +21,7 @@ void ConstraintGraphBuilderFixture::generateConstraints(const std::string& code)
dfg = std::make_unique<DataFlowGraph>(DataFlowGraphBuilder::build(root, NotNull{&ice}));
cgb = std::make_unique<ConstraintGraphBuilder>(mainModule, NotNull{&normalizer}, NotNull(&moduleResolver), builtinTypes, NotNull(&ice),
frontend.globals.globalScope, /*prepareModuleScope*/ nullptr, &logger, NotNull{dfg.get()}, std::vector<RequireCycle>());
cgb->visit(root);
cgb->visitModuleRoot(root);
rootScope = cgb->rootScope;
constraints = Luau::borrowConstraints(cgb->constraints);
}

View file

@ -657,6 +657,10 @@ TEST_CASE_FIXTURE(DifferFixture, "function_table_self_referential_cyclic")
)");
LUAU_REQUIRE_NO_ERRORS(result);
if (FFlag::DebugLuauDeferredConstraintResolution)
compareTypesNe("foo", "almostFoo",
R"(DiffError: these two types are not equal because the left type at <unlabeled-symbol>.Ret[1].bar.Ret[1] has type t1 where t1 = { bar: () -> t1 }, while the right type at <unlabeled-symbol>.Ret[1].bar.Ret[1] has type t1 where t1 = () -> t1)");
else
compareTypesNe("foo", "almostFoo",
R"(DiffError: these two types are not equal because the left type at <unlabeled-symbol>.Ret[1].bar.Ret[1] has type t1 where t1 = {| bar: () -> t1 |}, while the right type at <unlabeled-symbol>.Ret[1].bar.Ret[1] has type t1 where t1 = () -> t1)");
}
@ -812,6 +816,10 @@ TEST_CASE_FIXTURE(DifferFixture, "union_missing")
)");
LUAU_REQUIRE_NO_ERRORS(result);
if (FFlag::DebugLuauDeferredConstraintResolution)
compareTypesNe("foo", "almostFoo",
R"(DiffError: these two types are not equal because the left type at <unlabeled-symbol> is a union containing type { baz: boolean, rot: "singleton" }, while the right type at <unlabeled-symbol> is a union missing type { baz: boolean, rot: "singleton" })");
else
compareTypesNe("foo", "almostFoo",
R"(DiffError: these two types are not equal because the left type at <unlabeled-symbol> is a union containing type {| baz: boolean, rot: "singleton" |}, while the right type at <unlabeled-symbol> is a union missing type {| baz: boolean, rot: "singleton" |})");
}
@ -848,6 +856,10 @@ TEST_CASE_FIXTURE(DifferFixture, "intersection_tables_missing_right")
)");
LUAU_REQUIRE_NO_ERRORS(result);
if (FFlag::DebugLuauDeferredConstraintResolution)
compareTypesNe("foo", "almostFoo",
R"(DiffError: these two types are not equal because the left type at <unlabeled-symbol> is an intersection containing type { x: number }, while the right type at <unlabeled-symbol> is an intersection missing type { x: number })");
else
compareTypesNe("foo", "almostFoo",
R"(DiffError: these two types are not equal because the left type at <unlabeled-symbol> is an intersection containing type {| x: number |}, while the right type at <unlabeled-symbol> is an intersection missing type {| x: number |})");
}
@ -860,6 +872,10 @@ TEST_CASE_FIXTURE(DifferFixture, "intersection_tables_missing_left")
)");
LUAU_REQUIRE_NO_ERRORS(result);
if (FFlag::DebugLuauDeferredConstraintResolution)
compareTypesNe("foo", "almostFoo",
R"(DiffError: these two types are not equal because the left type at <unlabeled-symbol> is an intersection missing type { z: boolean }, while the right type at <unlabeled-symbol> is an intersection containing type { z: boolean })");
else
compareTypesNe("foo", "almostFoo",
R"(DiffError: these two types are not equal because the left type at <unlabeled-symbol> is an intersection missing type {| z: boolean |}, while the right type at <unlabeled-symbol> is an intersection containing type {| z: boolean |})");
}

View file

@ -12,6 +12,8 @@
using namespace Luau;
LUAU_FASTFLAG(DebugLuauDeferredConstraintResolution)
namespace
{
@ -160,6 +162,9 @@ TEST_CASE_FIXTURE(FrontendFixture, "automatically_check_dependent_scripts")
auto bExports = first(bModule->returnType);
REQUIRE(!!bExports);
if (FFlag::DebugLuauDeferredConstraintResolution)
CHECK_EQ("{ b_value: number }", toString(*bExports));
else
CHECK_EQ("{| b_value: number |}", toString(*bExports));
}
@ -302,6 +307,10 @@ TEST_CASE_FIXTURE(FrontendFixture, "nocheck_cycle_used_by_checked")
std::optional<TypeId> cExports = first(cModule->returnType);
REQUIRE(bool(cExports));
if (FFlag::DebugLuauDeferredConstraintResolution)
CHECK_EQ("{ a: any, b: any }", toString(*cExports));
else
CHECK_EQ("{| a: any, b: any |}", toString(*cExports));
}
@ -473,9 +482,15 @@ return {mod_b = 2}
LUAU_REQUIRE_ERRORS(resultB);
TypeId tyB = requireExportedType("game/B", "btype");
if (FFlag::DebugLuauDeferredConstraintResolution)
CHECK_EQ(toString(tyB, opts), "{ x: number }");
else
CHECK_EQ(toString(tyB, opts), "{| x: number |}");
TypeId tyA = requireExportedType("game/A", "atype");
if (FFlag::DebugLuauDeferredConstraintResolution)
CHECK_EQ(toString(tyA, opts), "{ x: any }");
else
CHECK_EQ(toString(tyA, opts), "{| x: any |}");
frontend.markDirty("game/B");
@ -483,9 +498,15 @@ return {mod_b = 2}
LUAU_REQUIRE_ERRORS(resultB);
tyB = requireExportedType("game/B", "btype");
if (FFlag::DebugLuauDeferredConstraintResolution)
CHECK_EQ(toString(tyB, opts), "{ x: number }");
else
CHECK_EQ(toString(tyB, opts), "{| x: number |}");
tyA = requireExportedType("game/A", "atype");
if (FFlag::DebugLuauDeferredConstraintResolution)
CHECK_EQ(toString(tyA, opts), "{ x: any }");
else
CHECK_EQ(toString(tyA, opts), "{| x: any |}");
}
@ -559,6 +580,9 @@ TEST_CASE_FIXTURE(FrontendFixture, "recheck_if_dependent_script_is_dirty")
auto bExports = first(bModule->returnType);
REQUIRE(!!bExports);
if (FFlag::DebugLuauDeferredConstraintResolution)
CHECK_EQ("{ b_value: string }", toString(*bExports));
else
CHECK_EQ("{| b_value: string |}", toString(*bExports));
}
@ -883,6 +907,10 @@ TEST_CASE_FIXTURE(FrontendFixture, "it_should_be_safe_to_stringify_errors_when_f
// When this test fails, it is because the TypeIds needed by the error have been deallocated.
// It is thus basically impossible to predict what will happen when this assert is evaluated.
// It could segfault, or you could see weird type names like the empty string or <VALUELESS BY EXCEPTION>
if (FFlag::DebugLuauDeferredConstraintResolution)
REQUIRE_EQ(
"Table type 'a' not compatible with type '{ Count: number }' because the former is missing field 'Count'", toString(result.errors[0]));
else
REQUIRE_EQ(
"Table type 'a' not compatible with type '{| Count: number |}' because the former is missing field 'Count'", toString(result.errors[0]));
}

View file

@ -2060,6 +2060,244 @@ bb_fallback_1:
)");
}
TEST_CASE_FIXTURE(IrBuilderFixture, "DuplicateArrayElemChecksSameIndex")
{
ScopedFastFlag luauReuseHashSlots{"LuauReuseArrSlots", true};
IrOp block = build.block(IrBlockKind::Internal);
IrOp fallback = build.block(IrBlockKind::Fallback);
build.beginBlock(block);
// This roughly corresponds to 'return t[1] + t[1]'
IrOp table1 = build.inst(IrCmd::LOAD_POINTER, build.vmReg(1));
build.inst(IrCmd::CHECK_ARRAY_SIZE, table1, build.constInt(0), fallback);
IrOp elem1 = build.inst(IrCmd::GET_ARR_ADDR, table1, build.constInt(0));
IrOp value1 = build.inst(IrCmd::LOAD_TVALUE, elem1, build.constInt(0));
build.inst(IrCmd::STORE_TVALUE, build.vmReg(3), value1);
build.inst(IrCmd::CHECK_ARRAY_SIZE, table1, build.constInt(0), fallback); // This will be removed
IrOp elem2 = build.inst(IrCmd::GET_ARR_ADDR, table1, build.constInt(0)); // And this will be substituted
IrOp value1b = build.inst(IrCmd::LOAD_TVALUE, elem2, build.constInt(0));
build.inst(IrCmd::STORE_TVALUE, build.vmReg(4), value1b);
IrOp a = build.inst(IrCmd::LOAD_DOUBLE, build.vmReg(3));
IrOp b = build.inst(IrCmd::LOAD_DOUBLE, build.vmReg(4));
IrOp sum = build.inst(IrCmd::ADD_NUM, a, b);
build.inst(IrCmd::STORE_DOUBLE, build.vmReg(2), sum);
build.inst(IrCmd::RETURN, build.vmReg(2), build.constUint(1));
build.beginBlock(fallback);
build.inst(IrCmd::RETURN, build.vmReg(0), build.constUint(1));
updateUseCounts(build.function);
constPropInBlockChains(build, true);
// In the future, we might even see duplicate identical TValue loads go away
// In the future, we might even see loads of different VM regs with the same value go away
CHECK("\n" + toString(build.function, /* includeUseInfo */ false) == R"(
bb_0:
%0 = LOAD_POINTER R1
CHECK_ARRAY_SIZE %0, 0i, bb_fallback_1
%2 = GET_ARR_ADDR %0, 0i
%3 = LOAD_TVALUE %2, 0i
STORE_TVALUE R3, %3
%7 = LOAD_TVALUE %2, 0i
STORE_TVALUE R4, %7
%9 = LOAD_DOUBLE R3
%10 = LOAD_DOUBLE R4
%11 = ADD_NUM %9, %10
STORE_DOUBLE R2, %11
RETURN R2, 1u
bb_fallback_1:
RETURN R0, 1u
)");
}
TEST_CASE_FIXTURE(IrBuilderFixture, "DuplicateArrayElemChecksLowerIndex")
{
ScopedFastFlag luauReuseHashSlots{"LuauReuseArrSlots", true};
IrOp block = build.block(IrBlockKind::Internal);
IrOp fallback = build.block(IrBlockKind::Fallback);
build.beginBlock(block);
// This roughly corresponds to 'return t[i] + t[i]'
IrOp table1 = build.inst(IrCmd::LOAD_POINTER, build.vmReg(1));
IrOp index = build.inst(IrCmd::LOAD_DOUBLE, build.vmReg(2));
IrOp validIndex = build.inst(IrCmd::TRY_NUM_TO_INDEX, index, fallback);
IrOp validOffset = build.inst(IrCmd::SUB_INT, validIndex, build.constInt(1));
build.inst(IrCmd::CHECK_ARRAY_SIZE, table1, validOffset, fallback);
IrOp elem1 = build.inst(IrCmd::GET_ARR_ADDR, table1, build.constInt(0));
IrOp value1 = build.inst(IrCmd::LOAD_TVALUE, elem1, build.constInt(0));
build.inst(IrCmd::STORE_TVALUE, build.vmReg(3), value1);
IrOp validIndex2 = build.inst(IrCmd::TRY_NUM_TO_INDEX, index, fallback);
IrOp validOffset2 = build.inst(IrCmd::SUB_INT, validIndex2, build.constInt(1));
build.inst(IrCmd::CHECK_ARRAY_SIZE, table1, validOffset2, fallback); // This will be removed
IrOp elem2 = build.inst(IrCmd::GET_ARR_ADDR, table1, build.constInt(0)); // And this will be substituted
IrOp value1b = build.inst(IrCmd::LOAD_TVALUE, elem2, build.constInt(0));
build.inst(IrCmd::STORE_TVALUE, build.vmReg(4), value1b);
IrOp a = build.inst(IrCmd::LOAD_DOUBLE, build.vmReg(3));
IrOp b = build.inst(IrCmd::LOAD_DOUBLE, build.vmReg(4));
IrOp sum = build.inst(IrCmd::ADD_NUM, a, b);
build.inst(IrCmd::STORE_DOUBLE, build.vmReg(2), sum);
build.inst(IrCmd::RETURN, build.vmReg(2), build.constUint(1));
build.beginBlock(fallback);
build.inst(IrCmd::RETURN, build.vmReg(0), build.constUint(1));
updateUseCounts(build.function);
constPropInBlockChains(build, true);
// In the future, we might even see duplicate identical TValue loads go away
// In the future, we might even see loads of different VM regs with the same value go away
CHECK("\n" + toString(build.function, /* includeUseInfo */ false) == R"(
bb_0:
%0 = LOAD_POINTER R1
%1 = LOAD_DOUBLE R2
%2 = TRY_NUM_TO_INDEX %1, bb_fallback_1
%3 = SUB_INT %2, 1i
CHECK_ARRAY_SIZE %0, %3, bb_fallback_1
%5 = GET_ARR_ADDR %0, 0i
%6 = LOAD_TVALUE %5, 0i
STORE_TVALUE R3, %6
%12 = LOAD_TVALUE %5, 0i
STORE_TVALUE R4, %12
%14 = LOAD_DOUBLE R3
%15 = LOAD_DOUBLE R4
%16 = ADD_NUM %14, %15
STORE_DOUBLE R2, %16
RETURN R2, 1u
bb_fallback_1:
RETURN R0, 1u
)");
}
TEST_CASE_FIXTURE(IrBuilderFixture, "DuplicateArrayElemChecksSameValue")
{
ScopedFastFlag luauReuseHashSlots{"LuauReuseArrSlots", true};
IrOp block = build.block(IrBlockKind::Internal);
IrOp fallback = build.block(IrBlockKind::Fallback);
build.beginBlock(block);
// This roughly corresponds to 'return t[2] + t[1]'
IrOp table1 = build.inst(IrCmd::LOAD_POINTER, build.vmReg(1));
build.inst(IrCmd::CHECK_ARRAY_SIZE, table1, build.constInt(1), fallback);
IrOp elem1 = build.inst(IrCmd::GET_ARR_ADDR, table1, build.constInt(1));
IrOp value1 = build.inst(IrCmd::LOAD_TVALUE, elem1, build.constInt(0));
build.inst(IrCmd::STORE_TVALUE, build.vmReg(3), value1);
build.inst(IrCmd::CHECK_ARRAY_SIZE, table1, build.constInt(0), fallback); // This will be removed
IrOp elem2 = build.inst(IrCmd::GET_ARR_ADDR, table1, build.constInt(0));
IrOp value1b = build.inst(IrCmd::LOAD_TVALUE, elem2, build.constInt(0));
build.inst(IrCmd::STORE_TVALUE, build.vmReg(4), value1b);
IrOp a = build.inst(IrCmd::LOAD_DOUBLE, build.vmReg(3));
IrOp b = build.inst(IrCmd::LOAD_DOUBLE, build.vmReg(4));
IrOp sum = build.inst(IrCmd::ADD_NUM, a, b);
build.inst(IrCmd::STORE_DOUBLE, build.vmReg(2), sum);
build.inst(IrCmd::RETURN, build.vmReg(2), build.constUint(1));
build.beginBlock(fallback);
build.inst(IrCmd::RETURN, build.vmReg(0), build.constUint(1));
updateUseCounts(build.function);
constPropInBlockChains(build, true);
CHECK("\n" + toString(build.function, /* includeUseInfo */ false) == R"(
bb_0:
%0 = LOAD_POINTER R1
CHECK_ARRAY_SIZE %0, 1i, bb_fallback_1
%2 = GET_ARR_ADDR %0, 1i
%3 = LOAD_TVALUE %2, 0i
STORE_TVALUE R3, %3
%6 = GET_ARR_ADDR %0, 0i
%7 = LOAD_TVALUE %6, 0i
STORE_TVALUE R4, %7
%9 = LOAD_DOUBLE R3
%10 = LOAD_DOUBLE R4
%11 = ADD_NUM %9, %10
STORE_DOUBLE R2, %11
RETURN R2, 1u
bb_fallback_1:
RETURN R0, 1u
)");
}
TEST_CASE_FIXTURE(IrBuilderFixture, "DuplicateArrayElemChecksInvalidations")
{
ScopedFastFlag luauReuseHashSlots{"LuauReuseArrSlots", true};
IrOp block = build.block(IrBlockKind::Internal);
IrOp fallback = build.block(IrBlockKind::Fallback);
build.beginBlock(block);
// This roughly corresponds to 'return t[1] + t[1]' with a strange table.insert in the middle
IrOp table1 = build.inst(IrCmd::LOAD_POINTER, build.vmReg(1));
build.inst(IrCmd::CHECK_ARRAY_SIZE, table1, build.constInt(0), fallback);
IrOp elem1 = build.inst(IrCmd::GET_ARR_ADDR, table1, build.constInt(0));
IrOp value1 = build.inst(IrCmd::LOAD_TVALUE, elem1, build.constInt(0));
build.inst(IrCmd::STORE_TVALUE, build.vmReg(3), value1);
build.inst(IrCmd::TABLE_SETNUM, table1, build.constInt(2));
build.inst(IrCmd::CHECK_ARRAY_SIZE, table1, build.constInt(0), fallback); // This will be removed
IrOp elem2 = build.inst(IrCmd::GET_ARR_ADDR, table1, build.constInt(0)); // And this will be substituted
IrOp value1b = build.inst(IrCmd::LOAD_TVALUE, elem2, build.constInt(0));
build.inst(IrCmd::STORE_TVALUE, build.vmReg(4), value1b);
IrOp a = build.inst(IrCmd::LOAD_DOUBLE, build.vmReg(3));
IrOp b = build.inst(IrCmd::LOAD_DOUBLE, build.vmReg(4));
IrOp sum = build.inst(IrCmd::ADD_NUM, a, b);
build.inst(IrCmd::STORE_DOUBLE, build.vmReg(2), sum);
build.inst(IrCmd::RETURN, build.vmReg(2), build.constUint(1));
build.beginBlock(fallback);
build.inst(IrCmd::RETURN, build.vmReg(0), build.constUint(1));
updateUseCounts(build.function);
constPropInBlockChains(build, true);
CHECK("\n" + toString(build.function, /* includeUseInfo */ false) == R"(
bb_0:
%0 = LOAD_POINTER R1
CHECK_ARRAY_SIZE %0, 0i, bb_fallback_1
%2 = GET_ARR_ADDR %0, 0i
%3 = LOAD_TVALUE %2, 0i
STORE_TVALUE R3, %3
%5 = TABLE_SETNUM %0, 2i
CHECK_ARRAY_SIZE %0, 0i, bb_fallback_1
%7 = GET_ARR_ADDR %0, 0i
%8 = LOAD_TVALUE %7, 0i
STORE_TVALUE R4, %8
%10 = LOAD_DOUBLE R3
%11 = LOAD_DOUBLE R4
%12 = ADD_NUM %10, %11
STORE_DOUBLE R2, %12
RETURN R2, 1u
bb_fallback_1:
RETURN R0, 1u
)");
}
TEST_SUITE_END();
TEST_SUITE_BEGIN("Analysis");

View file

@ -489,15 +489,20 @@ TEST_CASE_FIXTURE(BuiltinsFixture, "do_not_clone_types_of_reexported_values")
LUAU_REQUIRE_NO_ERRORS(result);
ModulePtr modA = frontend.moduleResolver.getModule("Module/A");
ModulePtr modB = frontend.moduleResolver.getModule("Module/B");
REQUIRE(modA);
ModulePtr modB = frontend.moduleResolver.getModule("Module/B");
REQUIRE(modB);
std::optional<TypeId> typeA = first(modA->returnType);
std::optional<TypeId> typeB = first(modB->returnType);
REQUIRE(typeA);
std::optional<TypeId> typeB = first(modB->returnType);
REQUIRE(typeB);
TableType* tableA = getMutable<TableType>(*typeA);
REQUIRE_MESSAGE(tableA, "Expected a table, but got " << toString(*typeA));
TableType* tableB = getMutable<TableType>(*typeB);
REQUIRE_MESSAGE(tableB, "Expected a table, but got " << toString(*typeB));
CHECK(tableA->props["a"].type() == tableB->props["b"].type());
}

View file

@ -0,0 +1,17 @@
// This file is part of the Luau programming language and is licensed under MIT License; see LICENSE.txt for details
#include "Luau/NonStrictTypeChecker.h"
#include "Fixture.h"
#include "doctest.h"
using namespace Luau;
TEST_SUITE_BEGIN("NonStrictTypeCheckerTest");
TEST_CASE_FIXTURE(Fixture, "basic")
{
Luau::checkNonStrict(builtinTypes, nullptr);
}
TEST_SUITE_END();

View file

@ -797,6 +797,9 @@ TEST_CASE_FIXTURE(NormalizeFixture, "narrow_union_of_classes_with_intersection")
TEST_CASE_FIXTURE(NormalizeFixture, "intersection_of_metatables_where_the_metatable_is_top_or_bottom")
{
if (FFlag::DebugLuauDeferredConstraintResolution)
CHECK("{ @metatable *error-type*, { } }" == toString(normal("Mt<{}, any> & Mt<{}, err>")));
else
CHECK("{ @metatable *error-type*, {| |} }" == toString(normal("Mt<{}, any> & Mt<{}, err>")));
}
@ -863,6 +866,9 @@ TEST_CASE_FIXTURE(NormalizeFixture, "classes_and_never")
TEST_CASE_FIXTURE(NormalizeFixture, "top_table_type")
{
CHECK("table" == toString(normal("{} | tbl")));
if (FFlag::DebugLuauDeferredConstraintResolution)
CHECK("{ }" == toString(normal("{} & tbl")));
else
CHECK("{| |}" == toString(normal("{} & tbl")));
CHECK("never" == toString(normal("number & tbl")));
}

View file

@ -394,8 +394,8 @@ TEST_CASE_FIXTURE(SimplifyFixture, "table_with_a_tag")
TypeId t1 = mkTable({{"tag", stringTy}, {"prop", numberTy}});
TypeId t2 = mkTable({{"tag", helloTy}});
CHECK("{| prop: number, tag: string |} & {| tag: \"hello\" |}" == intersectStr(t1, t2));
CHECK("{| prop: number, tag: string |} & {| tag: \"hello\" |}" == intersectStr(t2, t1));
CHECK("{ prop: number, tag: string } & { tag: \"hello\" }" == intersectStr(t1, t2));
CHECK("{ prop: number, tag: string } & { tag: \"hello\" }" == intersectStr(t2, t1));
}
TEST_CASE_FIXTURE(SimplifyFixture, "nested_table_tag_test")

View file

@ -50,6 +50,9 @@ TEST_CASE_FIXTURE(Fixture, "cyclic_table")
TableType* tableOne = getMutable<TableType>(&cyclicTable);
tableOne->props["self"] = {&cyclicTable};
if (FFlag::DebugLuauDeferredConstraintResolution)
CHECK_EQ("t1 where t1 = {| self: t1 |}", toString(&cyclicTable));
else
CHECK_EQ("t1 where t1 = { self: t1 }", toString(&cyclicTable));
}
@ -68,11 +71,17 @@ TEST_CASE_FIXTURE(Fixture, "empty_table")
local a: {}
)");
if (FFlag::DebugLuauDeferredConstraintResolution)
CHECK_EQ("{ }", toString(requireType("a")));
else
CHECK_EQ("{| |}", toString(requireType("a")));
// Should stay the same with useLineBreaks enabled
ToStringOptions opts;
opts.useLineBreaks = true;
if (FFlag::DebugLuauDeferredConstraintResolution)
CHECK_EQ("{ }", toString(requireType("a"), opts));
else
CHECK_EQ("{| |}", toString(requireType("a"), opts));
}
@ -86,6 +95,14 @@ TEST_CASE_FIXTURE(Fixture, "table_respects_use_line_break")
opts.useLineBreaks = true;
//clang-format off
if (FFlag::DebugLuauDeferredConstraintResolution)
CHECK_EQ("{\n"
" anotherProp: number,\n"
" prop: string,\n"
" thirdProp: boolean\n"
"}",
toString(requireType("a"), opts));
else
CHECK_EQ("{|\n"
" anotherProp: number,\n"
" prop: string,\n"
@ -122,6 +139,9 @@ TEST_CASE_FIXTURE(Fixture, "metatable")
Type table{TypeVariant(TableType())};
Type metatable{TypeVariant(TableType())};
Type mtv{TypeVariant(MetatableType{&table, &metatable})};
if (FFlag::DebugLuauDeferredConstraintResolution)
CHECK_EQ("{ @metatable {| |}, {| |} }", toString(&mtv));
else
CHECK_EQ("{ @metatable { }, { } }", toString(&mtv));
}
@ -258,6 +278,9 @@ TEST_CASE_FIXTURE(Fixture, "quit_stringifying_table_type_when_length_is_exceeded
ToStringOptions o;
o.exhaustive = false;
o.maxTableLength = 40;
if (FFlag::DebugLuauDeferredConstraintResolution)
CHECK_EQ(toString(&tv, o), "{| a: number, b: number, c: number, d: number, e: number, ... 10 more ... |}");
else
CHECK_EQ(toString(&tv, o), "{ a: number, b: number, c: number, d: number, e: number, ... 10 more ... }");
}
@ -272,6 +295,9 @@ TEST_CASE_FIXTURE(Fixture, "stringifying_table_type_is_still_capped_when_exhaust
ToStringOptions o;
o.exhaustive = true;
o.maxTableLength = 40;
if (FFlag::DebugLuauDeferredConstraintResolution)
CHECK_EQ(toString(&tv, o), "{| a: number, b: number, c: number, d: number, e: number, ... 2 more ... |}");
else
CHECK_EQ(toString(&tv, o), "{ a: number, b: number, c: number, d: number, e: number, ... 2 more ... }");
}
@ -346,6 +372,9 @@ TEST_CASE_FIXTURE(Fixture, "stringifying_table_type_correctly_use_matching_table
ToStringOptions o;
o.maxTableLength = 40;
if (FFlag::DebugLuauDeferredConstraintResolution)
CHECK_EQ(toString(&tv, o), "{ a: number, b: number, c: number, d: number, e: number, ... 5 more ... }");
else
CHECK_EQ(toString(&tv, o), "{| a: number, b: number, c: number, d: number, e: number, ... 5 more ... |}");
}
@ -377,6 +406,9 @@ TEST_CASE_FIXTURE(Fixture, "stringifying_array_uses_array_syntax")
CHECK_EQ("{string}", toString(Type{ttv}));
ttv.props["A"] = {builtinTypes->numberType};
if (FFlag::DebugLuauDeferredConstraintResolution)
CHECK_EQ("{ [number]: string, A: number }", toString(Type{ttv}));
else
CHECK_EQ("{| [number]: string, A: number |}", toString(Type{ttv}));
ttv.props.clear();
@ -576,9 +608,18 @@ TEST_CASE_FIXTURE(Fixture, "toString_the_boundTo_table_type_contained_within_a_T
TypePackVar tpv2{TypePack{{&tv2}}};
if (FFlag::DebugLuauDeferredConstraintResolution)
{
CHECK_EQ("{ hello: number, world: number }", toString(&tpv1));
CHECK_EQ("{ hello: number, world: number }", toString(&tpv2));
}
else
{
CHECK_EQ("{| hello: number, world: number |}", toString(&tpv1));
CHECK_EQ("{| hello: number, world: number |}", toString(&tpv2));
}
}
TEST_CASE_FIXTURE(Fixture, "no_parentheses_around_return_type_if_pack_has_an_empty_head_link")
{
@ -846,7 +887,24 @@ TEST_CASE_FIXTURE(Fixture, "tostring_error_mismatch")
end
)");
std::string expected = R"(Type
//clang-format off
std::string expected =
(FFlag::DebugLuauDeferredConstraintResolution) ?
R"(Type
'{| a: number, b: string, c: {| d: string |} |}'
could not be converted into
'{ a: number, b: string, c: { d: number } }'
caused by:
Property 'c' is not compatible.
Type
'{| d: string |}'
could not be converted into
'{ d: number }'
caused by:
Property 'd' is not compatible.
Type 'string' could not be converted into 'number' in an invariant context)"
:
R"(Type
'{ a: number, b: string, c: { d: string } }'
could not be converted into
'{| a: number, b: string, c: {| d: number |} |}'
@ -859,9 +917,12 @@ could not be converted into
caused by:
Property 'd' is not compatible.
Type 'string' could not be converted into 'number' in an invariant context)";
//clang-format on
//
std::string actual = toString(result.errors[0]);
LUAU_REQUIRE_ERROR_COUNT(1, result);
CHECK(expected == actual);
}
TEST_SUITE_END();

View file

@ -223,7 +223,7 @@ TEST_CASE_FIXTURE(Fixture, "dependent_generic_aliases")
const std::string expected = R"(Type 'bad' could not be converted into 'U<number>'
caused by:
Property 't' is not compatible.
Type '{ v: string }' could not be converted into 'T<number>'
Type '{| v: string |}' could not be converted into 'T<number>'
caused by:
Property 'v' is not compatible.
Type 'string' could not be converted into 'number' in an invariant context)";
@ -332,6 +332,9 @@ TEST_CASE_FIXTURE(Fixture, "stringify_type_alias_of_recursive_template_table_typ
TypeMismatch* tm = get<TypeMismatch>(result.errors[0]);
REQUIRE(tm);
if (FFlag::DebugLuauDeferredConstraintResolution)
CHECK_EQ("t1 where t1 = ({ a: t1 }) -> string", toString(tm->wantedType));
else
CHECK_EQ("t1 where t1 = ({| a: t1 |}) -> string", toString(tm->wantedType));
CHECK_EQ(builtinTypes->numberType, tm->givenType);
}
@ -815,6 +818,9 @@ TEST_CASE_FIXTURE(Fixture, "forward_declared_alias_is_not_clobbered_by_prior_uni
local d: FutureType = { smth = true } -- missing error, 'd' is resolved to 'any'
)");
if (FFlag::DebugLuauDeferredConstraintResolution)
CHECK_EQ("{ foo: number }", toString(requireType("d"), {true}));
else
CHECK_EQ("{| foo: number |}", toString(requireType("d"), {true}));
LUAU_REQUIRE_ERROR_COUNT(1, result);

View file

@ -398,6 +398,9 @@ TEST_CASE_FIXTURE(BuiltinsFixture, "table_pack")
)");
LUAU_REQUIRE_NO_ERRORS(result);
if (FFlag::DebugLuauDeferredConstraintResolution)
CHECK_EQ("{ [number]: boolean | number | string, n: number }", toString(requireType("t")));
else
CHECK_EQ("{| [number]: boolean | number | string, n: number |}", toString(requireType("t")));
}
@ -413,6 +416,9 @@ local t = table.pack(f())
)");
LUAU_REQUIRE_NO_ERRORS(result);
if (FFlag::DebugLuauDeferredConstraintResolution)
CHECK_EQ("{ [number]: number | string, n: number }", toString(requireType("t")));
else
CHECK_EQ("{| [number]: number | string, n: number |}", toString(requireType("t")));
}
@ -423,6 +429,9 @@ TEST_CASE_FIXTURE(BuiltinsFixture, "table_pack_reduce")
)");
LUAU_REQUIRE_NO_ERRORS(result);
if (FFlag::DebugLuauDeferredConstraintResolution)
CHECK_EQ("{ [number]: boolean | number, n: number }", toString(requireType("t")));
else
CHECK_EQ("{| [number]: boolean | number, n: number |}", toString(requireType("t")));
result = check(R"(
@ -430,6 +439,9 @@ TEST_CASE_FIXTURE(BuiltinsFixture, "table_pack_reduce")
)");
LUAU_REQUIRE_NO_ERRORS(result);
if (FFlag::DebugLuauDeferredConstraintResolution)
CHECK_EQ("{ [number]: string, n: number }", toString(requireType("t")));
else
CHECK_EQ("{| [number]: string, n: number |}", toString(requireType("t")));
}

View file

@ -26,6 +26,52 @@ TEST_CASE_FIXTURE(BuiltinsFixture, "if_not_x_return")
CHECK_EQ("string", toString(requireTypeAtPosition({6, 24})));
}
TEST_CASE_FIXTURE(BuiltinsFixture, "if_not_x_break")
{
ScopedFastFlag flags[] = {
{"LuauTinyControlFlowAnalysis", true},
{"LuauLoopControlFlowAnalysis", true}
};
CheckResult result = check(R"(
local function f(x: {{value: string?}})
for _, record in x do
if not record.value then
break
end
local foo = record.value
end
end
)");
LUAU_REQUIRE_NO_ERRORS(result);
CHECK_EQ("string", toString(requireTypeAtPosition({7, 34})));
}
TEST_CASE_FIXTURE(BuiltinsFixture, "if_not_x_continue")
{
ScopedFastFlag flags[] = {
{"LuauTinyControlFlowAnalysis", true},
{"LuauLoopControlFlowAnalysis", true}
};
CheckResult result = check(R"(
local function f(x: {{value: string?}})
for _, record in x do
if not record.value then
continue
end
local foo = record.value
end
end
)");
LUAU_REQUIRE_NO_ERRORS(result);
CHECK_EQ("string", toString(requireTypeAtPosition({7, 38})));
}
TEST_CASE_FIXTURE(BuiltinsFixture, "if_not_x_return_elif_not_y_return")
{
ScopedFastFlag sff{"LuauTinyControlFlowAnalysis", true};
@ -48,6 +94,118 @@ TEST_CASE_FIXTURE(BuiltinsFixture, "if_not_x_return_elif_not_y_return")
CHECK_EQ("string", toString(requireTypeAtPosition({9, 24})));
}
TEST_CASE_FIXTURE(BuiltinsFixture, "if_not_x_break_elif_not_y_break")
{
ScopedFastFlag flags[] = {
{"LuauTinyControlFlowAnalysis", true},
{"LuauLoopControlFlowAnalysis", true}
};
CheckResult result = check(R"(
local function f(x: {{value: string?}}, y: {{value: string?}})
for i, recordX in x do
local recordY = y[i]
if not recordX.value then
break
elseif not recordY.value then
break
end
local foo = recordX.value
local bar = recordY.value
end
end
)");
LUAU_REQUIRE_NO_ERRORS(result);
CHECK_EQ("string", toString(requireTypeAtPosition({10, 38})));
CHECK_EQ("string", toString(requireTypeAtPosition({11, 38})));
}
TEST_CASE_FIXTURE(BuiltinsFixture, "if_not_x_continue_elif_not_y_continue")
{
ScopedFastFlag flags[] = {
{"LuauTinyControlFlowAnalysis", true},
{"LuauLoopControlFlowAnalysis", true}
};
CheckResult result = check(R"(
local function f(x: {{value: string?}}, y: {{value: string?}})
for i, recordX in x do
local recordY = y[i]
if not recordX.value then
continue
elseif not recordY.value then
continue
end
local foo = recordX.value
local bar = recordY.value
end
end
)");
LUAU_REQUIRE_NO_ERRORS(result);
CHECK_EQ("string", toString(requireTypeAtPosition({10, 38})));
CHECK_EQ("string", toString(requireTypeAtPosition({11, 38})));
}
TEST_CASE_FIXTURE(BuiltinsFixture, "if_not_x_return_elif_not_y_break")
{
ScopedFastFlag flags[] = {
{"LuauTinyControlFlowAnalysis", true},
{"LuauLoopControlFlowAnalysis", true}
};
CheckResult result = check(R"(
local function f(x: {{value: string?}}, y: {{value: string?}})
for i, recordX in x do
local recordY = y[i]
if not recordX.value then
return
elseif not recordY.value then
break
end
local foo = recordX.value
local bar = recordY.value
end
end
)");
LUAU_REQUIRE_NO_ERRORS(result);
CHECK_EQ("string", toString(requireTypeAtPosition({10, 38})));
CHECK_EQ("string", toString(requireTypeAtPosition({11, 38})));
}
TEST_CASE_FIXTURE(BuiltinsFixture, "if_not_x_break_elif_not_y_continue")
{
ScopedFastFlag flags[] = {
{"LuauTinyControlFlowAnalysis", true},
{"LuauLoopControlFlowAnalysis", true}
};
CheckResult result = check(R"(
local function f(x: {{value: string?}}, y: {{value: string?}})
for i, recordX in x do
local recordY = y[i]
if not recordX.value then
break
elseif not recordY.value then
continue
end
local foo = recordX.value
local bar = recordY.value
end
end
)");
LUAU_REQUIRE_NO_ERRORS(result);
CHECK_EQ("string", toString(requireTypeAtPosition({10, 38})));
CHECK_EQ("string", toString(requireTypeAtPosition({11, 38})));
}
TEST_CASE_FIXTURE(BuiltinsFixture, "if_not_x_return_elif_rand_return_elif_not_y_return")
{
ScopedFastFlag sff{"LuauTinyControlFlowAnalysis", true};
@ -72,6 +230,66 @@ TEST_CASE_FIXTURE(BuiltinsFixture, "if_not_x_return_elif_rand_return_elif_not_y_
CHECK_EQ("string", toString(requireTypeAtPosition({11, 24})));
}
TEST_CASE_FIXTURE(BuiltinsFixture, "if_not_x_break_elif_rand_break_elif_not_y_break")
{
ScopedFastFlag flags[] = {
{"LuauTinyControlFlowAnalysis", true},
{"LuauLoopControlFlowAnalysis", true}
};
CheckResult result = check(R"(
local function f(x: {{value: string?}}, y: {{value: string?}})
for i, recordX in x do
local recordY = y[i]
if not recordX.value then
break
elseif math.random() > 0.5 then
break
elseif not recordY.value then
break
end
local foo = recordX.value
local bar = recordY.value
end
end
)");
LUAU_REQUIRE_NO_ERRORS(result);
CHECK_EQ("string", toString(requireTypeAtPosition({12, 38})));
CHECK_EQ("string", toString(requireTypeAtPosition({13, 38})));
}
TEST_CASE_FIXTURE(BuiltinsFixture, "if_not_x_continue_elif_rand_continue_elif_not_y_continue")
{
ScopedFastFlag flags[] = {
{"LuauTinyControlFlowAnalysis", true},
{"LuauLoopControlFlowAnalysis", true}
};
CheckResult result = check(R"(
local function f(x: {{value: string?}}, y: {{value: string?}})
for i, recordX in x do
local recordY = y[i]
if not recordX.value then
continue
elseif math.random() > 0.5 then
continue
elseif not recordY.value then
continue
end
local foo = recordX.value
local bar = recordY.value
end
end
)");
LUAU_REQUIRE_NO_ERRORS(result);
CHECK_EQ("string", toString(requireTypeAtPosition({12, 38})));
CHECK_EQ("string", toString(requireTypeAtPosition({13, 38})));
}
TEST_CASE_FIXTURE(BuiltinsFixture, "if_not_x_return_elif_not_rand_return_elif_not_y_fallthrough")
{
ScopedFastFlag sff{"LuauTinyControlFlowAnalysis", true};
@ -96,6 +314,66 @@ TEST_CASE_FIXTURE(BuiltinsFixture, "if_not_x_return_elif_not_rand_return_elif_no
CHECK_EQ("string?", toString(requireTypeAtPosition({11, 24})));
}
TEST_CASE_FIXTURE(BuiltinsFixture, "if_not_x_break_elif_rand_break_elif_not_y_fallthrough")
{
ScopedFastFlag flags[] = {
{"LuauTinyControlFlowAnalysis", true},
{"LuauLoopControlFlowAnalysis", true}
};
CheckResult result = check(R"(
local function f(x: {{value: string?}}, y: {{value: string?}})
for i, recordX in x do
local recordY = y[i]
if not recordX.value then
break
elseif math.random() > 0.5 then
break
elseif not recordY.value then
end
local foo = recordX.value
local bar = recordY.value
end
end
)");
LUAU_REQUIRE_NO_ERRORS(result);
CHECK_EQ("string", toString(requireTypeAtPosition({12, 38})));
CHECK_EQ("string?", toString(requireTypeAtPosition({13, 38})));
}
TEST_CASE_FIXTURE(BuiltinsFixture, "if_not_x_continue_elif_rand_continue_elif_not_y_fallthrough")
{
ScopedFastFlag flags[] = {
{"LuauTinyControlFlowAnalysis", true},
{"LuauLoopControlFlowAnalysis", true}
};
CheckResult result = check(R"(
local function f(x: {{value: string?}}, y: {{value: string?}})
for i, recordX in x do
local recordY = y[i]
if not recordX.value then
continue
elseif math.random() > 0.5 then
continue
elseif not recordY.value then
end
local foo = recordX.value
local bar = recordY.value
end
end
)");
LUAU_REQUIRE_NO_ERRORS(result);
CHECK_EQ("string", toString(requireTypeAtPosition({12, 38})));
CHECK_EQ("string?", toString(requireTypeAtPosition({13, 38})));
}
TEST_CASE_FIXTURE(BuiltinsFixture, "if_not_x_return_elif_not_y_fallthrough_elif_not_z_return")
{
ScopedFastFlag sff{"LuauTinyControlFlowAnalysis", true};
@ -122,6 +400,138 @@ TEST_CASE_FIXTURE(BuiltinsFixture, "if_not_x_return_elif_not_y_fallthrough_elif_
CHECK_EQ("string?", toString(requireTypeAtPosition({12, 24})));
}
TEST_CASE_FIXTURE(BuiltinsFixture, "if_not_x_break_elif_not_y_fallthrough_elif_not_z_break")
{
ScopedFastFlag flags[] = {
{"LuauTinyControlFlowAnalysis", true},
{"LuauLoopControlFlowAnalysis", true}
};
CheckResult result = check(R"(
local function f(x: {{value: string?}}, y: {{value: string?}}, z: {{value: string?}})
for i, recordX in x do
local recordY = y[i]
local recordZ = y[i]
if not recordX.value then
break
elseif not recordY.value then
elseif not recordZ.value then
break
end
local foo = recordX.value
local bar = recordY.value
local baz = recordZ.value
end
end
)");
LUAU_REQUIRE_NO_ERRORS(result);
CHECK_EQ("string", toString(requireTypeAtPosition({13, 38})));
CHECK_EQ("string?", toString(requireTypeAtPosition({14, 38})));
CHECK_EQ("string?", toString(requireTypeAtPosition({15, 38})));
}
TEST_CASE_FIXTURE(BuiltinsFixture, "if_not_x_continue_elif_not_y_fallthrough_elif_not_z_continue")
{
ScopedFastFlag flags[] = {
{"LuauTinyControlFlowAnalysis", true},
{"LuauLoopControlFlowAnalysis", true}
};
CheckResult result = check(R"(
local function f(x: {{value: string?}}, y: {{value: string?}}, z: {{value: string?}})
for i, recordX in x do
local recordY = y[i]
local recordZ = y[i]
if not recordX.value then
continue
elseif not recordY.value then
elseif not recordZ.value then
continue
end
local foo = recordX.value
local bar = recordY.value
local baz = recordZ.value
end
end
)");
LUAU_REQUIRE_NO_ERRORS(result);
CHECK_EQ("string", toString(requireTypeAtPosition({13, 38})));
CHECK_EQ("string?", toString(requireTypeAtPosition({14, 38})));
CHECK_EQ("string?", toString(requireTypeAtPosition({15, 38})));
}
TEST_CASE_FIXTURE(BuiltinsFixture, "if_not_x_continue_elif_not_y_throw_elif_not_z_fallthrough")
{
ScopedFastFlag flags[] = {
{"LuauTinyControlFlowAnalysis", true},
{"LuauLoopControlFlowAnalysis", true}
};
CheckResult result = check(R"(
local function f(x: {{value: string?}}, y: {{value: string?}}, z: {{value: string?}})
for i, recordX in x do
local recordY = y[i]
local recordZ = y[i]
if not recordX.value then
continue
elseif not recordY.value then
error("Y value not defined")
elseif not recordZ.value then
end
local foo = recordX.value
local bar = recordY.value
local baz = recordZ.value
end
end
)");
LUAU_REQUIRE_NO_ERRORS(result);
CHECK_EQ("string", toString(requireTypeAtPosition({13, 38})));
CHECK_EQ("string", toString(requireTypeAtPosition({14, 38})));
CHECK_EQ("string?", toString(requireTypeAtPosition({15, 38})));
}
TEST_CASE_FIXTURE(BuiltinsFixture, "if_not_x_return_elif_not_y_fallthrough_elif_not_z_break")
{
ScopedFastFlag flags[] = {
{"LuauTinyControlFlowAnalysis", true},
{"LuauLoopControlFlowAnalysis", true}
};
CheckResult result = check(R"(
local function f(x: {{value: string?}}, y: {{value: string?}}, z: {{value: string?}})
for i, recordX in x do
local recordY = y[i]
local recordZ = y[i]
if not recordX.value then
return
elseif not recordY.value then
elseif not recordZ.value then
break
end
local foo = recordX.value
local bar = recordY.value
local baz = recordZ.value
end
end
)");
LUAU_REQUIRE_NO_ERRORS(result);
CHECK_EQ("string", toString(requireTypeAtPosition({13, 38})));
CHECK_EQ("string?", toString(requireTypeAtPosition({14, 38})));
CHECK_EQ("string?", toString(requireTypeAtPosition({15, 38})));
}
TEST_CASE_FIXTURE(BuiltinsFixture, "do_if_not_x_return")
{
ScopedFastFlag sff{"LuauTinyControlFlowAnalysis", true};
@ -142,6 +552,56 @@ TEST_CASE_FIXTURE(BuiltinsFixture, "do_if_not_x_return")
CHECK_EQ("string", toString(requireTypeAtPosition({8, 24})));
}
TEST_CASE_FIXTURE(BuiltinsFixture, "for_record_do_if_not_x_break")
{
ScopedFastFlag flags[] = {
{"LuauTinyControlFlowAnalysis", true},
{"LuauLoopControlFlowAnalysis", true}
};
CheckResult result = check(R"(
local function f(x: {{value: string?}})
for _, record in x do
do
if not record.value then
break
end
end
local foo = record.value
end
end
)");
LUAU_REQUIRE_NO_ERRORS(result);
CHECK_EQ("string", toString(requireTypeAtPosition({9, 38})));
}
TEST_CASE_FIXTURE(BuiltinsFixture, "for_record_do_if_not_x_continue")
{
ScopedFastFlag flags[] = {
{"LuauTinyControlFlowAnalysis", true},
{"LuauLoopControlFlowAnalysis", true}
};
CheckResult result = check(R"(
local function f(x: {{value: string?}})
for _, record in x do
do
if not record.value then
continue
end
end
local foo = record.value
end
end
)");
LUAU_REQUIRE_NO_ERRORS(result);
CHECK_EQ("string", toString(requireTypeAtPosition({9, 38})));
}
TEST_CASE_FIXTURE(BuiltinsFixture, "early_return_in_a_loop_which_isnt_guaranteed_to_run_first")
{
ScopedFastFlag sff{"LuauTinyControlFlowAnalysis", true};
@ -271,6 +731,126 @@ TEST_CASE_FIXTURE(BuiltinsFixture, "if_not_x_return_if_not_y_return")
CHECK_EQ("string", toString(requireTypeAtPosition({11, 24})));
}
TEST_CASE_FIXTURE(BuiltinsFixture, "if_not_x_break_if_not_y_break")
{
ScopedFastFlag flags[] = {
{"LuauTinyControlFlowAnalysis", true},
{"LuauLoopControlFlowAnalysis", true}
};
CheckResult result = check(R"(
local function f(x: {{value: string?}}, y: {{value: string?}})
for i, recordX in x do
local recordY = y[i]
if not recordX.value then
break
end
if not recordY.value then
break
end
local foo = recordX.value
local bar = recordY.value
end
end
)");
LUAU_REQUIRE_NO_ERRORS(result);
CHECK_EQ("string", toString(requireTypeAtPosition({12, 38})));
CHECK_EQ("string", toString(requireTypeAtPosition({13, 38})));
}
TEST_CASE_FIXTURE(BuiltinsFixture, "if_not_x_continue_if_not_y_continue")
{
ScopedFastFlag flags[] = {
{"LuauTinyControlFlowAnalysis", true},
{"LuauLoopControlFlowAnalysis", true}
};
CheckResult result = check(R"(
local function f(x: {{value: string?}}, y: {{value: string?}})
for i, recordX in x do
local recordY = y[i]
if not recordX.value then
continue
end
if not recordY.value then
continue
end
local foo = recordX.value
local bar = recordY.value
end
end
)");
LUAU_REQUIRE_NO_ERRORS(result);
CHECK_EQ("string", toString(requireTypeAtPosition({12, 38})));
CHECK_EQ("string", toString(requireTypeAtPosition({13, 38})));
}
TEST_CASE_FIXTURE(BuiltinsFixture, "if_not_x_continue_if_not_y_throw")
{
ScopedFastFlag flags[] = {
{"LuauTinyControlFlowAnalysis", true},
{"LuauLoopControlFlowAnalysis", true}
};
CheckResult result = check(R"(
local function f(x: {{value: string?}}, y: {{value: string?}})
for i, recordX in x do
local recordY = y[i]
if not recordX.value then
continue
end
if not recordY.value then
error("Y value not defined")
end
local foo = recordX.value
local bar = recordY.value
end
end
)");
LUAU_REQUIRE_NO_ERRORS(result);
CHECK_EQ("string", toString(requireTypeAtPosition({12, 38})));
CHECK_EQ("string", toString(requireTypeAtPosition({13, 38})));
}
TEST_CASE_FIXTURE(BuiltinsFixture, "if_not_x_break_if_not_y_continue")
{
ScopedFastFlag flags[] = {
{"LuauTinyControlFlowAnalysis", true},
{"LuauLoopControlFlowAnalysis", true}
};
CheckResult result = check(R"(
local function f(x: {{value: string?}}, y: {{value: string?}})
for i, recordX in x do
local recordY = y[i]
if not recordX.value then
break
end
if not recordY.value then
continue
end
local foo = recordX.value
local bar = recordY.value
end
end
)");
LUAU_REQUIRE_NO_ERRORS(result);
CHECK_EQ("string", toString(requireTypeAtPosition({12, 38})));
CHECK_EQ("string", toString(requireTypeAtPosition({13, 38})));
}
TEST_CASE_FIXTURE(BuiltinsFixture, "type_alias_does_not_leak_out")
{
ScopedFastFlag sff{"LuauTinyControlFlowAnalysis", true};
@ -294,6 +874,62 @@ TEST_CASE_FIXTURE(BuiltinsFixture, "type_alias_does_not_leak_out")
CHECK_EQ("nil", toString(requireTypeAtPosition({8, 29})));
}
TEST_CASE_FIXTURE(BuiltinsFixture, "type_alias_does_not_leak_out_breaking")
{
ScopedFastFlag flags[] = {
{"LuauTinyControlFlowAnalysis", true},
{"LuauLoopControlFlowAnalysis", true}
};
CheckResult result = check(R"(
local function f(x: {{value: string?}})
for _, record in x do
if typeof(record.value) == "string" then
break
else
type Foo = number
end
local foo: Foo = record.value
end
end
)");
LUAU_REQUIRE_ERROR_COUNT(1, result);
CHECK_EQ("Unknown type 'Foo'", toString(result.errors[0]));
CHECK_EQ("nil", toString(requireTypeAtPosition({9, 43})));
}
TEST_CASE_FIXTURE(BuiltinsFixture, "type_alias_does_not_leak_out_continuing")
{
ScopedFastFlag flags[] = {
{"LuauTinyControlFlowAnalysis", true},
{"LuauLoopControlFlowAnalysis", true}
};
CheckResult result = check(R"(
local function f(x: {{value: string?}})
for _, record in x do
if typeof(record.value) == "string" then
continue
else
type Foo = number
end
local foo: Foo = record.value
end
end
)");
LUAU_REQUIRE_ERROR_COUNT(1, result);
CHECK_EQ("Unknown type 'Foo'", toString(result.errors[0]));
CHECK_EQ("nil", toString(requireTypeAtPosition({9, 43})));
}
TEST_CASE_FIXTURE(BuiltinsFixture, "prototyping_and_visiting_alias_has_the_same_scope")
{
ScopedFastFlag sff{"LuauTinyControlFlowAnalysis", true};
@ -320,6 +956,62 @@ TEST_CASE_FIXTURE(BuiltinsFixture, "prototyping_and_visiting_alias_has_the_same_
CHECK_EQ("nil", toString(requireTypeAtPosition({8, 29})));
}
TEST_CASE_FIXTURE(BuiltinsFixture, "prototyping_and_visiting_alias_has_the_same_scope_breaking")
{
ScopedFastFlag flags[] = {
{"LuauTinyControlFlowAnalysis", true},
{"LuauLoopControlFlowAnalysis", true}
};
CheckResult result = check(R"(
local function f(x: {{value: string?}})
for _, record in x do
type Foo = number
if typeof(record.value) == "string" then
break
end
local foo: Foo = record.value
end
end
)");
LUAU_REQUIRE_ERROR_COUNT(1, result);
CHECK_EQ("Type 'nil' could not be converted into 'number'", toString(result.errors[0]));
CHECK_EQ("nil", toString(requireTypeAtPosition({9, 43})));
}
TEST_CASE_FIXTURE(BuiltinsFixture, "prototyping_and_visiting_alias_has_the_same_scope_continuing")
{
ScopedFastFlag flags[] = {
{"LuauTinyControlFlowAnalysis", true},
{"LuauLoopControlFlowAnalysis", true}
};
CheckResult result = check(R"(
local function f(x: {{value: string?}})
for _, record in x do
type Foo = number
if typeof(record.value) == "string" then
continue
end
local foo: Foo = record.value
end
end
)");
LUAU_REQUIRE_ERROR_COUNT(1, result);
CHECK_EQ("Type 'nil' could not be converted into 'number'", toString(result.errors[0]));
CHECK_EQ("nil", toString(requireTypeAtPosition({9, 43})));
}
TEST_CASE_FIXTURE(BuiltinsFixture, "tagged_unions")
{
ScopedFastFlag sff{"LuauTinyControlFlowAnalysis", true};
@ -355,6 +1047,78 @@ TEST_CASE_FIXTURE(BuiltinsFixture, "tagged_unions")
CHECK_EQ("Err<E>", toString(requireTypeAtPosition({16, 19})));
}
TEST_CASE_FIXTURE(BuiltinsFixture, "tagged_unions_breaking")
{
ScopedFastFlag flags[] = {
{"LuauTinyControlFlowAnalysis", true},
{"LuauLoopControlFlowAnalysis", true}
};
CheckResult result = check(R"(
type Ok<T> = { tag: "ok", value: T }
type Err<E> = { tag: "err", error: E }
type Result<T, E> = Ok<T> | Err<E>
local function process<T, E>(results: {Result<T, E>})
for _, result in results do
if result.tag == "ok" then
local tag = result.tag
local val = result.value
break
end
local tag = result.tag
local err = result.error
end
end
)");
LUAU_REQUIRE_NO_ERRORS(result);
CHECK_EQ("\"ok\"", toString(requireTypeAtPosition({8, 39})));
CHECK_EQ("T", toString(requireTypeAtPosition({9, 39})));
CHECK_EQ("\"err\"", toString(requireTypeAtPosition({14, 35})));
CHECK_EQ("E", toString(requireTypeAtPosition({15, 35})));
}
TEST_CASE_FIXTURE(BuiltinsFixture, "tagged_unions_continuing")
{
ScopedFastFlag flags[] = {
{"LuauTinyControlFlowAnalysis", true},
{"LuauLoopControlFlowAnalysis", true}
};
CheckResult result = check(R"(
type Ok<T> = { tag: "ok", value: T }
type Err<E> = { tag: "err", error: E }
type Result<T, E> = Ok<T> | Err<E>
local function process<T, E>(results: {Result<T, E>})
for _, result in results do
if result.tag == "ok" then
local tag = result.tag
local val = result.value
continue
end
local tag = result.tag
local err = result.error
end
end
)");
LUAU_REQUIRE_NO_ERRORS(result);
CHECK_EQ("\"ok\"", toString(requireTypeAtPosition({8, 39})));
CHECK_EQ("T", toString(requireTypeAtPosition({9, 39})));
CHECK_EQ("\"err\"", toString(requireTypeAtPosition({14, 35})));
CHECK_EQ("E", toString(requireTypeAtPosition({15, 35})));
}
TEST_CASE_FIXTURE(BuiltinsFixture, "do_assert_x")
{
ScopedFastFlag sff{"LuauTinyControlFlowAnalysis", true};

View file

@ -2077,7 +2077,7 @@ TEST_CASE_FIXTURE(Fixture, "attempt_to_call_an_intersection_of_tables")
LUAU_REQUIRE_ERROR_COUNT(1, result);
if (FFlag::DebugLuauDeferredConstraintResolution)
CHECK_EQ(toString(result.errors[0]), "Cannot call non-function {| x: number |} & {| y: string |}");
CHECK_EQ(toString(result.errors[0]), "Cannot call non-function { x: number } & { y: string }");
else
CHECK_EQ(toString(result.errors[0]), "Cannot call non-function {| x: number |}");
}

View file

@ -164,7 +164,7 @@ TEST_CASE_FIXTURE(Fixture, "index_on_an_intersection_type_with_property_guarante
LUAU_REQUIRE_NO_ERRORS(result);
if (FFlag::DebugLuauDeferredConstraintResolution)
CHECK("{| y: number |}" == toString(requireType("r")));
CHECK("{ y: number }" == toString(requireType("r")));
else
CHECK("{| y: number |} & {| y: number |}" == toString(requireType("r")));
}
@ -513,7 +513,13 @@ TEST_CASE_FIXTURE(Fixture, "intersection_of_tables")
)");
LUAU_REQUIRE_ERROR_COUNT(1, result);
const std::string expected = R"(Type
const std::string expected =
(FFlag::DebugLuauDeferredConstraintResolution) ?
"Type "
"'{ p: number?, q: number?, r: number? } & { p: number?, q: string? }'"
" could not be converted into "
"'{ p: nil }'; none of the intersection parts are compatible" :
R"(Type
'{| p: number?, q: number?, r: number? |} & {| p: number?, q: string? |}'
could not be converted into
'{| p: nil |}'; none of the intersection parts are compatible)";
@ -581,7 +587,13 @@ TEST_CASE_FIXTURE(Fixture, "overloaded_functions_returning_intersections")
)");
LUAU_REQUIRE_ERROR_COUNT(1, result);
const std::string expected = R"(Type
const std::string expected =
(FFlag::DebugLuauDeferredConstraintResolution) ?
R"(Type
'((number?) -> { p: number } & { q: number }) & ((string?) -> { p: number } & { r: number })'
could not be converted into
'(number?) -> { p: number, q: number, r: number }'; none of the intersection parts are compatible)" :
R"(Type
'((number?) -> {| p: number |} & {| q: number |}) & ((string?) -> {| p: number |} & {| r: number |})'
could not be converted into
'(number?) -> {| p: number, q: number, r: number |}'; none of the intersection parts are compatible)";
@ -933,7 +945,7 @@ TEST_CASE_FIXTURE(Fixture, "less_greedy_unification_with_intersection_types_2")
LUAU_REQUIRE_NO_ERRORS(result);
CHECK_EQ("({| x: number |} & {| x: string |}) -> never", toString(requireType("f")));
CHECK_EQ("({ x: number } & { x: string }) -> never", toString(requireType("f")));
}
TEST_CASE_FIXTURE(BuiltinsFixture, "index_property_table_intersection_1")

View file

@ -225,6 +225,9 @@ local tbl: string = require(game.A)
CheckResult result = frontend.check("game/B");
LUAU_REQUIRE_ERROR_COUNT(1, result);
if (FFlag::DebugLuauDeferredConstraintResolution)
CHECK_EQ("Type '{ def: number }' could not be converted into 'string'", toString(result.errors[0]));
else
CHECK_EQ("Type '{| def: number |}' could not be converted into 'string'", toString(result.errors[0]));
}

View file

@ -410,7 +410,7 @@ TEST_CASE_FIXTURE(BuiltinsFixture, "cycle_between_object_constructor_and_alias")
CHECK_MESSAGE(get<MetatableType>(follow(aliasType)), "Expected metatable type but got: " << toString(aliasType));
}
TEST_CASE_FIXTURE(BuiltinsFixture, "promise_type_error_too_complex" * doctest::timeout(0.5))
TEST_CASE_FIXTURE(BuiltinsFixture, "promise_type_error_too_complex" * doctest::timeout(2))
{
// TODO: LTI changes to function call resolution have rendered this test impossibly slow
// shared self should fix it, but there may be other mitigations possible as well

View file

@ -571,6 +571,8 @@ TEST_CASE_FIXTURE(BuiltinsFixture, "typecheck_unary_len_error")
CHECK_EQ("number", toString(requireType("a")));
TypeMismatch* tm = get<TypeMismatch>(result.errors[0]);
REQUIRE_MESSAGE(tm, "Expected a TypeMismatch but got " << result.errors[0]);
REQUIRE_EQ(*tm->wantedType, *builtinTypes->numberType);
REQUIRE_EQ(*tm->givenType, *builtinTypes->stringType);
}

View file

@ -251,12 +251,24 @@ TEST_CASE_FIXTURE(Fixture, "discriminate_from_x_not_equal_to_nil")
LUAU_REQUIRE_NO_ERRORS(result);
if (FFlag::DebugLuauDeferredConstraintResolution)
{
CHECK_EQ("{ x: string, y: number }", toString(requireTypeAtPosition({5, 28})));
// Should be { x: nil, y: nil }
CHECK_EQ("{ x: nil, y: nil } | { x: string, y: number }", toString(requireTypeAtPosition({7, 28})));
}
else
{
CHECK_EQ("{| x: string, y: number |}", toString(requireTypeAtPosition({5, 28})));
// Should be {| x: nil, y: nil |}
CHECK_EQ("{| x: nil, y: nil |} | {| x: string, y: number |}", toString(requireTypeAtPosition({7, 28})));
}
}
TEST_CASE_FIXTURE(BuiltinsFixture, "bail_early_if_unification_is_too_complicated" * doctest::timeout(0.5))
{
ScopedFastInt sffi{"LuauTarjanChildLimit", 1};

View file

@ -535,6 +535,9 @@ TEST_CASE_FIXTURE(Fixture, "unknown_lvalue_is_not_synonymous_with_other_on_not_e
LUAU_REQUIRE_NO_ERRORS(result);
CHECK_EQ(toString(requireTypeAtPosition({3, 33})), "any"); // a ~= b
if (FFlag::DebugLuauDeferredConstraintResolution)
CHECK_EQ(toString(requireTypeAtPosition({3, 36})), "{ x: number }?"); // a ~= b
else
CHECK_EQ(toString(requireTypeAtPosition({3, 36})), "{| x: number |}?"); // a ~= b
}
@ -658,6 +661,9 @@ TEST_CASE_FIXTURE(BuiltinsFixture, "typeguard_narrows_for_table")
LUAU_REQUIRE_NO_ERRORS(result);
if (FFlag::DebugLuauDeferredConstraintResolution)
CHECK_EQ("{ x: number } | { y: boolean }", toString(requireTypeAtPosition({3, 28}))); // type(x) == "table"
else
CHECK_EQ("{| x: number |} | {| y: boolean |}", toString(requireTypeAtPosition({3, 28}))); // type(x) == "table"
CHECK_EQ("string", toString(requireTypeAtPosition({5, 28}))); // type(x) ~= "table"
}
@ -697,6 +703,9 @@ TEST_CASE_FIXTURE(BuiltinsFixture, "type_guard_can_filter_for_intersection_of_ta
ToStringOptions opts;
opts.exhaustive = true;
if (FFlag::DebugLuauDeferredConstraintResolution)
CHECK_EQ("{ x: number } & { y: number }", toString(requireTypeAtPosition({4, 28}), opts));
else
CHECK_EQ("{| x: number |} & {| y: number |}", toString(requireTypeAtPosition({4, 28}), opts));
CHECK_EQ("nil", toString(requireTypeAtPosition({6, 28})));
}
@ -1216,9 +1225,18 @@ TEST_CASE_FIXTURE(RefinementClassFixture, "discriminate_from_isa_of_x")
LUAU_REQUIRE_NO_ERRORS(result);
if (FFlag::DebugLuauDeferredConstraintResolution)
{
CHECK_EQ(R"({ tag: "Part", x: Part })", toString(requireTypeAtPosition({5, 28})));
CHECK_EQ(R"({ tag: "Folder", x: Folder })", toString(requireTypeAtPosition({7, 28})));
}
else
{
CHECK_EQ(R"({| tag: "Part", x: Part |})", toString(requireTypeAtPosition({5, 28})));
CHECK_EQ(R"({| tag: "Folder", x: Folder |})", toString(requireTypeAtPosition({7, 28})));
}
}
TEST_CASE_FIXTURE(RefinementClassFixture, "typeguard_cast_free_table_to_vector")
{

View file

@ -91,6 +91,9 @@ TEST_CASE_FIXTURE(Fixture, "cannot_augment_sealed_table")
// TODO: better, more robust comparison of type vars
auto s = toString(error->tableType, ToStringOptions{/*exhaustive*/ true});
if (FFlag::DebugLuauDeferredConstraintResolution)
CHECK_EQ(s, "{ prop: number }");
else
CHECK_EQ(s, "{| prop: number |}");
CHECK_EQ(error->prop, "foo");
CHECK_EQ(error->context, CannotExtendTable::Property);
@ -733,6 +736,9 @@ TEST_CASE_FIXTURE(Fixture, "infer_indexer_from_value_property_in_literal")
CHECK(bool(retType->indexer));
const TableIndexer& indexer = *retType->indexer;
if (FFlag::DebugLuauDeferredConstraintResolution)
CHECK_EQ("{ __name: string }", toString(indexer.indexType));
else
CHECK_EQ("{| __name: string |}", toString(indexer.indexType));
}
@ -775,6 +781,9 @@ TEST_CASE_FIXTURE(Fixture, "indexer_mismatch")
TypeMismatch* tm = get<TypeMismatch>(result.errors[0]);
REQUIRE(tm != nullptr);
CHECK(toString(tm->wantedType) == "{number}");
if (FFlag::DebugLuauDeferredConstraintResolution)
CHECK(toString(tm->givenType) == "{ [string]: string }");
else
CHECK(toString(tm->givenType) == "{| [string]: string |}");
CHECK_NE(*t1, *t2);
@ -1564,9 +1573,17 @@ TEST_CASE_FIXTURE(Fixture, "casting_sealed_tables_with_props_into_table_with_ind
ToStringOptions o{/* exhaustive= */ true};
TypeMismatch* tm = get<TypeMismatch>(result.errors[0]);
REQUIRE(tm);
if (FFlag::DebugLuauDeferredConstraintResolution)
{
CHECK_EQ("{ [string]: string }", toString(tm->wantedType, o));
CHECK_EQ("{ foo: number }", toString(tm->givenType, o));
}
else
{
CHECK_EQ("{| [string]: string |}", toString(tm->wantedType, o));
CHECK_EQ("{| foo: number |}", toString(tm->givenType, o));
}
}
TEST_CASE_FIXTURE(Fixture, "casting_tables_with_props_into_table_with_indexer2")
{
@ -1803,9 +1820,17 @@ TEST_CASE_FIXTURE(Fixture, "hide_table_error_properties")
LUAU_REQUIRE_ERROR_COUNT(2, result);
if (FFlag::DebugLuauDeferredConstraintResolution)
{
CHECK_EQ("Cannot add property 'a' to table '{ x: number }'", toString(result.errors[0]));
CHECK_EQ("Cannot add property 'b' to table '{ x: number }'", toString(result.errors[1]));
}
else
{
CHECK_EQ("Cannot add property 'a' to table '{| x: number |}'", toString(result.errors[0]));
CHECK_EQ("Cannot add property 'b' to table '{| x: number |}'", toString(result.errors[1]));
}
}
TEST_CASE_FIXTURE(BuiltinsFixture, "builtin_table_names")
{
@ -2969,6 +2994,9 @@ TEST_CASE_FIXTURE(BuiltinsFixture, "access_index_metamethod_that_returns_variadi
ToStringOptions o;
o.exhaustive = true;
if (FFlag::DebugLuauDeferredConstraintResolution)
CHECK_EQ("{ x: string }", toString(requireType("foo"), o));
else
CHECK_EQ("{| x: string |}", toString(requireType("foo"), o));
}
@ -3029,6 +3057,9 @@ TEST_CASE_FIXTURE(Fixture, "accidentally_checked_prop_in_opposite_branch")
)");
LUAU_REQUIRE_ERROR_COUNT(1, result);
if (FFlag::DebugLuauDeferredConstraintResolution)
CHECK_EQ("Value of type '{ x: number? }?' could be nil", toString(result.errors[0]));
else
CHECK_EQ("Value of type '{| x: number? |}?' could be nil", toString(result.errors[0]));
CHECK_EQ("boolean", toString(requireType("u")));
}
@ -3260,6 +3291,9 @@ TEST_CASE_FIXTURE(Fixture, "prop_access_on_unions_of_indexers_where_key_whose_ty
)");
LUAU_REQUIRE_ERROR_COUNT(1, result);
if (FFlag::DebugLuauDeferredConstraintResolution)
CHECK_EQ("Type '{ [boolean]: number } | {number}' does not have key 'x'", toString(result.errors[0]));
else
CHECK_EQ("Type '{number} | {| [boolean]: number |}' does not have key 'x'", toString(result.errors[0]));
}
@ -3824,4 +3858,24 @@ TEST_CASE_FIXTURE(Fixture, "cli_84607_missing_prop_in_array_or_dict")
CHECK_EQ("prop", error2->properties[0]);
}
TEST_CASE_FIXTURE(Fixture, "simple_method_definition")
{
CheckResult result = check(R"(
local T = {}
function T:m()
return 5
end
return T
)");
LUAU_REQUIRE_NO_ERRORS(result);
if (FFlag::DebugLuauDeferredConstraintResolution)
CHECK_EQ("{ m: (unknown) -> number }", toString(getMainModule()->returnType, ToStringOptions{true}));
else
CHECK_EQ("{| m: <a>(a) -> number |}", toString(getMainModule()->returnType, ToStringOptions{true}));
}
TEST_SUITE_END();

View file

@ -69,6 +69,18 @@ TEST_CASE_FIXTURE(Fixture, "infer_locals_with_nil_value")
CHECK_EQ(getPrimitiveType(ty), PrimitiveType::String);
}
TEST_CASE_FIXTURE(Fixture, "infer_locals_with_nil_value_2")
{
CheckResult result = check(R"(
local a = 2
local b = a,nil
)");
LUAU_REQUIRE_NO_ERRORS(result);
CHECK_EQ("number", toString(requireType("a")));
CHECK_EQ("number", toString(requireType("b")));
}
TEST_CASE_FIXTURE(Fixture, "infer_locals_via_assignment_from_its_call_site")
{
CheckResult result = check(R"(
@ -1168,6 +1180,28 @@ TEST_CASE_FIXTURE(Fixture, "bidirectional_checking_of_higher_order_function")
CHECK(location.end.line == 4);
}
TEST_CASE_FIXTURE(Fixture, "bidirectional_checking_of_callback_property")
{
CheckResult result = check(R"(
local print: (number) -> ()
type Point = {x: number, y: number}
local T : {callback: ((Point) -> ())?} = {}
T.callback = function(p) -- No error here
print(p.z) -- error here. Point has no property z
end
)");
LUAU_REQUIRE_ERROR_COUNT(1, result);
CHECK_MESSAGE(get<UnknownProperty>(result.errors[0]), "Expected UnknownProperty but got " << result.errors[0]);
Location location = result.errors[0].location;
CHECK(location.begin.line == 7);
CHECK(location.end.line == 7);
}
TEST_CASE_FIXTURE(BuiltinsFixture, "it_is_ok_to_have_inconsistent_number_of_return_values_in_nonstrict")
{
CheckResult result = check(R"(

View file

@ -373,12 +373,22 @@ TEST_CASE_FIXTURE(TryUnifyFixture, "metatables_unify_against_shape_of_free_table
state.log.commit();
REQUIRE_EQ(state.errors.size(), 1);
const std::string expected = R"(Type
// clang-format off
const std::string expected =
(FFlag::DebugLuauDeferredConstraintResolution) ?
R"(Type
'{ @metatable { __index: { foo: string } }, {| |} }'
could not be converted into
'{- foo: number -}'
caused by:
Type 'number' could not be converted into 'string')" :
R"(Type
'{ @metatable {| __index: {| foo: string |} |}, { } }'
could not be converted into
'{- foo: number -}'
caused by:
Type 'number' could not be converted into 'string')";
// clang-format on
CHECK_EQ(expected, toString(state.errors[0]));
}

View file

@ -308,11 +308,17 @@ local c: Packed<string, number, boolean>
tf = lookupType("Packed");
REQUIRE(tf);
CHECK_EQ(toString(*tf), "Packed<T, U...>");
if (FFlag::DebugLuauDeferredConstraintResolution)
CHECK_EQ(toString(*tf, {true}), "{ f: (T, U...) -> (T, U...) }");
else
CHECK_EQ(toString(*tf, {true}), "{| f: (T, U...) -> (T, U...) |}");
auto ttvA = get<TableType>(requireType("a"));
REQUIRE(ttvA);
CHECK_EQ(toString(requireType("a")), "Packed<number>");
if (FFlag::DebugLuauDeferredConstraintResolution)
CHECK_EQ(toString(requireType("a"), {true}), "{ f: (number) -> number }");
else
CHECK_EQ(toString(requireType("a"), {true}), "{| f: (number) -> number |}");
REQUIRE(ttvA->instantiatedTypeParams.size() == 1);
REQUIRE(ttvA->instantiatedTypePackParams.size() == 1);
@ -322,6 +328,9 @@ local c: Packed<string, number, boolean>
auto ttvB = get<TableType>(requireType("b"));
REQUIRE(ttvB);
CHECK_EQ(toString(requireType("b")), "Packed<string, number>");
if (FFlag::DebugLuauDeferredConstraintResolution)
CHECK_EQ(toString(requireType("b"), {true}), "{ f: (string, number) -> (string, number) }");
else
CHECK_EQ(toString(requireType("b"), {true}), "{| f: (string, number) -> (string, number) |}");
REQUIRE(ttvB->instantiatedTypeParams.size() == 1);
REQUIRE(ttvB->instantiatedTypePackParams.size() == 1);
@ -331,6 +340,9 @@ local c: Packed<string, number, boolean>
auto ttvC = get<TableType>(requireType("c"));
REQUIRE(ttvC);
CHECK_EQ(toString(requireType("c")), "Packed<string, number, boolean>");
if (FFlag::DebugLuauDeferredConstraintResolution)
CHECK_EQ(toString(requireType("c"), {true}), "{ f: (string, number, boolean) -> (string, number, boolean) }");
else
CHECK_EQ(toString(requireType("c"), {true}), "{| f: (string, number, boolean) -> (string, number, boolean) |}");
REQUIRE(ttvC->instantiatedTypeParams.size() == 1);
REQUIRE(ttvC->instantiatedTypePackParams.size() == 1);
@ -360,6 +372,18 @@ local d: { a: typeof(c) }
auto tf = lookupImportedType("Import", "Packed");
REQUIRE(tf);
CHECK_EQ(toString(*tf), "Packed<T, U...>");
if (FFlag::DebugLuauDeferredConstraintResolution)
{
CHECK_EQ(toString(*tf, {true}), "{ a: T, b: (U...) -> () }");
CHECK_EQ(toString(requireType("a"), {true}), "{ a: number, b: () -> () }");
CHECK_EQ(toString(requireType("b"), {true}), "{ a: string, b: (number) -> () }");
CHECK_EQ(toString(requireType("c"), {true}), "{ a: string, b: (number, boolean) -> () }");
CHECK_EQ(toString(requireType("d")), "{ a: Packed<string, number, boolean> }");
}
else
{
CHECK_EQ(toString(*tf, {true}), "{| a: T, b: (U...) -> () |}");
CHECK_EQ(toString(requireType("a"), {true}), "{| a: number, b: () -> () |}");
@ -367,6 +391,7 @@ local d: { a: typeof(c) }
CHECK_EQ(toString(requireType("c"), {true}), "{| a: string, b: (number, boolean) -> () |}");
CHECK_EQ(toString(requireType("d")), "{| a: Packed<string, number, boolean> |}");
}
}
TEST_CASE_FIXTURE(BuiltinsFixture, "type_pack_type_parameters")
{
@ -388,18 +413,30 @@ type C<X...> = Import.Packed<string, (number, X...)>
auto tf = lookupType("Alias");
REQUIRE(tf);
CHECK_EQ(toString(*tf), "Alias<S, T, R...>");
if (FFlag::DebugLuauDeferredConstraintResolution)
CHECK_EQ(toString(*tf, {true}), "{ a: S, b: (T, R...) -> () }");
else
CHECK_EQ(toString(*tf, {true}), "{| a: S, b: (T, R...) -> () |}");
if (FFlag::DebugLuauDeferredConstraintResolution)
CHECK_EQ(toString(requireType("a"), {true}), "{ a: string, b: (number, boolean) -> () }");
else
CHECK_EQ(toString(requireType("a"), {true}), "{| a: string, b: (number, boolean) -> () |}");
tf = lookupType("B");
REQUIRE(tf);
CHECK_EQ(toString(*tf), "B<X...>");
if (FFlag::DebugLuauDeferredConstraintResolution)
CHECK_EQ(toString(*tf, {true}), "{ a: string, b: (X...) -> () }");
else
CHECK_EQ(toString(*tf, {true}), "{| a: string, b: (X...) -> () |}");
tf = lookupType("C");
REQUIRE(tf);
CHECK_EQ(toString(*tf), "C<X...>");
if (FFlag::DebugLuauDeferredConstraintResolution)
CHECK_EQ(toString(*tf, {true}), "{ a: string, b: (number, X...) -> () }");
else
CHECK_EQ(toString(*tf, {true}), "{| a: string, b: (number, X...) -> () |}");
}
@ -867,6 +904,9 @@ type R = { m: F<R> }
LUAU_REQUIRE_NO_ERRORS(result);
if (FFlag::DebugLuauDeferredConstraintResolution)
CHECK_EQ(toString(*lookupType("R"), {true}), "t1 where t1 = { m: (t1) -> (t1) -> () }");
else
CHECK_EQ(toString(*lookupType("R"), {true}), "t1 where t1 = {| m: (t1) -> (t1) -> () |}");
}

View file

@ -356,6 +356,9 @@ a.x = 2
LUAU_REQUIRE_ERROR_COUNT(1, result);
auto s = toString(result.errors[0]);
if (FFlag::DebugLuauDeferredConstraintResolution)
CHECK_EQ("Value of type '({ x: number } & { y: number })?' could be nil", s);
else
CHECK_EQ("Value of type '({| x: number |} & {| y: number |})?' could be nil", s);
}
@ -471,11 +474,20 @@ local b: { w: number } = a
)");
LUAU_REQUIRE_ERROR_COUNT(1, result);
const std::string expected = R"(Type 'X | Y | Z' could not be converted into '{| w: number |}'
caused by:
Not all union options are compatible.
Table type 'X' not compatible with type '{| w: number |}' because the former is missing field 'w')";
CHECK_EQ(expected, toString(result.errors[0]));
TypeMismatch* tm = get<TypeMismatch>(result.errors[0]);
REQUIRE(tm);
CHECK_EQ(tm->reason, "Not all union options are compatible.");
CHECK_EQ("X | Y | Z", toString(tm->givenType));
const TableType* expected = get<TableType>(tm->wantedType);
REQUIRE(expected);
CHECK_EQ(TableState::Sealed, expected->state);
CHECK_EQ(1, expected->props.size());
auto propW = expected->props.find("w");
REQUIRE_NE(expected->props.end(), propW);
CHECK_EQ("number", toString(propW->second.type()));
}
TEST_CASE_FIXTURE(Fixture, "error_detailed_union_all")
@ -744,6 +756,9 @@ TEST_CASE_FIXTURE(Fixture, "less_greedy_unification_with_union_types_2")
LUAU_REQUIRE_NO_ERRORS(result);
if (FFlag::DebugLuauDeferredConstraintResolution)
CHECK_EQ("({ x: number } | { x: string }) -> number | string", toString(requireType("f")));
else
CHECK_EQ("({| x: number |} | {| x: string |}) -> number | string", toString(requireType("f")));
}

View file

@ -298,6 +298,9 @@ TEST_CASE_FIXTURE(Fixture, "substitution_skip_failure")
REQUIRE(!anyification.normalizationTooComplex);
REQUIRE(any.has_value());
if (FFlag::DebugLuauDeferredConstraintResolution)
CHECK_EQ("{ f: t1 } where t1 = () -> { f: () -> { f: ({ f: t1 }) -> (), signal: { f: (any) -> () } } }", toString(*any));
else
CHECK_EQ("{| f: t1 |} where t1 = () -> {| f: () -> {| f: ({| f: t1 |}) -> (), signal: {| f: (any) -> () |} |} |}", toString(*any));
}

View file

@ -38,6 +38,11 @@ struct Unifier2Fixture
{
return ::Luau::toString(ty, opts);
}
std::string toString(TypePackId ty)
{
return ::Luau::toString(ty, opts);
}
};
TEST_SUITE_BEGIN("Unifier2");
@ -99,6 +104,32 @@ TEST_CASE_FIXTURE(Unifier2Fixture, "(string) -> () <: (X) -> Y...")
CHECK(!yPack->tail);
}
TEST_CASE_FIXTURE(Unifier2Fixture, "unify_binds_free_subtype_tail_pack")
{
TypePackId numberPack = arena.addTypePack({builtinTypes.numberType});
TypePackId freeTail = arena.freshTypePack(&scope);
TypeId freeHead = arena.addType(FreeType{&scope, builtinTypes.neverType, builtinTypes.unknownType});
TypePackId freeAndFree = arena.addTypePack({freeHead}, freeTail);
u2.unify(freeAndFree, numberPack);
CHECK("('a <: number)" == toString(freeAndFree));
}
TEST_CASE_FIXTURE(Unifier2Fixture, "unify_binds_free_supertype_tail_pack")
{
TypePackId numberPack = arena.addTypePack({builtinTypes.numberType});
TypePackId freeTail = arena.freshTypePack(&scope);
TypeId freeHead = arena.addType(FreeType{&scope, builtinTypes.neverType, builtinTypes.unknownType});
TypePackId freeAndFree = arena.addTypePack({freeHead}, freeTail);
u2.unify(numberPack, freeAndFree);
CHECK("(number <: 'a)" == toString(freeAndFree));
}
TEST_CASE_FIXTURE(Unifier2Fixture, "generalize_a_type_that_is_bounded_by_another_generalizable_type")
{
auto [t1, ft1] = freshType();

View file

@ -18,6 +18,11 @@ assert(os.date(string.rep("%d", 1000), t) ==
assert(os.date(string.rep("%", 200)) == string.rep("%", 100))
assert(os.date("", -1) == nil)
assert(os.time({ year = 1969, month = 12, day = 31, hour = 23, min = 59, sec = 59}) == nil) -- just before start
assert(os.time({ year = 1970, month = 1, day = 1, hour = 0, min = 0, sec = 0}) == 0) -- start
assert(os.time({ year = 3000, month = 12, day = 31, hour = 23, min = 59, sec = 59}) == 32535215999) -- just before Windows max range
assert(os.time({ year = 1970, month = 1, day = 1, hour = 0, min = 0, sec = -1}) == nil) -- going before using time fields
local function checkDateTable (t)
local D = os.date("!*t", t)
assert(os.time(D) == t)

View file

@ -171,4 +171,38 @@ end
nilInvalidatesSlot()
local function arraySizeOpt1(a)
a[1] += 2
a[1] *= 3
table.insert(a, 3)
table.insert(a, 4)
table.insert(a, 5)
table.insert(a, 6)
a[1] += 4
a[1] *= 5
return a[1] + a[5]
end
assert(arraySizeOpt1({1}) == 71)
local function arraySizeOpt2(a, i)
a[i] += 2
a[i] *= 3
table.insert(a, 3)
table.insert(a, 4)
table.insert(a, 5)
table.insert(a, 6)
a[i] += 4
a[i] *= 5
return a[i] + a[5]
end
assert(arraySizeOpt1({1}, 1) == 71)
return('OK')

View file

@ -94,6 +94,8 @@ static int testAssertionHandler(const char* expr, const char* file, int line, co
return 1;
}
struct BoostLikeReporter : doctest::IReporter
{
const doctest::TestCaseData* currentTest = nullptr;
@ -255,6 +257,8 @@ int main(int argc, char** argv)
{
Luau::assertHandler() = testAssertionHandler;
doctest::registerReporter<BoostLikeReporter>("boost", 0, true);
doctest::Context context;

View file

@ -14,9 +14,12 @@ AutocompleteTest.type_correct_expected_argument_type_pack_suggestion
AutocompleteTest.type_correct_expected_argument_type_suggestion
AutocompleteTest.type_correct_expected_argument_type_suggestion_optional
AutocompleteTest.type_correct_expected_argument_type_suggestion_self
AutocompleteTest.type_correct_expected_return_type_pack_suggestion
AutocompleteTest.type_correct_expected_return_type_suggestion
AutocompleteTest.type_correct_function_no_parenthesis
AutocompleteTest.type_correct_function_return_types
AutocompleteTest.type_correct_keywords
AutocompleteTest.type_correct_suggestion_for_overloads
AutocompleteTest.type_correct_suggestion_in_argument
AutocompleteTest.unsealed_table_2
BuiltinTests.aliased_string_format
@ -55,8 +58,35 @@ BuiltinTests.table_dot_remove_optionally_returns_generic
BuiltinTests.table_freeze_is_generic
BuiltinTests.table_insert_correctly_infers_type_of_array_2_args_overload
BuiltinTests.table_insert_correctly_infers_type_of_array_3_args_overload
BuiltinTests.table_pack_variadic
BuiltinTests.trivial_select
BuiltinTests.xpcall
ControlFlowAnalysis.for_record_do_if_not_x_break
ControlFlowAnalysis.for_record_do_if_not_x_continue
ControlFlowAnalysis.if_not_x_break
ControlFlowAnalysis.if_not_x_break_elif_not_y_break
ControlFlowAnalysis.if_not_x_break_elif_not_y_continue
ControlFlowAnalysis.if_not_x_break_elif_not_y_fallthrough_elif_not_z_break
ControlFlowAnalysis.if_not_x_break_elif_rand_break_elif_not_y_break
ControlFlowAnalysis.if_not_x_break_elif_rand_break_elif_not_y_fallthrough
ControlFlowAnalysis.if_not_x_break_if_not_y_break
ControlFlowAnalysis.if_not_x_break_if_not_y_continue
ControlFlowAnalysis.if_not_x_continue
ControlFlowAnalysis.if_not_x_continue_elif_not_y_continue
ControlFlowAnalysis.if_not_x_continue_elif_not_y_fallthrough_elif_not_z_continue
ControlFlowAnalysis.if_not_x_continue_elif_not_y_throw_elif_not_z_fallthrough
ControlFlowAnalysis.if_not_x_continue_elif_rand_continue_elif_not_y_continue
ControlFlowAnalysis.if_not_x_continue_elif_rand_continue_elif_not_y_fallthrough
ControlFlowAnalysis.if_not_x_continue_if_not_y_continue
ControlFlowAnalysis.if_not_x_continue_if_not_y_throw
ControlFlowAnalysis.if_not_x_return_elif_not_y_break
ControlFlowAnalysis.if_not_x_return_elif_not_y_fallthrough_elif_not_z_break
ControlFlowAnalysis.prototyping_and_visiting_alias_has_the_same_scope_breaking
ControlFlowAnalysis.prototyping_and_visiting_alias_has_the_same_scope_continuing
ControlFlowAnalysis.tagged_unions_breaking
ControlFlowAnalysis.tagged_unions_continuing
ControlFlowAnalysis.type_alias_does_not_leak_out_breaking
ControlFlowAnalysis.type_alias_does_not_leak_out_continuing
DefinitionTests.class_definition_indexer
DefinitionTests.class_definition_overload_metamethods
DefinitionTests.class_definition_string_props
@ -81,7 +111,6 @@ GenericsTests.generic_argument_count_too_many
GenericsTests.generic_functions_dont_cache_type_parameters
GenericsTests.generic_functions_should_be_memory_safe
GenericsTests.generic_type_pack_parentheses
GenericsTests.generic_type_pack_unification2
GenericsTests.higher_rank_polymorphism_should_not_accept_instantiated_arguments
GenericsTests.hof_subtype_instantiation_regression
GenericsTests.infer_generic_function_function_argument
@ -90,9 +119,6 @@ GenericsTests.infer_generic_function_function_argument_3
GenericsTests.infer_generic_function_function_argument_overloaded
GenericsTests.infer_generic_lib_function_function_argument
GenericsTests.infer_generic_property
GenericsTests.instantiate_cyclic_generic_function
GenericsTests.instantiate_generic_function_in_assignments
GenericsTests.instantiate_generic_function_in_assignments2
GenericsTests.instantiated_function_argument_names
GenericsTests.mutable_state_polymorphism
GenericsTests.no_stack_overflow_from_quantifying
@ -135,7 +161,6 @@ RefinementTest.isa_type_refinement_must_be_known_ahead_of_time
RefinementTest.narrow_property_of_a_bounded_variable
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_2
RefinementTest.refine_a_property_of_some_global
RefinementTest.truthy_constraint_on_properties
RefinementTest.type_narrow_to_vector
@ -193,9 +218,9 @@ TableTests.shared_selfs
TableTests.shared_selfs_from_free_param
TableTests.shared_selfs_through_metatables
TableTests.table_call_metamethod_basic
TableTests.table_call_metamethod_generic
TableTests.table_param_width_subtyping_1
TableTests.table_param_width_subtyping_2
TableTests.table_param_width_subtyping_3
TableTests.table_simple_call
TableTests.table_subtyping_with_extra_props_dont_report_multiple_errors
TableTests.table_subtyping_with_missing_props_dont_report_multiple_errors
@ -213,9 +238,7 @@ ToString.named_metatable_toStringNamedFunction
ToString.pick_distinct_names_for_mixed_explicit_and_implicit_generics
ToString.toStringDetailed2
ToString.toStringErrorPack
ToString.toStringGenericPack
ToString.toStringNamedFunction_generic_pack
ToString.toStringNamedFunction_map
TryUnifyTests.members_of_failed_typepack_unification_are_unified_with_errorType
TryUnifyTests.result_of_failed_typepack_unification_is_constrained
TryUnifyTests.typepack_unification_should_trim_free_tails
@ -239,8 +262,7 @@ TypeFamilyTests.function_internal_families
TypeFamilyTests.internal_families_raise_errors
TypeFamilyTests.table_internal_families
TypeFamilyTests.unsolvable_family
TypeInfer.be_sure_to_use_active_txnlog_when_evaluating_a_variadic_overload
TypeInfer.bidirectional_checking_of_higher_order_function
TypeInfer.bidirectional_checking_of_callback_property
TypeInfer.check_expr_recursion_limit
TypeInfer.check_type_infer_recursion_count
TypeInfer.cli_39932_use_unifier_in_ensure_methods
@ -249,14 +271,12 @@ TypeInfer.dont_report_type_errors_within_an_AstExprError
TypeInfer.dont_report_type_errors_within_an_AstStatError
TypeInfer.fuzz_free_table_type_change_during_index_check
TypeInfer.infer_assignment_value_types_mutable_lval
TypeInfer.infer_locals_via_assignment_from_its_call_site
TypeInfer.interesting_local_type_inference_case
TypeInfer.no_stack_overflow_from_isoptional
TypeInfer.promote_tail_type_packs
TypeInfer.recursive_function_that_invokes_itself_with_a_refinement_of_its_parameter
TypeInfer.recursive_function_that_invokes_itself_with_a_refinement_of_its_parameter_2
TypeInfer.tc_after_error_recovery_no_replacement_name_in_error
TypeInfer.type_infer_cache_limit_normalizer
TypeInfer.tc_if_else_expressions_expected_type_3
TypeInfer.type_infer_recursion_limit_no_ice
TypeInfer.type_infer_recursion_limit_normalizer
TypeInferAnyError.can_subscript_any
@ -274,7 +294,6 @@ TypeInferClasses.class_type_mismatch_with_name_conflict
TypeInferClasses.detailed_class_unification_error
TypeInferClasses.index_instance_property
TypeInferClasses.table_class_unification_reports_sane_errors_for_missing_properties
TypeInferClasses.table_indexers_are_invariant
TypeInferFunctions.apply_of_lambda_with_inferred_and_explicit_types
TypeInferFunctions.cannot_hoist_interior_defns_into_signature
TypeInferFunctions.dont_assert_when_the_tarjan_limit_is_exceeded_during_generalization
@ -295,7 +314,6 @@ TypeInferFunctions.infer_anonymous_function_arguments
TypeInferFunctions.infer_generic_function_function_argument
TypeInferFunctions.infer_generic_function_function_argument_overloaded
TypeInferFunctions.infer_generic_lib_function_function_argument
TypeInferFunctions.infer_higher_order_function
TypeInferFunctions.infer_return_type_from_selected_overload
TypeInferFunctions.infer_that_function_does_not_return_a_table
TypeInferFunctions.it_is_ok_to_oversaturate_a_higher_order_function_argument
@ -336,6 +354,7 @@ TypeInferLoops.properly_infer_iteratee_is_a_free_table
TypeInferLoops.unreachable_code_after_infinite_loop
TypeInferLoops.varlist_declared_by_for_in_loop_should_be_free
TypeInferModules.bound_free_table_export_is_ok
TypeInferModules.do_not_modify_imported_types_4
TypeInferModules.do_not_modify_imported_types_5
TypeInferModules.module_type_conflict
TypeInferModules.module_type_conflict_instantiated
@ -358,6 +377,7 @@ TypeInferOperators.concat_op_on_string_lhs_and_free_rhs
TypeInferOperators.disallow_string_and_types_without_metatables_from_arithmetic_binary_ops
TypeInferOperators.luau_polyfill_is_array
TypeInferOperators.operator_eq_completely_incompatible
TypeInferOperators.reducing_and
TypeInferOperators.strict_binary_op_where_lhs_unknown
TypeInferOperators.typecheck_overloaded_multiply_that_is_an_intersection
TypeInferOperators.typecheck_overloaded_multiply_that_is_an_intersection_on_rhs
@ -370,7 +390,7 @@ TypeInferPrimitives.CheckMethodsOfNumber
TypeInferPrimitives.string_index
TypeInferUnknownNever.length_of_never
TypeInferUnknownNever.math_operators_and_never
TypePackTests.higher_order_function
TypePackTests.fuzz_typepack_iter_follow_2
TypePackTests.pack_tail_unification_check
TypePackTests.type_alias_backwards_compatible
TypePackTests.type_alias_default_type_errors
@ -378,6 +398,7 @@ TypePackTests.type_packs_with_tails_in_vararg_adjustment
TypeSingletons.function_args_infer_singletons
TypeSingletons.function_call_with_singletons
TypeSingletons.function_call_with_singletons_mismatch
TypeSingletons.overloaded_function_call_with_singletons
TypeSingletons.return_type_of_f_is_not_widened
TypeSingletons.table_properties_type_error_escapes
TypeSingletons.widen_the_supertype_if_it_is_free_and_subtype_has_singleton
@ -385,3 +406,4 @@ TypeSingletons.widening_happens_almost_everywhere
UnionTypes.index_on_a_union_type_with_missing_property
UnionTypes.less_greedy_unification_with_union_types
UnionTypes.table_union_write_indirect
UnionTypes.unify_unsealed_table_union_check