From 3bfc8642802c89ba6227628463f1b82fc4f13363 Mon Sep 17 00:00:00 2001 From: Alexander McCord Date: Fri, 29 Sep 2023 17:22:06 -0700 Subject: [PATCH] Sync to upstream/release/597 --- .../include/Luau/ConstraintGraphBuilder.h | 23 +- Analysis/include/Luau/ControlFlow.h | 4 +- Analysis/include/Luau/NonStrictTypeChecker.h | 15 + Analysis/src/Clone.cpp | 28 +- Analysis/src/ConstraintGraphBuilder.cpp | 152 ++-- Analysis/src/ConstraintSolver.cpp | 38 + Analysis/src/Frontend.cpp | 3 +- Analysis/src/NonStrictTypeChecker.cpp | 87 ++ Analysis/src/Normalize.cpp | 1 - Analysis/src/ToString.cpp | 29 +- Analysis/src/TypeInfer.cpp | 18 +- Analysis/src/Unifier2.cpp | 51 +- CodeGen/src/CodeGenUtils.cpp | 13 +- CodeGen/src/OptimizeConstProp.cpp | 137 +++- Sources.cmake | 3 + VM/src/loslib.cpp | 21 - extern/doctest.h | 6 + tests/Autocomplete.test.cpp | 5 +- tests/CodeAllocator.test.cpp | 12 + tests/ConstraintGraphBuilderFixture.cpp | 2 +- tests/Differ.test.cpp | 32 +- tests/Frontend.test.cpp | 46 +- tests/IrBuilder.test.cpp | 238 ++++++ tests/Module.test.cpp | 9 +- tests/NonStrictTypeChecker.test.cpp | 17 + tests/Normalize.test.cpp | 10 +- tests/Simplify.test.cpp | 4 +- tests/ToString.test.cpp | 95 ++- tests/TypeInfer.aliases.test.cpp | 12 +- tests/TypeInfer.builtins.test.cpp | 20 +- tests/TypeInfer.cfa.test.cpp | 764 ++++++++++++++++++ tests/TypeInfer.functions.test.cpp | 2 +- tests/TypeInfer.intersectionTypes.test.cpp | 20 +- tests/TypeInfer.modules.test.cpp | 5 +- tests/TypeInfer.oop.test.cpp | 2 +- tests/TypeInfer.operators.test.cpp | 2 + tests/TypeInfer.provisional.test.cpp | 18 +- tests/TypeInfer.refinements.test.cpp | 28 +- tests/TypeInfer.tables.test.cpp | 74 +- tests/TypeInfer.test.cpp | 34 + tests/TypeInfer.tryUnify.test.cpp | 12 +- tests/TypeInfer.typePacks.cpp | 68 +- tests/TypeInfer.unionTypes.test.cpp | 29 +- tests/TypeVar.test.cpp | 5 +- tests/Unifier2.test.cpp | 31 + tests/conformance/datetime.lua | 5 + tests/conformance/native.lua | 34 + tests/main.cpp | 4 + tools/faillist.txt | 54 +- 49 files changed, 2006 insertions(+), 316 deletions(-) create mode 100644 Analysis/include/Luau/NonStrictTypeChecker.h create mode 100644 Analysis/src/NonStrictTypeChecker.cpp create mode 100644 tests/NonStrictTypeChecker.test.cpp diff --git a/Analysis/include/Luau/ConstraintGraphBuilder.h b/Analysis/include/Luau/ConstraintGraphBuilder.h index 902da0d5..c484b686 100644 --- a/Analysis/include/Luau/ConstraintGraphBuilder.h +++ b/Analysis/include/Luau/ConstraintGraphBuilder.h @@ -106,6 +106,14 @@ struct ConstraintGraphBuilder std::function prepareModuleScope, DcrLogger* logger, NotNull dfg, std::vector 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 exprs, const std::vector>& expectedTypes = {}); - InferencePack checkPack(const ScopePtr& scope, AstExpr* expr, const std::vector>& expectedTypes = {}); + InferencePack checkPack( + const ScopePtr& scope, AstExpr* expr, const std::vector>& 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 expectedType = {}, - bool forceSingleton = false); + bool forceSingleton = false, bool generalize = true); Inference check(const ScopePtr& scope, AstExprConstantString* string, std::optional expectedType, bool forceSingleton); Inference check(const ScopePtr& scope, AstExprConstantBool* bool_, std::optional 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 expectedType); + Inference check(const ScopePtr& scope, AstExprFunction* func, std::optional expectedType, bool generalize); Inference check(const ScopePtr& scope, AstExprUnary* unary); Inference check(const ScopePtr& scope, AstExprBinary* binary, std::optional expectedType); Inference check(const ScopePtr& scope, AstExprIfElse* ifElse, std::optional expectedType); diff --git a/Analysis/include/Luau/ControlFlow.h b/Analysis/include/Luau/ControlFlow.h index 566d77bd..82c0403c 100644 --- a/Analysis/include/Luau/ControlFlow.h +++ b/Analysis/include/Luau/ControlFlow.h @@ -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) diff --git a/Analysis/include/Luau/NonStrictTypeChecker.h b/Analysis/include/Luau/NonStrictTypeChecker.h new file mode 100644 index 00000000..6f12998e --- /dev/null +++ b/Analysis/include/Luau/NonStrictTypeChecker.h @@ -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, Module* module); + +} // namespace Luau diff --git a/Analysis/src/Clone.cpp b/Analysis/src/Clone.cpp index c28e1678..4d6bcf03 100644 --- a/Analysis/src/Clone.cpp +++ b/Analysis/src/Clone.cpp @@ -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& 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& 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); diff --git a/Analysis/src/ConstraintGraphBuilder.cpp b/Analysis/src/ConstraintGraphBuilder.cpp index ae143ca5..c0022246 100644 --- a/Analysis/src/ConstraintGraphBuilder.cpp +++ b/Analysis/src/ConstraintGraphBuilder.cpp @@ -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(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(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()) return visit(scope, s); - else if (stat->is() || stat->is()) - { - // Nothing - return ControlFlow::None; // TODO: ControlFlow::Break/Continue - } + else if (stat->is()) + return FFlag::LuauLoopControlFlowAnalysis ? ControlFlow::Breaks : ControlFlow::None; + else if (stat->is()) + return FFlag::LuauLoopControlFlowAnalysis ? ControlFlow::Continues : ControlFlow::None; else if (auto r = stat->as()) return visit(scope, r); else if (auto e = stat->as()) @@ -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>& expectedTypes) +InferencePack ConstraintGraphBuilder::checkPack( + const ScopePtr& scope, AstExpr* expr, const std::vector>& expectedTypes, bool generalize) { RecursionCounter counter{&recursionCount}; @@ -1404,7 +1407,7 @@ InferencePack ConstraintGraphBuilder::checkPack(const ScopePtr& scope, AstExpr* std::optional 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> 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 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() || lastArg->is()); - - 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 args; std::optional argTail; std::vector argumentRefinements; - Checkpoint argCheckpoint = checkpoint(this); - for (size_t i = 0; i < exprArgs.size(); ++i) { AstExpr* arg = exprArgs[i]; - std::optional 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() || arg->is())) { - 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 expectedType, bool forceSingleton) + const ScopePtr& scope, AstExpr* expr, ValueContext context, std::optional expectedType, bool forceSingleton, bool generalize) { RecursionCounter counter{&recursionCount}; @@ -1647,7 +1619,7 @@ Inference ConstraintGraphBuilder::check( else if (auto call = expr->as()) result = flattenPack(scope, expr->location, checkPack(scope, call)); // TODO: needs predicates too else if (auto a = expr->as()) - result = check(scope, a, expectedType); + result = check(scope, a, expectedType, generalize); else if (auto indexName = expr->as()) result = check(scope, indexName); else if (auto indexExpr = expr->as()) @@ -1815,30 +1787,37 @@ Inference ConstraintGraphBuilder::check(const ScopePtr& scope, AstExprIndexExpr* return Inference{result}; } -Inference ConstraintGraphBuilder::check(const ScopePtr& scope, AstExprFunction* func, std::optional expectedType) +Inference ConstraintGraphBuilder::check(const ScopePtr& scope, AstExprFunction* func, std::optional expectedType, bool generalize) { Checkpoint startCheckpoint = checkpoint(this); FunctionSignature sig = checkFunctionSignature(scope, func, expectedType); checkFunctionBody(sig.bodyScope, func); Checkpoint endCheckpoint = checkpoint(this); - TypeId generalizedTy = arena->addType(BlockedType{}); - NotNull gc = addConstraint(sig.signatureScope, func->location, GeneralizationConstraint{generalizedTy, sig.signature}); + if (generalize) + { + TypeId generalizedTy = arena->addType(BlockedType{}); + NotNull gc = addConstraint(sig.signatureScope, func->location, GeneralizationConstraint{generalizedTy, sig.signature}); - Constraint* previous = nullptr; - forEachConstraint(startCheckpoint, endCheckpoint, this, [gc, &previous](const ConstraintPtr& constraint) { - gc->dependencies.emplace_back(constraint.get()); + Constraint* previous = nullptr; + forEachConstraint(startCheckpoint, endCheckpoint, this, [gc, &previous](const ConstraintPtr& constraint) { + gc->dependencies.emplace_back(constraint.get()); - if (auto psc = get(*constraint); psc && psc->returns) - { - if (previous) - constraint->dependencies.push_back(NotNull{previous}); + if (auto psc = get(*constraint); psc && psc->returns) + { + if (previous) + constraint->dependencies.push_back(NotNull{previous}); - previous = constraint.get(); - } - }); + previous = constraint.get(); + } + }); - return Inference{generalizedTy}; + 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); diff --git a/Analysis/src/ConstraintSolver.cpp b/Analysis/src/ConstraintSolver.cpp index e93f45b2..c1544bd7 100644 --- a/Analysis/src/ConstraintSolver.cpp +++ b/Analysis/src/ConstraintSolver.cpp @@ -1380,6 +1380,44 @@ bool ConstraintSolver::tryDispatch(const FunctionCallConstraint& c, NotNullanyType}; } + // 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(fn)) + { + const std::vector expectedArgs = flatten(ftv->argTypes).first; + const std::vector 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(follow(expectedArgs[i])); + const FunctionType* lambdaTy = get(follow(argPackHead[i])); + const AstExprFunction* lambdaExpr = c.callSite->args.data[i]->as(); + + if (expectedLambdaTy && lambdaTy && lambdaExpr) + { + const std::vector expectedLambdaArgTys = flatten(expectedLambdaTy->argTypes).first; + const std::vector 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(follow(lambdaArgTys[j]))) + { + asMutable(lambdaArgTys[j])->ty.emplace(expectedLambdaArgTys[j]); + } + } + } + } + } + TypeId inferredTy = arena->addType(FunctionType{TypeLevel{}, constraint->scope.get(), argsPack, c.result}); Unifier2 u2{NotNull{arena}, builtinTypes, constraint->scope, NotNull{&iceReporter}}; diff --git a/Analysis/src/Frontend.cpp b/Analysis/src/Frontend.cpp index 677458ab..4872843e 100644 --- a/Analysis/src/Frontend.cpp +++ b/Analysis/src/Frontend.cpp @@ -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::vectorerrors = std::move(cgb.errors); ConstraintSolver cs{NotNull{&normalizer}, NotNull(cgb.rootScope), borrowConstraints(cgb.constraints), result->humanReadableName, moduleResolver, diff --git a/Analysis/src/NonStrictTypeChecker.cpp b/Analysis/src/NonStrictTypeChecker.cpp new file mode 100644 index 00000000..4ec3ade8 --- /dev/null +++ b/Analysis/src/NonStrictTypeChecker.cpp @@ -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 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& defs) + { + // TODO: unimplemented + } + + std::optional 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; + const NotNull ice; + TypeArena arena; + Module* module; + Normalizer normalizer; + Subtyping subtyping; + + + NonStrictTypeChecker(NotNull builtinTypes, Subtyping subtyping, const NotNull ice, + NotNull 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, Module* module) +{ + // TODO: unimplemented +} +} // namespace Luau diff --git a/Analysis/src/Normalize.cpp b/Analysis/src/Normalize.cpp index d22e593b..4f9532c0 100644 --- a/Analysis/src/Normalize.cpp +++ b/Analysis/src/Normalize.cpp @@ -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. diff --git a/Analysis/src/ToString.cpp b/Analysis/src/ToString.cpp index 2950ecd0..10fe440e 100644 --- a/Analysis/src/ToString.cpp +++ b/Analysis/src/ToString.cpp @@ -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: - state.result.invalid = true; - openbrace = "{|"; - closedbrace = "|}"; + if (FFlag::DebugLuauDeferredConstraintResolution) + { + openbrace = "{"; + closedbrace = "}"; + } + else + { + state.result.invalid = true; + openbrace = "{|"; + closedbrace = "|}"; + } break; case TableState::Unsealed: - openbrace = "{"; - closedbrace = "}"; + if (FFlag::DebugLuauDeferredConstraintResolution) + { + state.result.invalid = true; + openbrace = "{|"; + closedbrace = "|}"; + } + else + { + openbrace = "{"; + closedbrace = "}"; + } break; case TableState::Free: state.result.invalid = true; diff --git a/Analysis/src/TypeInfer.cpp b/Analysis/src/TypeInfer.cpp index 61c90ba8..a29b1e06 100644 --- a/Analysis/src/TypeInfer.cpp +++ b/Analysis/src/TypeInfer.cpp @@ -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()) return check(scope, *repeat); - else if (program.is() || program.is()) - { - // Nothing to do - return ControlFlow::None; - } + else if (program.is()) + return FFlag::LuauLoopControlFlowAnalysis ? ControlFlow::Breaks : ControlFlow::None; + else if (program.is()) + return FFlag::LuauLoopControlFlowAnalysis ? ControlFlow::Continues : ControlFlow::None; else if (auto return_ = program.as()) return check(scope, *return_); else if (auto expr = program.as()) @@ -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; diff --git a/Analysis/src/Unifier2.cpp b/Analysis/src/Unifier2.cpp index 83352bd3..e9e13c2a 100644 --- a/Analysis/src/Unifier2.cpp +++ b/Analysis/src/Unifier2.cpp @@ -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); - TypePackId followedSubTail = follow(*subTail); - TypePackId followedSuperTail = follow(*superTail); - - if (get(followedSubTail) || get(followedSuperTail)) - return unify(followedSubTail, followedSuperTail); + if (get(followedSubTail) || get(followedSuperTail)) + return unify(followedSubTail, followedSuperTail); + } + else if (subTail) + { + TypePackId followedSubTail = follow(*subTail); + if (get(followedSubTail)) + asMutable(followedSubTail)->ty.emplace(builtinTypes->emptyTypePack); + } + else if (superTail) + { + TypePackId followedSuperTail = follow(*superTail); + if (get(followedSuperTail)) + asMutable(followedSuperTail)->ty.emplace(builtinTypes->emptyTypePack); + } return true; } @@ -582,13 +594,6 @@ struct MutatingGeneralizer : TypeOnceVisitor TableType* tt = getMutable(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 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(ty); ft && (!ft->generics.empty() || !ft->genericPacks.empty())) @@ -627,16 +629,23 @@ std::optional Unifier2::generalize(TypeId ty) gen.traverse(ty); - std::optional 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(follow(*res)); + if (ty->owningArena != arena || ty->persistent) + return ty; + + FunctionType* ftv = getMutable(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) diff --git a/CodeGen/src/CodeGenUtils.cpp b/CodeGen/src/CodeGenUtils.cpp index 40a96845..c7f24c80 100644 --- a/CodeGen/src/CodeGenUtils.cpp +++ b/CodeGen/src/CodeGenUtils.cpp @@ -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++ diff --git a/CodeGen/src/OptimizeConstProp.cpp b/CodeGen/src/OptimizeConstProp.cpp index de839061..9e55eb36 100644 --- a/CodeGen/src/OptimizeConstProp.cpp +++ b/CodeGen/src/OptimizeConstProp.cpp @@ -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 valueMap; - std::vector getSlotNodeCache; - std::vector checkSlotMatchCache; + // Some instruction re-uses can't be stored in valueMap because of extra requirements + std::vector tryNumToIndexCache; // Fallback block argument might be different + + // Heap changes might affect table state + std::vector getSlotNodeCache; // Additionally, pcpos argument might be different + std::vector checkSlotMatchCache; // Additionally, fallback block argument might be different + + std::vector getArrAddrCache; + std::vector 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& 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 diff --git a/Sources.cmake b/Sources.cmake index fdc27f9c..3567b8be 100644 --- a/Sources.cmake +++ b/Sources.cmake @@ -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 diff --git a/VM/src/loslib.cpp b/VM/src/loslib.cpp index 5e69bae2..b5063504 100644 --- a/VM/src/loslib.cpp +++ b/VM/src/loslib.cpp @@ -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) diff --git a/extern/doctest.h b/extern/doctest.h index aa2724c7..42d93c7f 100644 --- a/extern/doctest.h +++ b/extern/doctest.h @@ -3139,7 +3139,11 @@ DOCTEST_MAKE_STD_HEADERS_CLEAN_FROM_WARNINGS_ON_WALL_BEGIN #include #include #include + +#if !defined(DOCTEST_CONFIG_NO_POSIX_SIGNALS) #include +#endif + #include #include #include @@ -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 diff --git a/tests/Autocomplete.test.cpp b/tests/Autocomplete.test.cpp index 84dee432..f6f21384 100644 --- a/tests/Autocomplete.test.cpp +++ b/tests/Autocomplete.test.cpp @@ -2162,7 +2162,10 @@ local fp: @1= f auto ac = autocomplete('1'); - REQUIRE_EQ("({| x: number, y: number |}) -> number", toString(requireType("f"))); + 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")); } diff --git a/tests/CodeAllocator.test.cpp b/tests/CodeAllocator.test.cpp index 3e9219c9..1ea35201 100644 --- a/tests/CodeAllocator.test.cpp +++ b/tests/CodeAllocator.test.cpp @@ -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); diff --git a/tests/ConstraintGraphBuilderFixture.cpp b/tests/ConstraintGraphBuilderFixture.cpp index 88477b9b..293c26ff 100644 --- a/tests/ConstraintGraphBuilderFixture.cpp +++ b/tests/ConstraintGraphBuilderFixture.cpp @@ -21,7 +21,7 @@ void ConstraintGraphBuilderFixture::generateConstraints(const std::string& code) dfg = std::make_unique(DataFlowGraphBuilder::build(root, NotNull{&ice})); cgb = std::make_unique(mainModule, NotNull{&normalizer}, NotNull(&moduleResolver), builtinTypes, NotNull(&ice), frontend.globals.globalScope, /*prepareModuleScope*/ nullptr, &logger, NotNull{dfg.get()}, std::vector()); - cgb->visit(root); + cgb->visitModuleRoot(root); rootScope = cgb->rootScope; constraints = Luau::borrowConstraints(cgb->constraints); } diff --git a/tests/Differ.test.cpp b/tests/Differ.test.cpp index 84a383c1..287f44c8 100644 --- a/tests/Differ.test.cpp +++ b/tests/Differ.test.cpp @@ -657,8 +657,12 @@ TEST_CASE_FIXTURE(DifferFixture, "function_table_self_referential_cyclic") )"); LUAU_REQUIRE_NO_ERRORS(result); - compareTypesNe("foo", "almostFoo", - R"(DiffError: these two types are not equal because the left type at .Ret[1].bar.Ret[1] has type t1 where t1 = {| bar: () -> t1 |}, while the right type at .Ret[1].bar.Ret[1] has type t1 where t1 = () -> t1)"); + if (FFlag::DebugLuauDeferredConstraintResolution) + compareTypesNe("foo", "almostFoo", + R"(DiffError: these two types are not equal because the left type at .Ret[1].bar.Ret[1] has type t1 where t1 = { bar: () -> t1 }, while the right type at .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 .Ret[1].bar.Ret[1] has type t1 where t1 = {| bar: () -> t1 |}, while the right type at .Ret[1].bar.Ret[1] has type t1 where t1 = () -> t1)"); } TEST_CASE_FIXTURE(DifferFixture, "equal_union_cyclic") @@ -812,8 +816,12 @@ TEST_CASE_FIXTURE(DifferFixture, "union_missing") )"); LUAU_REQUIRE_NO_ERRORS(result); - compareTypesNe("foo", "almostFoo", - R"(DiffError: these two types are not equal because the left type at is a union containing type {| baz: boolean, rot: "singleton" |}, while the right type at is a union missing type {| baz: boolean, rot: "singleton" |})"); + if (FFlag::DebugLuauDeferredConstraintResolution) + compareTypesNe("foo", "almostFoo", + R"(DiffError: these two types are not equal because the left type at is a union containing type { baz: boolean, rot: "singleton" }, while the right type at 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 is a union containing type {| baz: boolean, rot: "singleton" |}, while the right type at is a union missing type {| baz: boolean, rot: "singleton" |})"); } TEST_CASE_FIXTURE(DifferFixture, "intersection_missing_right") @@ -848,8 +856,12 @@ TEST_CASE_FIXTURE(DifferFixture, "intersection_tables_missing_right") )"); LUAU_REQUIRE_NO_ERRORS(result); - compareTypesNe("foo", "almostFoo", - R"(DiffError: these two types are not equal because the left type at is an intersection containing type {| x: number |}, while the right type at is an intersection missing type {| x: number |})"); + if (FFlag::DebugLuauDeferredConstraintResolution) + compareTypesNe("foo", "almostFoo", + R"(DiffError: these two types are not equal because the left type at is an intersection containing type { x: number }, while the right type at is an intersection missing type { x: number })"); + else + compareTypesNe("foo", "almostFoo", + R"(DiffError: these two types are not equal because the left type at is an intersection containing type {| x: number |}, while the right type at is an intersection missing type {| x: number |})"); } TEST_CASE_FIXTURE(DifferFixture, "intersection_tables_missing_left") @@ -860,8 +872,12 @@ TEST_CASE_FIXTURE(DifferFixture, "intersection_tables_missing_left") )"); LUAU_REQUIRE_NO_ERRORS(result); - compareTypesNe("foo", "almostFoo", - R"(DiffError: these two types are not equal because the left type at is an intersection missing type {| z: boolean |}, while the right type at is an intersection containing type {| z: boolean |})"); + if (FFlag::DebugLuauDeferredConstraintResolution) + compareTypesNe("foo", "almostFoo", + R"(DiffError: these two types are not equal because the left type at is an intersection missing type { z: boolean }, while the right type at is an intersection containing type { z: boolean })"); + else + compareTypesNe("foo", "almostFoo", + R"(DiffError: these two types are not equal because the left type at is an intersection missing type {| z: boolean |}, while the right type at is an intersection containing type {| z: boolean |})"); } TEST_CASE_FIXTURE(DifferFixture, "equal_function") diff --git a/tests/Frontend.test.cpp b/tests/Frontend.test.cpp index 96032fd1..61333422 100644 --- a/tests/Frontend.test.cpp +++ b/tests/Frontend.test.cpp @@ -12,6 +12,8 @@ using namespace Luau; +LUAU_FASTFLAG(DebugLuauDeferredConstraintResolution) + namespace { @@ -160,7 +162,10 @@ TEST_CASE_FIXTURE(FrontendFixture, "automatically_check_dependent_scripts") auto bExports = first(bModule->returnType); REQUIRE(!!bExports); - CHECK_EQ("{| b_value: number |}", toString(*bExports)); + if (FFlag::DebugLuauDeferredConstraintResolution) + CHECK_EQ("{ b_value: number }", toString(*bExports)); + else + CHECK_EQ("{| b_value: number |}", toString(*bExports)); } TEST_CASE_FIXTURE(FrontendFixture, "automatically_check_cyclically_dependent_scripts") @@ -302,7 +307,11 @@ TEST_CASE_FIXTURE(FrontendFixture, "nocheck_cycle_used_by_checked") std::optional cExports = first(cModule->returnType); REQUIRE(bool(cExports)); - CHECK_EQ("{| a: any, b: any |}", toString(*cExports)); + + if (FFlag::DebugLuauDeferredConstraintResolution) + CHECK_EQ("{ a: any, b: any }", toString(*cExports)); + else + CHECK_EQ("{| a: any, b: any |}", toString(*cExports)); } TEST_CASE_FIXTURE(FrontendFixture, "cycle_detection_disabled_in_nocheck") @@ -473,20 +482,32 @@ return {mod_b = 2} LUAU_REQUIRE_ERRORS(resultB); TypeId tyB = requireExportedType("game/B", "btype"); - CHECK_EQ(toString(tyB, opts), "{| x: number |}"); + if (FFlag::DebugLuauDeferredConstraintResolution) + CHECK_EQ(toString(tyB, opts), "{ x: number }"); + else + CHECK_EQ(toString(tyB, opts), "{| x: number |}"); TypeId tyA = requireExportedType("game/A", "atype"); - CHECK_EQ(toString(tyA, opts), "{| x: any |}"); + if (FFlag::DebugLuauDeferredConstraintResolution) + CHECK_EQ(toString(tyA, opts), "{ x: any }"); + else + CHECK_EQ(toString(tyA, opts), "{| x: any |}"); frontend.markDirty("game/B"); resultB = frontend.check("game/B"); LUAU_REQUIRE_ERRORS(resultB); tyB = requireExportedType("game/B", "btype"); - CHECK_EQ(toString(tyB, opts), "{| x: number |}"); + if (FFlag::DebugLuauDeferredConstraintResolution) + CHECK_EQ(toString(tyB, opts), "{ x: number }"); + else + CHECK_EQ(toString(tyB, opts), "{| x: number |}"); tyA = requireExportedType("game/A", "atype"); - CHECK_EQ(toString(tyA, opts), "{| x: any |}"); + if (FFlag::DebugLuauDeferredConstraintResolution) + CHECK_EQ(toString(tyA, opts), "{ x: any }"); + else + CHECK_EQ(toString(tyA, opts), "{| x: any |}"); } TEST_CASE_FIXTURE(FrontendFixture, "dont_reparse_clean_file_when_linting") @@ -559,7 +580,10 @@ TEST_CASE_FIXTURE(FrontendFixture, "recheck_if_dependent_script_is_dirty") auto bExports = first(bModule->returnType); REQUIRE(!!bExports); - CHECK_EQ("{| b_value: string |}", toString(*bExports)); + if (FFlag::DebugLuauDeferredConstraintResolution) + CHECK_EQ("{ b_value: string }", toString(*bExports)); + else + CHECK_EQ("{| b_value: string |}", toString(*bExports)); } TEST_CASE_FIXTURE(FrontendFixture, "mark_non_immediate_reverse_deps_as_dirty") @@ -883,8 +907,12 @@ 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 - REQUIRE_EQ( - "Table type 'a' not compatible with type '{| Count: number |}' because the former is missing field 'Count'", toString(result.errors[0])); + 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])); } TEST_CASE_FIXTURE(FrontendFixture, "trace_requires_in_nonstrict_mode") diff --git a/tests/IrBuilder.test.cpp b/tests/IrBuilder.test.cpp index b511156d..c70f6933 100644 --- a/tests/IrBuilder.test.cpp +++ b/tests/IrBuilder.test.cpp @@ -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"); diff --git a/tests/Module.test.cpp b/tests/Module.test.cpp index 4f489065..208db5d8 100644 --- a/tests/Module.test.cpp +++ b/tests/Module.test.cpp @@ -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 typeA = first(modA->returnType); - std::optional typeB = first(modB->returnType); REQUIRE(typeA); + std::optional typeB = first(modB->returnType); REQUIRE(typeB); + TableType* tableA = getMutable(*typeA); + REQUIRE_MESSAGE(tableA, "Expected a table, but got " << toString(*typeA)); TableType* tableB = getMutable(*typeB); + REQUIRE_MESSAGE(tableB, "Expected a table, but got " << toString(*typeB)); + CHECK(tableA->props["a"].type() == tableB->props["b"].type()); } diff --git a/tests/NonStrictTypeChecker.test.cpp b/tests/NonStrictTypeChecker.test.cpp new file mode 100644 index 00000000..9021a9af --- /dev/null +++ b/tests/NonStrictTypeChecker.test.cpp @@ -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(); diff --git a/tests/Normalize.test.cpp b/tests/Normalize.test.cpp index 8bbbaa39..ee818022 100644 --- a/tests/Normalize.test.cpp +++ b/tests/Normalize.test.cpp @@ -797,7 +797,10 @@ TEST_CASE_FIXTURE(NormalizeFixture, "narrow_union_of_classes_with_intersection") TEST_CASE_FIXTURE(NormalizeFixture, "intersection_of_metatables_where_the_metatable_is_top_or_bottom") { - CHECK("{ @metatable *error-type*, {| |} }" == toString(normal("Mt<{}, any> & Mt<{}, err>"))); + if (FFlag::DebugLuauDeferredConstraintResolution) + CHECK("{ @metatable *error-type*, { } }" == toString(normal("Mt<{}, any> & Mt<{}, err>"))); + else + CHECK("{ @metatable *error-type*, {| |} }" == toString(normal("Mt<{}, any> & Mt<{}, err>"))); } TEST_CASE_FIXTURE(NormalizeFixture, "recurring_intersection") @@ -863,7 +866,10 @@ TEST_CASE_FIXTURE(NormalizeFixture, "classes_and_never") TEST_CASE_FIXTURE(NormalizeFixture, "top_table_type") { CHECK("table" == toString(normal("{} | tbl"))); - CHECK("{| |}" == toString(normal("{} & tbl"))); + if (FFlag::DebugLuauDeferredConstraintResolution) + CHECK("{ }" == toString(normal("{} & tbl"))); + else + CHECK("{| |}" == toString(normal("{} & tbl"))); CHECK("never" == toString(normal("number & tbl"))); } diff --git a/tests/Simplify.test.cpp b/tests/Simplify.test.cpp index a1491baf..cb333562 100644 --- a/tests/Simplify.test.cpp +++ b/tests/Simplify.test.cpp @@ -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") diff --git a/tests/ToString.test.cpp b/tests/ToString.test.cpp index 526ff014..06fc1b8f 100644 --- a/tests/ToString.test.cpp +++ b/tests/ToString.test.cpp @@ -50,7 +50,10 @@ TEST_CASE_FIXTURE(Fixture, "cyclic_table") TableType* tableOne = getMutable(&cyclicTable); tableOne->props["self"] = {&cyclicTable}; - CHECK_EQ("t1 where t1 = { self: t1 }", toString(&cyclicTable)); + if (FFlag::DebugLuauDeferredConstraintResolution) + CHECK_EQ("t1 where t1 = {| self: t1 |}", toString(&cyclicTable)); + else + CHECK_EQ("t1 where t1 = { self: t1 }", toString(&cyclicTable)); } TEST_CASE_FIXTURE(Fixture, "named_table") @@ -68,12 +71,18 @@ TEST_CASE_FIXTURE(Fixture, "empty_table") local a: {} )"); - CHECK_EQ("{| |}", toString(requireType("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; - CHECK_EQ("{| |}", toString(requireType("a"), opts)); + if (FFlag::DebugLuauDeferredConstraintResolution) + CHECK_EQ("{ }", toString(requireType("a"), opts)); + else + CHECK_EQ("{| |}", toString(requireType("a"), opts)); } TEST_CASE_FIXTURE(Fixture, "table_respects_use_line_break") @@ -86,12 +95,20 @@ TEST_CASE_FIXTURE(Fixture, "table_respects_use_line_break") opts.useLineBreaks = true; //clang-format off - CHECK_EQ("{|\n" - " anotherProp: number,\n" - " prop: string,\n" - " thirdProp: boolean\n" - "|}", - toString(requireType("a"), opts)); + 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" + " thirdProp: boolean\n" + "|}", + toString(requireType("a"), opts)); //clang-format on } @@ -122,7 +139,10 @@ TEST_CASE_FIXTURE(Fixture, "metatable") Type table{TypeVariant(TableType())}; Type metatable{TypeVariant(TableType())}; Type mtv{TypeVariant(MetatableType{&table, &metatable})}; - CHECK_EQ("{ @metatable { }, { } }", toString(&mtv)); + if (FFlag::DebugLuauDeferredConstraintResolution) + CHECK_EQ("{ @metatable {| |}, {| |} }", toString(&mtv)); + else + CHECK_EQ("{ @metatable { }, { } }", toString(&mtv)); } TEST_CASE_FIXTURE(Fixture, "named_metatable") @@ -258,7 +278,10 @@ TEST_CASE_FIXTURE(Fixture, "quit_stringifying_table_type_when_length_is_exceeded ToStringOptions o; o.exhaustive = false; o.maxTableLength = 40; - CHECK_EQ(toString(&tv, o), "{ a: number, b: number, c: number, d: number, e: number, ... 10 more ... }"); + 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 ... }"); } TEST_CASE_FIXTURE(Fixture, "stringifying_table_type_is_still_capped_when_exhaustive") @@ -272,7 +295,10 @@ TEST_CASE_FIXTURE(Fixture, "stringifying_table_type_is_still_capped_when_exhaust ToStringOptions o; o.exhaustive = true; o.maxTableLength = 40; - CHECK_EQ(toString(&tv, o), "{ a: number, b: number, c: number, d: number, e: number, ... 2 more ... }"); + 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 ... }"); } TEST_CASE_FIXTURE(Fixture, "quit_stringifying_type_when_length_is_exceeded") @@ -346,7 +372,10 @@ TEST_CASE_FIXTURE(Fixture, "stringifying_table_type_correctly_use_matching_table ToStringOptions o; o.maxTableLength = 40; - CHECK_EQ(toString(&tv, o), "{| a: number, b: number, c: number, d: number, e: number, ... 5 more ... |}"); + 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 ... |}"); } TEST_CASE_FIXTURE(Fixture, "stringifying_cyclic_union_type_bails_early") @@ -377,7 +406,10 @@ TEST_CASE_FIXTURE(Fixture, "stringifying_array_uses_array_syntax") CHECK_EQ("{string}", toString(Type{ttv})); ttv.props["A"] = {builtinTypes->numberType}; - CHECK_EQ("{| [number]: string, A: number |}", toString(Type{ttv})); + 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(); ttv.state = TableState::Unsealed; @@ -576,8 +608,17 @@ TEST_CASE_FIXTURE(Fixture, "toString_the_boundTo_table_type_contained_within_a_T TypePackVar tpv2{TypePack{{&tv2}}}; - CHECK_EQ("{| hello: number, world: number |}", toString(&tpv1)); - CHECK_EQ("{| hello: number, world: number |}", toString(&tpv2)); + + 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(); diff --git a/tests/TypeInfer.aliases.test.cpp b/tests/TypeInfer.aliases.test.cpp index 29dcff83..ba82d8fd 100644 --- a/tests/TypeInfer.aliases.test.cpp +++ b/tests/TypeInfer.aliases.test.cpp @@ -223,7 +223,7 @@ TEST_CASE_FIXTURE(Fixture, "dependent_generic_aliases") const std::string expected = R"(Type 'bad' could not be converted into 'U' caused by: Property 't' is not compatible. -Type '{ v: string }' could not be converted into 'T' +Type '{| v: string |}' could not be converted into 'T' caused by: Property 'v' is not compatible. Type 'string' could not be converted into 'number' in an invariant context)"; @@ -332,7 +332,10 @@ TEST_CASE_FIXTURE(Fixture, "stringify_type_alias_of_recursive_template_table_typ TypeMismatch* tm = get(result.errors[0]); REQUIRE(tm); - CHECK_EQ("t1 where t1 = ({| a: t1 |}) -> string", toString(tm->wantedType)); + 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,7 +818,10 @@ 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' )"); - CHECK_EQ("{| foo: number |}", toString(requireType("d"), {true})); + 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); } diff --git a/tests/TypeInfer.builtins.test.cpp b/tests/TypeInfer.builtins.test.cpp index de29812b..549bd3be 100644 --- a/tests/TypeInfer.builtins.test.cpp +++ b/tests/TypeInfer.builtins.test.cpp @@ -398,7 +398,10 @@ TEST_CASE_FIXTURE(BuiltinsFixture, "table_pack") )"); LUAU_REQUIRE_NO_ERRORS(result); - CHECK_EQ("{| [number]: boolean | number | string, n: number |}", toString(requireType("t"))); + 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"))); } TEST_CASE_FIXTURE(BuiltinsFixture, "table_pack_variadic") @@ -413,7 +416,10 @@ local t = table.pack(f()) )"); LUAU_REQUIRE_NO_ERRORS(result); - CHECK_EQ("{| [number]: number | string, n: number |}", toString(requireType("t"))); + if (FFlag::DebugLuauDeferredConstraintResolution) + CHECK_EQ("{ [number]: number | string, n: number }", toString(requireType("t"))); + else + CHECK_EQ("{| [number]: number | string, n: number |}", toString(requireType("t"))); } TEST_CASE_FIXTURE(BuiltinsFixture, "table_pack_reduce") @@ -423,14 +429,20 @@ TEST_CASE_FIXTURE(BuiltinsFixture, "table_pack_reduce") )"); LUAU_REQUIRE_NO_ERRORS(result); - CHECK_EQ("{| [number]: boolean | number, n: number |}", toString(requireType("t"))); + 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"( local t = table.pack("a", "b", "c") )"); LUAU_REQUIRE_NO_ERRORS(result); - CHECK_EQ("{| [number]: string, n: number |}", toString(requireType("t"))); + if (FFlag::DebugLuauDeferredConstraintResolution) + CHECK_EQ("{ [number]: string, n: number }", toString(requireType("t"))); + else + CHECK_EQ("{| [number]: string, n: number |}", toString(requireType("t"))); } TEST_CASE_FIXTURE(BuiltinsFixture, "gcinfo") diff --git a/tests/TypeInfer.cfa.test.cpp b/tests/TypeInfer.cfa.test.cpp index 04aeb54b..19700d2c 100644 --- a/tests/TypeInfer.cfa.test.cpp +++ b/tests/TypeInfer.cfa.test.cpp @@ -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", toString(requireTypeAtPosition({16, 19}))); } +TEST_CASE_FIXTURE(BuiltinsFixture, "tagged_unions_breaking") +{ + ScopedFastFlag flags[] = { + {"LuauTinyControlFlowAnalysis", true}, + {"LuauLoopControlFlowAnalysis", true} + }; + + CheckResult result = check(R"( + type Ok = { tag: "ok", value: T } + type Err = { tag: "err", error: E } + type Result = Ok | Err + + local function process(results: {Result}) + 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 = { tag: "ok", value: T } + type Err = { tag: "err", error: E } + type Result = Ok | Err + + local function process(results: {Result}) + 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}; diff --git a/tests/TypeInfer.functions.test.cpp b/tests/TypeInfer.functions.test.cpp index 036b073b..bac70592 100644 --- a/tests/TypeInfer.functions.test.cpp +++ b/tests/TypeInfer.functions.test.cpp @@ -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 |}"); } diff --git a/tests/TypeInfer.intersectionTypes.test.cpp b/tests/TypeInfer.intersectionTypes.test.cpp index 9e685881..87a3e833 100644 --- a/tests/TypeInfer.intersectionTypes.test.cpp +++ b/tests/TypeInfer.intersectionTypes.test.cpp @@ -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") diff --git a/tests/TypeInfer.modules.test.cpp b/tests/TypeInfer.modules.test.cpp index 08291b44..2d7d4778 100644 --- a/tests/TypeInfer.modules.test.cpp +++ b/tests/TypeInfer.modules.test.cpp @@ -225,7 +225,10 @@ local tbl: string = require(game.A) CheckResult result = frontend.check("game/B"); LUAU_REQUIRE_ERROR_COUNT(1, result); - CHECK_EQ("Type '{| def: number |}' could not be converted into 'string'", toString(result.errors[0])); + 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])); } TEST_CASE_FIXTURE(Fixture, "bound_free_table_export_is_ok") diff --git a/tests/TypeInfer.oop.test.cpp b/tests/TypeInfer.oop.test.cpp index a56424a7..a332dee2 100644 --- a/tests/TypeInfer.oop.test.cpp +++ b/tests/TypeInfer.oop.test.cpp @@ -410,7 +410,7 @@ TEST_CASE_FIXTURE(BuiltinsFixture, "cycle_between_object_constructor_and_alias") CHECK_MESSAGE(get(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 diff --git a/tests/TypeInfer.operators.test.cpp b/tests/TypeInfer.operators.test.cpp index c588eaba..129e25e1 100644 --- a/tests/TypeInfer.operators.test.cpp +++ b/tests/TypeInfer.operators.test.cpp @@ -571,6 +571,8 @@ TEST_CASE_FIXTURE(BuiltinsFixture, "typecheck_unary_len_error") CHECK_EQ("number", toString(requireType("a"))); TypeMismatch* tm = get(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); } diff --git a/tests/TypeInfer.provisional.test.cpp b/tests/TypeInfer.provisional.test.cpp index dd474d43..4d7fceee 100644 --- a/tests/TypeInfer.provisional.test.cpp +++ b/tests/TypeInfer.provisional.test.cpp @@ -251,10 +251,22 @@ TEST_CASE_FIXTURE(Fixture, "discriminate_from_x_not_equal_to_nil") LUAU_REQUIRE_NO_ERRORS(result); - CHECK_EQ("{| x: string, y: number |}", toString(requireTypeAtPosition({5, 28}))); + 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}))); + } - // 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)) diff --git a/tests/TypeInfer.refinements.test.cpp b/tests/TypeInfer.refinements.test.cpp index bcaad238..726a0ffa 100644 --- a/tests/TypeInfer.refinements.test.cpp +++ b/tests/TypeInfer.refinements.test.cpp @@ -535,7 +535,10 @@ 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 - CHECK_EQ(toString(requireTypeAtPosition({3, 36})), "{| x: number |}?"); // 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 } TEST_CASE_FIXTURE(Fixture, "string_not_equal_to_string_or_nil") @@ -658,7 +661,10 @@ TEST_CASE_FIXTURE(BuiltinsFixture, "typeguard_narrows_for_table") LUAU_REQUIRE_NO_ERRORS(result); - CHECK_EQ("{| x: number |} | {| y: boolean |}", toString(requireTypeAtPosition({3, 28}))); // type(x) == "table" + 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,7 +703,10 @@ TEST_CASE_FIXTURE(BuiltinsFixture, "type_guard_can_filter_for_intersection_of_ta ToStringOptions opts; opts.exhaustive = true; - CHECK_EQ("{| x: number |} & {| y: number |}", toString(requireTypeAtPosition({4, 28}), opts)); + 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,8 +1225,17 @@ TEST_CASE_FIXTURE(RefinementClassFixture, "discriminate_from_isa_of_x") LUAU_REQUIRE_NO_ERRORS(result); - CHECK_EQ(R"({| tag: "Part", x: Part |})", toString(requireTypeAtPosition({5, 28}))); - CHECK_EQ(R"({| tag: "Folder", x: Folder |})", toString(requireTypeAtPosition({7, 28}))); + + 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") diff --git a/tests/TypeInfer.tables.test.cpp b/tests/TypeInfer.tables.test.cpp index 28cebaba..b36c2ca4 100644 --- a/tests/TypeInfer.tables.test.cpp +++ b/tests/TypeInfer.tables.test.cpp @@ -91,7 +91,10 @@ TEST_CASE_FIXTURE(Fixture, "cannot_augment_sealed_table") // TODO: better, more robust comparison of type vars auto s = toString(error->tableType, ToStringOptions{/*exhaustive*/ true}); - CHECK_EQ(s, "{| prop: number |}"); + 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,7 +736,10 @@ TEST_CASE_FIXTURE(Fixture, "infer_indexer_from_value_property_in_literal") CHECK(bool(retType->indexer)); const TableIndexer& indexer = *retType->indexer; - CHECK_EQ("{| __name: string |}", toString(indexer.indexType)); + if (FFlag::DebugLuauDeferredConstraintResolution) + CHECK_EQ("{ __name: string }", toString(indexer.indexType)); + else + CHECK_EQ("{| __name: string |}", toString(indexer.indexType)); } TEST_CASE_FIXTURE(Fixture, "infer_indexer_from_its_variable_type_and_unifiable") @@ -775,7 +781,10 @@ TEST_CASE_FIXTURE(Fixture, "indexer_mismatch") TypeMismatch* tm = get(result.errors[0]); REQUIRE(tm != nullptr); CHECK(toString(tm->wantedType) == "{number}"); - CHECK(toString(tm->givenType) == "{| [string]: string |}"); + if (FFlag::DebugLuauDeferredConstraintResolution) + CHECK(toString(tm->givenType) == "{ [string]: string }"); + else + CHECK(toString(tm->givenType) == "{| [string]: string |}"); CHECK_NE(*t1, *t2); } @@ -1564,8 +1573,16 @@ TEST_CASE_FIXTURE(Fixture, "casting_sealed_tables_with_props_into_table_with_ind ToStringOptions o{/* exhaustive= */ true}; TypeMismatch* tm = get(result.errors[0]); REQUIRE(tm); - CHECK_EQ("{| [string]: string |}", toString(tm->wantedType, o)); - CHECK_EQ("{| foo: number |}", toString(tm->givenType, o)); + 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,8 +1820,16 @@ TEST_CASE_FIXTURE(Fixture, "hide_table_error_properties") LUAU_REQUIRE_ERROR_COUNT(2, result); - 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])); + 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,7 +2994,10 @@ TEST_CASE_FIXTURE(BuiltinsFixture, "access_index_metamethod_that_returns_variadi ToStringOptions o; o.exhaustive = true; - CHECK_EQ("{| x: string |}", toString(requireType("foo"), o)); + if (FFlag::DebugLuauDeferredConstraintResolution) + CHECK_EQ("{ x: string }", toString(requireType("foo"), o)); + else + CHECK_EQ("{| x: string |}", toString(requireType("foo"), o)); } TEST_CASE_FIXTURE(Fixture, "dont_invalidate_the_properties_iterator_of_free_table_when_rolled_back") @@ -3029,7 +3057,10 @@ TEST_CASE_FIXTURE(Fixture, "accidentally_checked_prop_in_opposite_branch") )"); LUAU_REQUIRE_ERROR_COUNT(1, result); - CHECK_EQ("Value of type '{| x: number? |}?' could be nil", toString(result.errors[0])); + 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,7 +3291,10 @@ TEST_CASE_FIXTURE(Fixture, "prop_access_on_unions_of_indexers_where_key_whose_ty )"); LUAU_REQUIRE_ERROR_COUNT(1, result); - CHECK_EQ("Type '{number} | {| [boolean]: number |}' does not have key 'x'", toString(result.errors[0])); + 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])); } TEST_CASE_FIXTURE(BuiltinsFixture, "quantify_metatables_of_metatables_of_table") @@ -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) -> number |}", toString(getMainModule()->returnType, ToStringOptions{true})); +} + TEST_SUITE_END(); diff --git a/tests/TypeInfer.test.cpp b/tests/TypeInfer.test.cpp index aa25b300..0e7a9715 100644 --- a/tests/TypeInfer.test.cpp +++ b/tests/TypeInfer.test.cpp @@ -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(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"( diff --git a/tests/TypeInfer.tryUnify.test.cpp b/tests/TypeInfer.tryUnify.test.cpp index 1f0b04c6..cc925cab 100644 --- a/tests/TypeInfer.tryUnify.test.cpp +++ b/tests/TypeInfer.tryUnify.test.cpp @@ -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])); } diff --git a/tests/TypeInfer.typePacks.cpp b/tests/TypeInfer.typePacks.cpp index 92d736f8..8efa8303 100644 --- a/tests/TypeInfer.typePacks.cpp +++ b/tests/TypeInfer.typePacks.cpp @@ -308,12 +308,18 @@ local c: Packed tf = lookupType("Packed"); REQUIRE(tf); CHECK_EQ(toString(*tf), "Packed"); - CHECK_EQ(toString(*tf, {true}), "{| f: (T, U...) -> (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(requireType("a")); REQUIRE(ttvA); CHECK_EQ(toString(requireType("a")), "Packed"); - CHECK_EQ(toString(requireType("a"), {true}), "{| f: (number) -> 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); CHECK_EQ(toString(ttvA->instantiatedTypeParams[0], {true}), "number"); @@ -322,7 +328,10 @@ local c: Packed auto ttvB = get(requireType("b")); REQUIRE(ttvB); CHECK_EQ(toString(requireType("b")), "Packed"); - CHECK_EQ(toString(requireType("b"), {true}), "{| f: (string, number) -> (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); CHECK_EQ(toString(ttvB->instantiatedTypeParams[0], {true}), "string"); @@ -331,7 +340,10 @@ local c: Packed auto ttvC = get(requireType("c")); REQUIRE(ttvC); CHECK_EQ(toString(requireType("c")), "Packed"); - CHECK_EQ(toString(requireType("c"), {true}), "{| f: (string, number, boolean) -> (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); CHECK_EQ(toString(ttvC->instantiatedTypeParams[0], {true}), "string"); @@ -360,12 +372,25 @@ local d: { a: typeof(c) } auto tf = lookupImportedType("Import", "Packed"); REQUIRE(tf); CHECK_EQ(toString(*tf), "Packed"); - 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 |}"); + 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 }"); + } + else + { + 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 |}"); + } } TEST_CASE_FIXTURE(BuiltinsFixture, "type_pack_type_parameters") @@ -388,19 +413,31 @@ type C = Import.Packed auto tf = lookupType("Alias"); REQUIRE(tf); CHECK_EQ(toString(*tf), "Alias"); - CHECK_EQ(toString(*tf, {true}), "{| a: S, b: (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...) -> () |}"); - CHECK_EQ(toString(requireType("a"), {true}), "{| a: string, b: (number, boolean) -> () |}"); + 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"); - CHECK_EQ(toString(*tf, {true}), "{| a: string, 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"); - CHECK_EQ(toString(*tf, {true}), "{| a: string, b: (number, 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...) -> () |}"); } TEST_CASE_FIXTURE(Fixture, "type_alias_type_packs_nested") @@ -867,7 +904,10 @@ type R = { m: F } LUAU_REQUIRE_NO_ERRORS(result); - CHECK_EQ(toString(*lookupType("R"), {true}), "t1 where t1 = {| m: (t1) -> (t1) -> () |}"); + 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) -> () |}"); } TEST_CASE_FIXTURE(Fixture, "pack_tail_unification_check") diff --git a/tests/TypeInfer.unionTypes.test.cpp b/tests/TypeInfer.unionTypes.test.cpp index b455903c..bef02ded 100644 --- a/tests/TypeInfer.unionTypes.test.cpp +++ b/tests/TypeInfer.unionTypes.test.cpp @@ -356,7 +356,10 @@ a.x = 2 LUAU_REQUIRE_ERROR_COUNT(1, result); auto s = toString(result.errors[0]); - CHECK_EQ("Value of type '({| x: number |} & {| y: number |})?' could be nil", s); + 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); } TEST_CASE_FIXTURE(Fixture, "optional_length_error") @@ -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(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(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,7 +756,10 @@ TEST_CASE_FIXTURE(Fixture, "less_greedy_unification_with_union_types_2") LUAU_REQUIRE_NO_ERRORS(result); - CHECK_EQ("({| x: number |} | {| x: string |}) -> number | string", toString(requireType("f"))); + if (FFlag::DebugLuauDeferredConstraintResolution) + CHECK_EQ("({ x: number } | { x: string }) -> number | string", toString(requireType("f"))); + else + CHECK_EQ("({| x: number |} | {| x: string |}) -> number | string", toString(requireType("f"))); } TEST_CASE_FIXTURE(Fixture, "union_table_any_property") diff --git a/tests/TypeVar.test.cpp b/tests/TypeVar.test.cpp index 8cdd36ea..674a4155 100644 --- a/tests/TypeVar.test.cpp +++ b/tests/TypeVar.test.cpp @@ -298,7 +298,10 @@ TEST_CASE_FIXTURE(Fixture, "substitution_skip_failure") REQUIRE(!anyification.normalizationTooComplex); REQUIRE(any.has_value()); - CHECK_EQ("{| f: t1 |} where t1 = () -> {| f: () -> {| f: ({| f: t1 |}) -> (), signal: {| f: (any) -> () |} |} |}", toString(*any)); + 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)); } TEST_CASE("tagging_tables") diff --git a/tests/Unifier2.test.cpp b/tests/Unifier2.test.cpp index 8ccd2587..d5ba04a9 100644 --- a/tests/Unifier2.test.cpp +++ b/tests/Unifier2.test.cpp @@ -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(); diff --git a/tests/conformance/datetime.lua b/tests/conformance/datetime.lua index dc73948b..81e0daea 100644 --- a/tests/conformance/datetime.lua +++ b/tests/conformance/datetime.lua @@ -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) diff --git a/tests/conformance/native.lua b/tests/conformance/native.lua index 7a77edec..a4369dbf 100644 --- a/tests/conformance/native.lua +++ b/tests/conformance/native.lua @@ -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') diff --git a/tests/main.cpp b/tests/main.cpp index 26872196..26981926 100644 --- a/tests/main.cpp +++ b/tests/main.cpp @@ -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("boost", 0, true); doctest::Context context; diff --git a/tools/faillist.txt b/tools/faillist.txt index a3bf97a5..17d79661 100644 --- a/tools/faillist.txt +++ b/tools/faillist.txt @@ -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