Sync to upstream/release/524 (#462)

This commit is contained in:
Arseny Kapoulkine 2022-04-21 14:44:27 -07:00 committed by GitHub
parent 5bb9f379b0
commit e0a6461173
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
53 changed files with 1596 additions and 355 deletions

View file

@ -18,7 +18,7 @@ struct CloneState
SeenTypePacks seenTypePacks; SeenTypePacks seenTypePacks;
int recursionCount = 0; int recursionCount = 0;
bool encounteredFreeType = false; bool encounteredFreeType = false; // TODO: Remove with LuauLosslessClone.
}; };
TypePackId clone(TypePackId tp, TypeArena& dest, CloneState& cloneState); TypePackId clone(TypePackId tp, TypeArena& dest, CloneState& cloneState);

View file

@ -13,6 +13,7 @@
#include <optional> #include <optional>
LUAU_FASTFLAG(LuauSeparateTypechecks) LUAU_FASTFLAG(LuauSeparateTypechecks)
LUAU_FASTFLAG(LuauDirtySourceModule)
namespace Luau namespace Luau
{ {
@ -57,19 +58,27 @@ std::optional<ModuleName> pathExprToModuleName(const ModuleName& currentModuleNa
struct SourceNode struct SourceNode
{ {
bool isDirty(bool forAutocomplete) const bool hasDirtySourceModule() const
{
LUAU_ASSERT(FFlag::LuauDirtySourceModule);
return dirtySourceModule;
}
bool hasDirtyModule(bool forAutocomplete) const
{ {
if (FFlag::LuauSeparateTypechecks) if (FFlag::LuauSeparateTypechecks)
return forAutocomplete ? dirtyAutocomplete : dirty; return forAutocomplete ? dirtyModuleForAutocomplete : dirtyModule;
else else
return dirty; return dirtyModule;
} }
ModuleName name; ModuleName name;
std::unordered_set<ModuleName> requires; std::unordered_set<ModuleName> requires;
std::vector<std::pair<ModuleName, Location>> requireLocations; std::vector<std::pair<ModuleName, Location>> requireLocations;
bool dirty = true; bool dirtySourceModule = true;
bool dirtyAutocomplete = true; bool dirtyModule = true;
bool dirtyModuleForAutocomplete = true;
double autocompleteLimitsMult = 1.0; double autocompleteLimitsMult = 1.0;
}; };
@ -163,7 +172,7 @@ struct Frontend
void applyBuiltinDefinitionToEnvironment(const std::string& environmentName, const std::string& definitionName); void applyBuiltinDefinitionToEnvironment(const std::string& environmentName, const std::string& definitionName);
private: private:
std::pair<SourceNode*, SourceModule*> getSourceNode(CheckResult& checkResult, const ModuleName& name, bool forAutocomplete); std::pair<SourceNode*, SourceModule*> getSourceNode(CheckResult& checkResult, const ModuleName& name, bool forAutocomplete_DEPRECATED);
SourceModule parse(const ModuleName& name, std::string_view src, const ParseOptions& parseOptions); SourceModule parse(const ModuleName& name, std::string_view src, const ParseOptions& parseOptions);
bool parseGraph(std::vector<ModuleName>& buildQueue, CheckResult& checkResult, const ModuleName& root, bool forAutocomplete); bool parseGraph(std::vector<ModuleName>& buildQueue, CheckResult& checkResult, const ModuleName& root, bool forAutocomplete);

View file

@ -373,15 +373,17 @@ struct ClassTypeVar
std::optional<TypeId> metatable; // metaclass? std::optional<TypeId> metatable; // metaclass?
Tags tags; Tags tags;
std::shared_ptr<ClassUserData> userData; std::shared_ptr<ClassUserData> userData;
ModuleName definitionModuleName;
ClassTypeVar( ClassTypeVar(Name name, Props props, std::optional<TypeId> parent, std::optional<TypeId> metatable, Tags tags,
Name name, Props props, std::optional<TypeId> parent, std::optional<TypeId> metatable, Tags tags, std::shared_ptr<ClassUserData> userData) std::shared_ptr<ClassUserData> userData, ModuleName definitionModuleName)
: name(name) : name(name)
, props(props) , props(props)
, parent(parent) , parent(parent)
, metatable(metatable) , metatable(metatable)
, tags(tags) , tags(tags)
, userData(userData) , userData(userData)
, definitionModuleName(definitionModuleName)
{ {
} }
}; };

View file

@ -92,7 +92,6 @@ private:
bool canCacheResult(TypeId subTy, TypeId superTy); bool canCacheResult(TypeId subTy, TypeId superTy);
void cacheResult(TypeId subTy, TypeId superTy, size_t prevErrorCount); void cacheResult(TypeId subTy, TypeId superTy, size_t prevErrorCount);
void cacheResult_DEPRECATED(TypeId subTy, TypeId superTy);
public: public:
void tryUnify(TypePackId subTy, TypePackId superTy, bool isFunctionCall = false); void tryUnify(TypePackId subTy, TypePackId superTy, bool isFunctionCall = false);

View file

@ -52,7 +52,7 @@ inline void unsee(std::unordered_set<void*>& seen, const void* tv)
inline void unsee(DenseHashSet<void*>& seen, const void* tv) inline void unsee(DenseHashSet<void*>& seen, const void* tv)
{ {
// When DenseHashSet is used for 'visitOnce', where don't forget visited elements // When DenseHashSet is used for 'visitTypeVarOnce', where don't forget visited elements
} }
template<typename F, typename Set> template<typename F, typename Set>

View file

@ -15,6 +15,7 @@
LUAU_FASTFLAGVARIABLE(LuauIfElseExprFixCompletionIssue, false); LUAU_FASTFLAGVARIABLE(LuauIfElseExprFixCompletionIssue, false);
LUAU_FASTFLAGVARIABLE(LuauAutocompleteSingletonTypes, false); LUAU_FASTFLAGVARIABLE(LuauAutocompleteSingletonTypes, false);
LUAU_FASTFLAGVARIABLE(LuauFixAutocompleteClassSecurityLevel, false);
LUAU_FASTFLAG(LuauSelfCallAutocompleteFix) LUAU_FASTFLAG(LuauSelfCallAutocompleteFix)
static const std::unordered_set<std::string> kStatementStartingKeywords = { static const std::unordered_set<std::string> kStatementStartingKeywords = {
@ -462,7 +463,8 @@ static void autocompleteProps(const Module& module, TypeArena* typeArena, TypeId
containingClass = containingClass.value_or(cls); containingClass = containingClass.value_or(cls);
fillProps(cls->props); fillProps(cls->props);
if (cls->parent) if (cls->parent)
autocompleteProps(module, typeArena, rootTy, *cls->parent, indexType, nodes, result, seen, cls); autocompleteProps(module, typeArena, rootTy, *cls->parent, indexType, nodes, result, seen,
FFlag::LuauFixAutocompleteClassSecurityLevel ? containingClass : cls);
} }
else if (auto tbl = get<TableTypeVar>(ty)) else if (auto tbl = get<TableTypeVar>(ty))
fillProps(tbl->props); fillProps(tbl->props);

View file

@ -10,6 +10,7 @@ LUAU_FASTFLAG(DebugLuauCopyBeforeNormalizing)
LUAU_FASTINTVARIABLE(LuauTypeCloneRecursionLimit, 300) LUAU_FASTINTVARIABLE(LuauTypeCloneRecursionLimit, 300)
LUAU_FASTFLAG(LuauTypecheckOptPass) LUAU_FASTFLAG(LuauTypecheckOptPass)
LUAU_FASTFLAGVARIABLE(LuauLosslessClone, false)
namespace Luau namespace Luau
{ {
@ -87,11 +88,18 @@ struct TypePackCloner
void operator()(const Unifiable::Free& t) void operator()(const Unifiable::Free& t)
{ {
cloneState.encounteredFreeType = true; if (FFlag::LuauLosslessClone)
{
defaultClone(t);
}
else
{
cloneState.encounteredFreeType = true;
TypePackId err = getSingletonTypes().errorRecoveryTypePack(getSingletonTypes().anyTypePack); TypePackId err = getSingletonTypes().errorRecoveryTypePack(getSingletonTypes().anyTypePack);
TypePackId cloned = dest.addTypePack(*err); TypePackId cloned = dest.addTypePack(*err);
seenTypePacks[typePackId] = cloned; seenTypePacks[typePackId] = cloned;
}
} }
void operator()(const Unifiable::Generic& t) void operator()(const Unifiable::Generic& t)
@ -143,10 +151,18 @@ void TypeCloner::defaultClone(const T& t)
void TypeCloner::operator()(const Unifiable::Free& t) void TypeCloner::operator()(const Unifiable::Free& t)
{ {
cloneState.encounteredFreeType = true; if (FFlag::LuauLosslessClone)
TypeId err = getSingletonTypes().errorRecoveryType(getSingletonTypes().anyType); {
TypeId cloned = dest.addType(*err); defaultClone(t);
seenTypes[typeId] = cloned; }
else
{
cloneState.encounteredFreeType = true;
TypeId err = getSingletonTypes().errorRecoveryType(getSingletonTypes().anyType);
TypeId cloned = dest.addType(*err);
seenTypes[typeId] = cloned;
}
} }
void TypeCloner::operator()(const Unifiable::Generic& t) void TypeCloner::operator()(const Unifiable::Generic& t)
@ -174,7 +190,8 @@ void TypeCloner::operator()(const PrimitiveTypeVar& t)
void TypeCloner::operator()(const ConstrainedTypeVar& t) void TypeCloner::operator()(const ConstrainedTypeVar& t)
{ {
cloneState.encounteredFreeType = true; if (!FFlag::LuauLosslessClone)
cloneState.encounteredFreeType = true;
TypeId res = dest.addType(ConstrainedTypeVar{t.level}); TypeId res = dest.addType(ConstrainedTypeVar{t.level});
ConstrainedTypeVar* ctv = getMutable<ConstrainedTypeVar>(res); ConstrainedTypeVar* ctv = getMutable<ConstrainedTypeVar>(res);
@ -252,7 +269,7 @@ void TypeCloner::operator()(const TableTypeVar& t)
for (TypePackId& arg : ttv->instantiatedTypePackParams) for (TypePackId& arg : ttv->instantiatedTypePackParams)
arg = clone(arg, dest, cloneState); arg = clone(arg, dest, cloneState);
if (ttv->state == TableState::Free) if (!FFlag::LuauLosslessClone && ttv->state == TableState::Free)
{ {
cloneState.encounteredFreeType = true; cloneState.encounteredFreeType = true;
@ -276,7 +293,7 @@ void TypeCloner::operator()(const MetatableTypeVar& t)
void TypeCloner::operator()(const ClassTypeVar& t) void TypeCloner::operator()(const ClassTypeVar& t)
{ {
TypeId result = dest.addType(ClassTypeVar{t.name, {}, std::nullopt, std::nullopt, t.tags, t.userData}); TypeId result = dest.addType(ClassTypeVar{t.name, {}, std::nullopt, std::nullopt, t.tags, t.userData, t.definitionModuleName});
ClassTypeVar* ctv = getMutable<ClassTypeVar>(result); ClassTypeVar* ctv = getMutable<ClassTypeVar>(result);
seenTypes[typeId] = result; seenTypes[typeId] = result;
@ -361,7 +378,10 @@ TypeId clone(TypeId typeId, TypeArena& dest, CloneState& cloneState)
// Persistent types are not being cloned and we get the original type back which might be read-only // Persistent types are not being cloned and we get the original type back which might be read-only
if (!res->persistent) if (!res->persistent)
{
asMutable(res)->documentationSymbol = typeId->documentationSymbol; asMutable(res)->documentationSymbol = typeId->documentationSymbol;
asMutable(res)->normal = typeId->normal;
}
} }
return res; return res;

View file

@ -8,8 +8,6 @@
#include <stdexcept> #include <stdexcept>
LUAU_FASTFLAGVARIABLE(LuauTypeMismatchModuleName, false);
static std::string wrongNumberOfArgsString(size_t expectedCount, size_t actualCount, const char* argPrefix = nullptr, bool isVariadic = false) static std::string wrongNumberOfArgsString(size_t expectedCount, size_t actualCount, const char* argPrefix = nullptr, bool isVariadic = false)
{ {
std::string s = "expects "; std::string s = "expects ";
@ -59,27 +57,20 @@ struct ErrorConverter
std::string result; std::string result;
if (FFlag::LuauTypeMismatchModuleName) if (givenTypeName == wantedTypeName)
{ {
if (givenTypeName == wantedTypeName) if (auto givenDefinitionModule = getDefinitionModuleName(tm.givenType))
{ {
if (auto givenDefinitionModule = getDefinitionModuleName(tm.givenType)) if (auto wantedDefinitionModule = getDefinitionModuleName(tm.wantedType))
{ {
if (auto wantedDefinitionModule = getDefinitionModuleName(tm.wantedType)) result = "Type '" + givenTypeName + "' from '" + *givenDefinitionModule + "' could not be converted into '" + wantedTypeName +
{ "' from '" + *wantedDefinitionModule + "'";
result = "Type '" + givenTypeName + "' from '" + *givenDefinitionModule + "' could not be converted into '" + wantedTypeName +
"' from '" + *wantedDefinitionModule + "'";
}
} }
} }
}
if (result.empty()) if (result.empty())
result = "Type '" + givenTypeName + "' could not be converted into '" + wantedTypeName + "'";
}
else
{
result = "Type '" + givenTypeName + "' could not be converted into '" + wantedTypeName + "'"; result = "Type '" + givenTypeName + "' could not be converted into '" + wantedTypeName + "'";
}
if (tm.error) if (tm.error)
{ {

View file

@ -23,6 +23,7 @@ LUAU_FASTFLAG(LuauInferInNoCheckMode)
LUAU_FASTFLAGVARIABLE(LuauKnowsTheDataModel3, false) LUAU_FASTFLAGVARIABLE(LuauKnowsTheDataModel3, false)
LUAU_FASTFLAGVARIABLE(LuauSeparateTypechecks, false) LUAU_FASTFLAGVARIABLE(LuauSeparateTypechecks, false)
LUAU_FASTFLAGVARIABLE(LuauAutocompleteDynamicLimits, false) LUAU_FASTFLAGVARIABLE(LuauAutocompleteDynamicLimits, false)
LUAU_FASTFLAGVARIABLE(LuauDirtySourceModule, false)
LUAU_FASTINTVARIABLE(LuauAutocompleteCheckTimeoutMs, 0) LUAU_FASTINTVARIABLE(LuauAutocompleteCheckTimeoutMs, 0)
namespace Luau namespace Luau
@ -358,7 +359,7 @@ CheckResult Frontend::check(const ModuleName& name, std::optional<FrontendOption
CheckResult checkResult; CheckResult checkResult;
auto it = sourceNodes.find(name); auto it = sourceNodes.find(name);
if (it != sourceNodes.end() && !it->second.isDirty(frontendOptions.forAutocomplete)) if (it != sourceNodes.end() && !it->second.hasDirtyModule(frontendOptions.forAutocomplete))
{ {
// No recheck required. // No recheck required.
if (FFlag::LuauSeparateTypechecks) if (FFlag::LuauSeparateTypechecks)
@ -402,7 +403,7 @@ CheckResult Frontend::check(const ModuleName& name, std::optional<FrontendOption
LUAU_ASSERT(sourceNodes.count(moduleName)); LUAU_ASSERT(sourceNodes.count(moduleName));
SourceNode& sourceNode = sourceNodes[moduleName]; SourceNode& sourceNode = sourceNodes[moduleName];
if (!sourceNode.isDirty(frontendOptions.forAutocomplete)) if (!sourceNode.hasDirtyModule(frontendOptions.forAutocomplete))
continue; continue;
LUAU_ASSERT(sourceModules.count(moduleName)); LUAU_ASSERT(sourceModules.count(moduleName));
@ -478,7 +479,7 @@ CheckResult Frontend::check(const ModuleName& name, std::optional<FrontendOption
stats.timeCheck += duration; stats.timeCheck += duration;
stats.filesStrict += 1; stats.filesStrict += 1;
sourceNode.dirtyAutocomplete = false; sourceNode.dirtyModuleForAutocomplete = false;
continue; continue;
} }
@ -541,7 +542,7 @@ CheckResult Frontend::check(const ModuleName& name, std::optional<FrontendOption
checkResult.errors.insert(checkResult.errors.end(), module->errors.begin(), module->errors.end()); checkResult.errors.insert(checkResult.errors.end(), module->errors.begin(), module->errors.end());
moduleResolver.modules[moduleName] = std::move(module); moduleResolver.modules[moduleName] = std::move(module);
sourceNode.dirty = false; sourceNode.dirtyModule = false;
} }
return checkResult; return checkResult;
@ -618,7 +619,7 @@ bool Frontend::parseGraph(std::vector<ModuleName>& buildQueue, CheckResult& chec
// this relies on the fact that markDirty marks reverse-dependencies dirty as well // this relies on the fact that markDirty marks reverse-dependencies dirty as well
// thus if a node is not dirty, all its transitive deps aren't dirty, which means that they won't ever need // thus if a node is not dirty, all its transitive deps aren't dirty, which means that they won't ever need
// to be built, *and* can't form a cycle with any nodes we did process. // to be built, *and* can't form a cycle with any nodes we did process.
if (!it->second.isDirty(forAutocomplete)) if (!it->second.hasDirtyModule(forAutocomplete))
continue; continue;
// note: this check is technically redundant *except* that getSourceNode has somewhat broken memoization // note: this check is technically redundant *except* that getSourceNode has somewhat broken memoization
@ -768,7 +769,7 @@ LintResult Frontend::lint(const SourceModule& module, std::optional<Luau::LintOp
bool Frontend::isDirty(const ModuleName& name, bool forAutocomplete) const bool Frontend::isDirty(const ModuleName& name, bool forAutocomplete) const
{ {
auto it = sourceNodes.find(name); auto it = sourceNodes.find(name);
return it == sourceNodes.end() || it->second.isDirty(forAutocomplete); return it == sourceNodes.end() || it->second.hasDirtyModule(forAutocomplete);
} }
/* /*
@ -810,20 +811,31 @@ void Frontend::markDirty(const ModuleName& name, std::vector<ModuleName>* marked
if (markedDirty) if (markedDirty)
markedDirty->push_back(next); markedDirty->push_back(next);
if (FFlag::LuauSeparateTypechecks) if (FFlag::LuauDirtySourceModule)
{ {
if (sourceNode.dirty && sourceNode.dirtyAutocomplete) LUAU_ASSERT(FFlag::LuauSeparateTypechecks);
if (sourceNode.dirtySourceModule && sourceNode.dirtyModule && sourceNode.dirtyModuleForAutocomplete)
continue; continue;
sourceNode.dirty = true; sourceNode.dirtySourceModule = true;
sourceNode.dirtyAutocomplete = true; sourceNode.dirtyModule = true;
sourceNode.dirtyModuleForAutocomplete = true;
}
else if (FFlag::LuauSeparateTypechecks)
{
if (sourceNode.dirtyModule && sourceNode.dirtyModuleForAutocomplete)
continue;
sourceNode.dirtyModule = true;
sourceNode.dirtyModuleForAutocomplete = true;
} }
else else
{ {
if (sourceNode.dirty) if (sourceNode.dirtyModule)
continue; continue;
sourceNode.dirty = true; sourceNode.dirtyModule = true;
} }
if (0 == reverseDeps.count(name)) if (0 == reverseDeps.count(name))
@ -851,13 +863,14 @@ const SourceModule* Frontend::getSourceModule(const ModuleName& moduleName) cons
} }
// Read AST into sourceModules if necessary. Trace require()s. Report parse errors. // Read AST into sourceModules if necessary. Trace require()s. Report parse errors.
std::pair<SourceNode*, SourceModule*> Frontend::getSourceNode(CheckResult& checkResult, const ModuleName& name, bool forAutocomplete) std::pair<SourceNode*, SourceModule*> Frontend::getSourceNode(CheckResult& checkResult, const ModuleName& name, bool forAutocomplete_DEPRECATED)
{ {
LUAU_TIMETRACE_SCOPE("Frontend::getSourceNode", "Frontend"); LUAU_TIMETRACE_SCOPE("Frontend::getSourceNode", "Frontend");
LUAU_TIMETRACE_ARGUMENT("name", name.c_str()); LUAU_TIMETRACE_ARGUMENT("name", name.c_str());
auto it = sourceNodes.find(name); auto it = sourceNodes.find(name);
if (it != sourceNodes.end() && !it->second.isDirty(forAutocomplete)) if (it != sourceNodes.end() &&
(FFlag::LuauDirtySourceModule ? !it->second.hasDirtySourceModule() : !it->second.hasDirtyModule(forAutocomplete_DEPRECATED)))
{ {
auto moduleIt = sourceModules.find(name); auto moduleIt = sourceModules.find(name);
if (moduleIt != sourceModules.end()) if (moduleIt != sourceModules.end())
@ -901,17 +914,20 @@ std::pair<SourceNode*, SourceModule*> Frontend::getSourceNode(CheckResult& check
sourceNode.requires.clear(); sourceNode.requires.clear();
sourceNode.requireLocations.clear(); sourceNode.requireLocations.clear();
if (FFlag::LuauDirtySourceModule)
sourceNode.dirtySourceModule = false;
if (FFlag::LuauSeparateTypechecks) if (FFlag::LuauSeparateTypechecks)
{ {
if (it == sourceNodes.end()) if (it == sourceNodes.end())
{ {
sourceNode.dirty = true; sourceNode.dirtyModule = true;
sourceNode.dirtyAutocomplete = true; sourceNode.dirtyModuleForAutocomplete = true;
} }
} }
else else
{ {
sourceNode.dirty = true; sourceNode.dirtyModule = true;
} }
for (const auto& [moduleName, location] : requireTrace.requires) for (const auto& [moduleName, location] : requireTrace.requires)

View file

@ -14,8 +14,8 @@
#include <algorithm> #include <algorithm>
LUAU_FASTFLAGVARIABLE(DebugLuauFreezeArena, false) LUAU_FASTFLAGVARIABLE(DebugLuauFreezeArena, false)
LUAU_FASTFLAGVARIABLE(LuauCloneDeclaredGlobals, false)
LUAU_FASTFLAG(LuauLowerBoundsCalculation) LUAU_FASTFLAG(LuauLowerBoundsCalculation)
LUAU_FASTFLAG(LuauLosslessClone)
namespace Luau namespace Luau
{ {
@ -182,20 +182,20 @@ bool Module::clonePublicInterface(InternalErrorReporter& ice)
} }
} }
if (FFlag::LuauCloneDeclaredGlobals) for (auto& [name, ty] : declaredGlobals)
{ {
for (auto& [name, ty] : declaredGlobals) ty = clone(ty, interfaceTypes, cloneState);
{ if (FFlag::LuauLowerBoundsCalculation)
ty = clone(ty, interfaceTypes, cloneState); normalize(ty, interfaceTypes, ice);
if (FFlag::LuauLowerBoundsCalculation)
normalize(ty, interfaceTypes, ice);
}
} }
freeze(internalTypes); freeze(internalTypes);
freeze(interfaceTypes); freeze(interfaceTypes);
return cloneState.encounteredFreeType; if (FFlag::LuauLosslessClone)
return false; // TODO: make function return void.
else
return cloneState.encounteredFreeType;
} }
} // namespace Luau } // namespace Luau

View file

@ -5,7 +5,6 @@
#include <algorithm> #include <algorithm>
#include "Luau/Clone.h" #include "Luau/Clone.h"
#include "Luau/DenseHash.h"
#include "Luau/Substitution.h" #include "Luau/Substitution.h"
#include "Luau/Unifier.h" #include "Luau/Unifier.h"
#include "Luau/VisitTypeVar.h" #include "Luau/VisitTypeVar.h"
@ -254,7 +253,7 @@ bool isSubtype(TypeId subTy, TypeId superTy, InternalErrorReporter& ice)
} }
template<typename T> template<typename T>
static bool areNormal_(const T& t, const DenseHashSet<void*>& seen, InternalErrorReporter& ice) static bool areNormal_(const T& t, const std::unordered_set<void*>& seen, InternalErrorReporter& ice)
{ {
int count = 0; int count = 0;
auto isNormal = [&](TypeId ty) { auto isNormal = [&](TypeId ty) {
@ -262,18 +261,19 @@ static bool areNormal_(const T& t, const DenseHashSet<void*>& seen, InternalErro
if (count >= FInt::LuauNormalizeIterationLimit) if (count >= FInt::LuauNormalizeIterationLimit)
ice.ice("Luau::areNormal hit iteration limit"); ice.ice("Luau::areNormal hit iteration limit");
return ty->normal || seen.find(asMutable(ty)); // The follow is here because a bound type may not be normal, but the bound type is normal.
return ty->normal || follow(ty)->normal || seen.find(asMutable(ty)) != seen.end();
}; };
return std::all_of(begin(t), end(t), isNormal); return std::all_of(begin(t), end(t), isNormal);
} }
static bool areNormal(const std::vector<TypeId>& types, const DenseHashSet<void*>& seen, InternalErrorReporter& ice) static bool areNormal(const std::vector<TypeId>& types, const std::unordered_set<void*>& seen, InternalErrorReporter& ice)
{ {
return areNormal_(types, seen, ice); return areNormal_(types, seen, ice);
} }
static bool areNormal(TypePackId tp, const DenseHashSet<void*>& seen, InternalErrorReporter& ice) static bool areNormal(TypePackId tp, const std::unordered_set<void*>& seen, InternalErrorReporter& ice)
{ {
tp = follow(tp); tp = follow(tp);
if (get<FreeTypePack>(tp)) if (get<FreeTypePack>(tp))
@ -288,7 +288,7 @@ static bool areNormal(TypePackId tp, const DenseHashSet<void*>& seen, InternalEr
return true; return true;
if (auto vtp = get<VariadicTypePack>(*tail)) if (auto vtp = get<VariadicTypePack>(*tail))
return vtp->ty->normal || seen.find(asMutable(vtp->ty)); return vtp->ty->normal || follow(vtp->ty)->normal || seen.find(asMutable(vtp->ty)) != seen.end();
return true; return true;
} }
@ -335,9 +335,14 @@ struct Normalize
return false; return false;
} }
bool operator()(TypeId ty, const BoundTypeVar& btv) bool operator()(TypeId ty, const BoundTypeVar& btv, std::unordered_set<void*>& seen)
{ {
// It should never be the case that this TypeVar is normal, but is bound to a non-normal type. // A type could be considered normal when it is in the stack, but we will eventually find out it is not normal as normalization progresses.
// So we need to avoid eagerly saying that this bound type is normal if the thing it is bound to is in the stack.
if (seen.find(asMutable(btv.boundTo)) != seen.end())
return false;
// It should never be the case that this TypeVar is normal, but is bound to a non-normal type, except in nontrivial cases.
LUAU_ASSERT(!ty->normal || ty->normal == btv.boundTo->normal); LUAU_ASSERT(!ty->normal || ty->normal == btv.boundTo->normal);
asMutable(ty)->normal = btv.boundTo->normal; asMutable(ty)->normal = btv.boundTo->normal;
@ -365,7 +370,7 @@ struct Normalize
return false; return false;
} }
bool operator()(TypeId ty, const ConstrainedTypeVar& ctvRef, DenseHashSet<void*>& seen) bool operator()(TypeId ty, const ConstrainedTypeVar& ctvRef, std::unordered_set<void*>& seen)
{ {
CHECK_ITERATION_LIMIT(false); CHECK_ITERATION_LIMIT(false);
@ -391,8 +396,7 @@ struct Normalize
return false; return false;
} }
bool operator()(TypeId ty, const FunctionTypeVar& ftv) = delete; bool operator()(TypeId ty, const FunctionTypeVar& ftv, std::unordered_set<void*>& seen)
bool operator()(TypeId ty, const FunctionTypeVar& ftv, DenseHashSet<void*>& seen)
{ {
CHECK_ITERATION_LIMIT(false); CHECK_ITERATION_LIMIT(false);
@ -407,7 +411,7 @@ struct Normalize
return false; return false;
} }
bool operator()(TypeId ty, const TableTypeVar& ttv, DenseHashSet<void*>& seen) bool operator()(TypeId ty, const TableTypeVar& ttv, std::unordered_set<void*>& seen)
{ {
CHECK_ITERATION_LIMIT(false); CHECK_ITERATION_LIMIT(false);
@ -419,7 +423,7 @@ struct Normalize
auto checkNormal = [&](TypeId t) { auto checkNormal = [&](TypeId t) {
// if t is on the stack, it is possible that this type is normal. // if t is on the stack, it is possible that this type is normal.
// If t is not normal and it is not on the stack, this type is definitely not normal. // If t is not normal and it is not on the stack, this type is definitely not normal.
if (!t->normal && !seen.find(asMutable(t))) if (!t->normal && seen.find(asMutable(t)) == seen.end())
normal = false; normal = false;
}; };
@ -449,7 +453,7 @@ struct Normalize
return false; return false;
} }
bool operator()(TypeId ty, const MetatableTypeVar& mtv, DenseHashSet<void*>& seen) bool operator()(TypeId ty, const MetatableTypeVar& mtv, std::unordered_set<void*>& seen)
{ {
CHECK_ITERATION_LIMIT(false); CHECK_ITERATION_LIMIT(false);
@ -477,7 +481,7 @@ struct Normalize
return false; return false;
} }
bool operator()(TypeId ty, const UnionTypeVar& utvRef, DenseHashSet<void*>& seen) bool operator()(TypeId ty, const UnionTypeVar& utvRef, std::unordered_set<void*>& seen)
{ {
CHECK_ITERATION_LIMIT(false); CHECK_ITERATION_LIMIT(false);
@ -507,7 +511,7 @@ struct Normalize
return false; return false;
} }
bool operator()(TypeId ty, const IntersectionTypeVar& itvRef, DenseHashSet<void*>& seen) bool operator()(TypeId ty, const IntersectionTypeVar& itvRef, std::unordered_set<void*>& seen)
{ {
CHECK_ITERATION_LIMIT(false); CHECK_ITERATION_LIMIT(false);
@ -775,8 +779,8 @@ std::pair<TypeId, bool> normalize(TypeId ty, TypeArena& arena, InternalErrorRepo
(void)clone(ty, arena, state); (void)clone(ty, arena, state);
Normalize n{arena, ice, std::move(state.seenTypes), std::move(state.seenTypePacks)}; Normalize n{arena, ice, std::move(state.seenTypes), std::move(state.seenTypePacks)};
DenseHashSet<void*> seen{nullptr}; std::unordered_set<void*> seen;
visitTypeVarOnce(ty, n, seen); visitTypeVar(ty, n, seen);
return {ty, !n.limitExceeded}; return {ty, !n.limitExceeded};
} }
@ -800,8 +804,8 @@ std::pair<TypePackId, bool> normalize(TypePackId tp, TypeArena& arena, InternalE
(void)clone(tp, arena, state); (void)clone(tp, arena, state);
Normalize n{arena, ice, std::move(state.seenTypes), std::move(state.seenTypePacks)}; Normalize n{arena, ice, std::move(state.seenTypes), std::move(state.seenTypePacks)};
DenseHashSet<void*> seen{nullptr}; std::unordered_set<void*> seen;
visitTypeVarOnce(tp, n, seen); visitTypeVar(tp, n, seen);
return {tp, !n.limitExceeded}; return {tp, !n.limitExceeded};
} }

View file

@ -11,6 +11,7 @@ LUAU_FASTFLAG(LuauLowerBoundsCalculation)
LUAU_FASTINTVARIABLE(LuauTarjanChildLimit, 1000) LUAU_FASTINTVARIABLE(LuauTarjanChildLimit, 1000)
LUAU_FASTFLAG(LuauTypecheckOptPass) LUAU_FASTFLAG(LuauTypecheckOptPass)
LUAU_FASTFLAGVARIABLE(LuauSubstituteFollowNewTypes, false) LUAU_FASTFLAGVARIABLE(LuauSubstituteFollowNewTypes, false)
LUAU_FASTFLAGVARIABLE(LuauSubstituteFollowPossibleMutations, false)
namespace Luau namespace Luau
{ {
@ -106,7 +107,7 @@ void Tarjan::visitChildren(TypePackId tp, int index)
std::pair<int, bool> Tarjan::indexify(TypeId ty) std::pair<int, bool> Tarjan::indexify(TypeId ty)
{ {
if (FFlag::LuauTypecheckOptPass) if (FFlag::LuauTypecheckOptPass && !FFlag::LuauSubstituteFollowPossibleMutations)
LUAU_ASSERT(ty == log->follow(ty)); LUAU_ASSERT(ty == log->follow(ty));
else else
ty = log->follow(ty); ty = log->follow(ty);
@ -127,7 +128,7 @@ std::pair<int, bool> Tarjan::indexify(TypeId ty)
std::pair<int, bool> Tarjan::indexify(TypePackId tp) std::pair<int, bool> Tarjan::indexify(TypePackId tp)
{ {
if (FFlag::LuauTypecheckOptPass) if (FFlag::LuauTypecheckOptPass && !FFlag::LuauSubstituteFollowPossibleMutations)
LUAU_ASSERT(tp == log->follow(tp)); LUAU_ASSERT(tp == log->follow(tp));
else else
tp = log->follow(tp); tp = log->follow(tp);
@ -148,7 +149,8 @@ std::pair<int, bool> Tarjan::indexify(TypePackId tp)
void Tarjan::visitChild(TypeId ty) void Tarjan::visitChild(TypeId ty)
{ {
ty = log->follow(ty); if (!FFlag::LuauSubstituteFollowPossibleMutations)
ty = log->follow(ty);
edgesTy.push_back(ty); edgesTy.push_back(ty);
edgesTp.push_back(nullptr); edgesTp.push_back(nullptr);
@ -156,7 +158,8 @@ void Tarjan::visitChild(TypeId ty)
void Tarjan::visitChild(TypePackId tp) void Tarjan::visitChild(TypePackId tp)
{ {
tp = log->follow(tp); if (!FFlag::LuauSubstituteFollowPossibleMutations)
tp = log->follow(tp);
edgesTy.push_back(nullptr); edgesTy.push_back(nullptr);
edgesTp.push_back(tp); edgesTp.push_back(tp);
@ -471,7 +474,7 @@ TypePackId Substitution::clone(TypePackId tp)
void Substitution::foundDirty(TypeId ty) void Substitution::foundDirty(TypeId ty)
{ {
if (FFlag::LuauTypecheckOptPass) if (FFlag::LuauTypecheckOptPass && !FFlag::LuauSubstituteFollowPossibleMutations)
LUAU_ASSERT(ty == log->follow(ty)); LUAU_ASSERT(ty == log->follow(ty));
else else
ty = log->follow(ty); ty = log->follow(ty);
@ -484,7 +487,7 @@ void Substitution::foundDirty(TypeId ty)
void Substitution::foundDirty(TypePackId tp) void Substitution::foundDirty(TypePackId tp)
{ {
if (FFlag::LuauTypecheckOptPass) if (FFlag::LuauTypecheckOptPass && !FFlag::LuauSubstituteFollowPossibleMutations)
LUAU_ASSERT(tp == log->follow(tp)); LUAU_ASSERT(tp == log->follow(tp));
else else
tp = log->follow(tp); tp = log->follow(tp);

View file

@ -327,7 +327,7 @@ void StateDot::visitChildren(TypePackId tp, int index)
} }
else if (const VariadicTypePack* vtp = get<VariadicTypePack>(tp)) else if (const VariadicTypePack* vtp = get<VariadicTypePack>(tp))
{ {
formatAppend(result, "VariadicTypePack %d", index); formatAppend(result, "VariadicTypePack %s%d", vtp->hidden ? "hidden " : "", index);
finishNodeLabel(tp); finishNodeLabel(tp);
finishNode(); finishNode();

View file

@ -1025,31 +1025,42 @@ struct Printer
} }
else if (const auto& a = typeAnnotation.as<AstTypeTable>()) else if (const auto& a = typeAnnotation.as<AstTypeTable>())
{ {
CommaSeparatorInserter comma(writer); AstTypeReference* indexType = a->indexer ? a->indexer->indexType->as<AstTypeReference>() : nullptr;
writer.symbol("{"); if (a->props.size == 0 && indexType && indexType->name == "number")
for (std::size_t i = 0; i < a->props.size; ++i)
{ {
comma(); writer.symbol("{");
advance(a->props.data[i].location.begin);
writer.identifier(a->props.data[i].name.value);
if (a->props.data[i].type)
{
writer.symbol(":");
visualizeTypeAnnotation(*a->props.data[i].type);
}
}
if (a->indexer)
{
comma();
writer.symbol("[");
visualizeTypeAnnotation(*a->indexer->indexType);
writer.symbol("]");
writer.symbol(":");
visualizeTypeAnnotation(*a->indexer->resultType); visualizeTypeAnnotation(*a->indexer->resultType);
writer.symbol("}");
}
else
{
CommaSeparatorInserter comma(writer);
writer.symbol("{");
for (std::size_t i = 0; i < a->props.size; ++i)
{
comma();
advance(a->props.data[i].location.begin);
writer.identifier(a->props.data[i].name.value);
if (a->props.data[i].type)
{
writer.symbol(":");
visualizeTypeAnnotation(*a->props.data[i].type);
}
}
if (a->indexer)
{
comma();
writer.symbol("[");
visualizeTypeAnnotation(*a->indexer->indexType);
writer.symbol("]");
writer.symbol(":");
visualizeTypeAnnotation(*a->indexer->resultType);
}
writer.symbol("}");
} }
writer.symbol("}");
} }
else if (auto a = typeAnnotation.as<AstTypeTypeof>()) else if (auto a = typeAnnotation.as<AstTypeTypeof>())
{ {

View file

@ -479,6 +479,20 @@ public:
{ {
return visitLocal(al->local); return visitLocal(al->local);
} }
virtual bool visit(AstStatFor* stat) override
{
visitLocal(stat->var);
return true;
}
virtual bool visit(AstStatForIn* stat) override
{
for (size_t i = 0; i < stat->vars.size; ++i)
visitLocal(stat->vars.data[i]);
return true;
}
virtual bool visit(AstExprFunction* fn) override virtual bool visit(AstExprFunction* fn) override
{ {
// TODO: add generics if the inferred type of the function is generic CLI-39908 // TODO: add generics if the inferred type of the function is generic CLI-39908

View file

@ -1,6 +1,7 @@
// This file is part of the Luau programming language and is licensed under MIT License; see LICENSE.txt for details // This file is part of the Luau programming language and is licensed under MIT License; see LICENSE.txt for details
#include "Luau/TypeInfer.h" #include "Luau/TypeInfer.h"
#include "Luau/Clone.h"
#include "Luau/Common.h" #include "Luau/Common.h"
#include "Luau/ModuleResolver.h" #include "Luau/ModuleResolver.h"
#include "Luau/Normalize.h" #include "Luau/Normalize.h"
@ -47,7 +48,6 @@ LUAU_FASTFLAGVARIABLE(LuauPropertiesGetExpectedType, false)
LUAU_FASTFLAGVARIABLE(LuauStatFunctionSimplify4, false) LUAU_FASTFLAGVARIABLE(LuauStatFunctionSimplify4, false)
LUAU_FASTFLAGVARIABLE(LuauTypecheckOptPass, false) LUAU_FASTFLAGVARIABLE(LuauTypecheckOptPass, false)
LUAU_FASTFLAGVARIABLE(LuauUnsealedTableLiteral, false) LUAU_FASTFLAGVARIABLE(LuauUnsealedTableLiteral, false)
LUAU_FASTFLAG(LuauTypeMismatchModuleName)
LUAU_FASTFLAGVARIABLE(LuauTwoPassAliasDefinitionFix, false) LUAU_FASTFLAGVARIABLE(LuauTwoPassAliasDefinitionFix, false)
LUAU_FASTFLAGVARIABLE(LuauAssertStripsFalsyTypes, false) LUAU_FASTFLAGVARIABLE(LuauAssertStripsFalsyTypes, false)
LUAU_FASTFLAGVARIABLE(LuauReturnAnyInsteadOfICE, false) // Eventually removed as false. LUAU_FASTFLAGVARIABLE(LuauReturnAnyInsteadOfICE, false) // Eventually removed as false.
@ -61,7 +61,9 @@ LUAU_FASTFLAG(LuauAnyInIsOptionalIsOptional)
LUAU_FASTFLAGVARIABLE(LuauDecoupleOperatorInferenceFromUnifiedTypeInference, false) LUAU_FASTFLAGVARIABLE(LuauDecoupleOperatorInferenceFromUnifiedTypeInference, false)
LUAU_FASTFLAGVARIABLE(LuauArgCountMismatchSaysAtLeastWhenVariadic, false) LUAU_FASTFLAGVARIABLE(LuauArgCountMismatchSaysAtLeastWhenVariadic, false)
LUAU_FASTFLAGVARIABLE(LuauTableUseCounterInstead, false) LUAU_FASTFLAGVARIABLE(LuauTableUseCounterInstead, false)
LUAU_FASTFLAGVARIABLE(LuauReturnTypeInferenceInNonstrict, false)
LUAU_FASTFLAGVARIABLE(LuauRecursionLimitException, false); LUAU_FASTFLAGVARIABLE(LuauRecursionLimitException, false);
LUAU_FASTFLAG(LuauLosslessClone)
namespace Luau namespace Luau
{ {
@ -376,7 +378,7 @@ ModulePtr TypeChecker::checkWithoutRecursionCheck(const SourceModule& module, Mo
prepareErrorsForDisplay(currentModule->errors); prepareErrorsForDisplay(currentModule->errors);
bool encounteredFreeType = currentModule->clonePublicInterface(*iceHandler); bool encounteredFreeType = currentModule->clonePublicInterface(*iceHandler);
if (encounteredFreeType) if (!FFlag::LuauLosslessClone && encounteredFreeType)
{ {
reportError(TypeError{module.root->location, reportError(TypeError{module.root->location,
GenericError{"Free types leaked into this module's public interface. This is an internal Luau error; please report it."}}); GenericError{"Free types leaked into this module's public interface. This is an internal Luau error; please report it."}});
@ -785,7 +787,7 @@ void TypeChecker::check(const ScopePtr& scope, const AstStatReturn& return_)
TypePackId retPack = checkExprList(scope, return_.location, return_.list, false, {}, expectedTypes).type; TypePackId retPack = checkExprList(scope, return_.location, return_.list, false, {}, expectedTypes).type;
if (useConstrainedIntersections()) if (FFlag::LuauReturnTypeInferenceInNonstrict ? FFlag::LuauLowerBoundsCalculation : useConstrainedIntersections())
{ {
unifyLowerBound(retPack, scope->returnType, return_.location); unifyLowerBound(retPack, scope->returnType, return_.location);
return; return;
@ -1241,7 +1243,12 @@ void TypeChecker::check(const ScopePtr& scope, TypeId ty, const ScopePtr& funSco
// If in nonstrict mode and allowing redefinition of global function, restore the previous definition type // If in nonstrict mode and allowing redefinition of global function, restore the previous definition type
// in case this function has a differing signature. The signature discrepancy will be caught in checkBlock. // in case this function has a differing signature. The signature discrepancy will be caught in checkBlock.
if (previouslyDefined) if (previouslyDefined)
{
if (FFlag::LuauReturnTypeInferenceInNonstrict && FFlag::LuauLowerBoundsCalculation)
quantify(funScope, ty, exprName->location);
globalBindings[name] = oldBinding; globalBindings[name] = oldBinding;
}
else else
globalBindings[name] = {quantify(funScope, ty, exprName->location), exprName->location}; globalBindings[name] = {quantify(funScope, ty, exprName->location), exprName->location};
@ -1555,7 +1562,7 @@ void TypeChecker::check(const ScopePtr& scope, const AstStatDeclareClass& declar
Name className(declaredClass.name.value); Name className(declaredClass.name.value);
TypeId classTy = addType(ClassTypeVar(className, {}, superTy, std::nullopt, {}, {})); TypeId classTy = addType(ClassTypeVar(className, {}, superTy, std::nullopt, {}, {}, currentModuleName));
ClassTypeVar* ctv = getMutable<ClassTypeVar>(classTy); ClassTypeVar* ctv = getMutable<ClassTypeVar>(classTy);
TypeId metaTy = addType(TableTypeVar{TableState::Sealed, scope->level}); TypeId metaTy = addType(TableTypeVar{TableState::Sealed, scope->level});
@ -3284,7 +3291,7 @@ std::pair<TypeId, ScopePtr> TypeChecker::checkFunctionSignature(
TypePackId retPack; TypePackId retPack;
if (expr.returnAnnotation) if (expr.returnAnnotation)
retPack = resolveTypePack(funScope, *expr.returnAnnotation); retPack = resolveTypePack(funScope, *expr.returnAnnotation);
else if (isNonstrictMode()) else if (FFlag::LuauReturnTypeInferenceInNonstrict ? (!FFlag::LuauLowerBoundsCalculation && isNonstrictMode()) : isNonstrictMode())
retPack = anyTypePack; retPack = anyTypePack;
else if (expectedFunctionType) else if (expectedFunctionType)
{ {
@ -5328,19 +5335,9 @@ TypeId TypeChecker::resolveType(const ScopePtr& scope, const AstType& annotation
if (const auto& indexer = table->indexer) if (const auto& indexer = table->indexer)
tableIndexer = TableIndexer(resolveType(scope, *indexer->indexType), resolveType(scope, *indexer->resultType)); tableIndexer = TableIndexer(resolveType(scope, *indexer->indexType), resolveType(scope, *indexer->resultType));
if (FFlag::LuauTypeMismatchModuleName) TableTypeVar ttv{props, tableIndexer, scope->level, TableState::Sealed};
{ ttv.definitionModuleName = currentModuleName;
TableTypeVar ttv{props, tableIndexer, scope->level, TableState::Sealed}; return addType(std::move(ttv));
ttv.definitionModuleName = currentModuleName;
return addType(std::move(ttv));
}
else
{
return addType(TableTypeVar{
props, tableIndexer, scope->level,
TableState::Sealed // FIXME: probably want a way to annotate other kinds of tables maybe
});
}
} }
else if (const auto& func = annotation.as<AstTypeFunction>()) else if (const auto& func = annotation.as<AstTypeFunction>())
{ {
@ -5602,9 +5599,7 @@ TypeId TypeChecker::instantiateTypeFun(const ScopePtr& scope, const TypeFun& tf,
{ {
ttv->instantiatedTypeParams = typeParams; ttv->instantiatedTypeParams = typeParams;
ttv->instantiatedTypePackParams = typePackParams; ttv->instantiatedTypePackParams = typePackParams;
ttv->definitionModuleName = currentModuleName;
if (FFlag::LuauTypeMismatchModuleName)
ttv->definitionModuleName = currentModuleName;
} }
return instantiated; return instantiated;

View file

@ -27,6 +27,7 @@ LUAU_FASTFLAG(LuauErrorRecoveryType)
LUAU_FASTFLAG(LuauSubtypingAddOptPropsToUnsealedTables) LUAU_FASTFLAG(LuauSubtypingAddOptPropsToUnsealedTables)
LUAU_FASTFLAG(LuauDiscriminableUnions2) LUAU_FASTFLAG(LuauDiscriminableUnions2)
LUAU_FASTFLAGVARIABLE(LuauAnyInIsOptionalIsOptional, false) LUAU_FASTFLAGVARIABLE(LuauAnyInIsOptionalIsOptional, false)
LUAU_FASTFLAGVARIABLE(LuauClassDefinitionModuleInError, false)
namespace Luau namespace Luau
{ {
@ -304,6 +305,11 @@ std::optional<ModuleName> getDefinitionModuleName(TypeId type)
if (ftv->definition) if (ftv->definition)
return ftv->definition->definitionModuleName; return ftv->definition->definitionModuleName;
} }
else if (auto ctv = get<ClassTypeVar>(type); ctv && FFlag::LuauClassDefinitionModuleInError)
{
if (!ctv->definitionModuleName.empty())
return ctv->definitionModuleName;
}
return std::nullopt; return std::nullopt;
} }

View file

@ -17,7 +17,6 @@ LUAU_FASTINT(LuauTypeInferTypePackLoopLimit);
LUAU_FASTINT(LuauTypeInferIterationLimit); LUAU_FASTINT(LuauTypeInferIterationLimit);
LUAU_FASTFLAG(LuauAutocompleteDynamicLimits) LUAU_FASTFLAG(LuauAutocompleteDynamicLimits)
LUAU_FASTINTVARIABLE(LuauTypeInferLowerBoundsIterationLimit, 2000); LUAU_FASTINTVARIABLE(LuauTypeInferLowerBoundsIterationLimit, 2000);
LUAU_FASTFLAGVARIABLE(LuauExtendedIndexerError, false);
LUAU_FASTFLAGVARIABLE(LuauTableSubtypingVariance2, false); LUAU_FASTFLAGVARIABLE(LuauTableSubtypingVariance2, false);
LUAU_FASTFLAG(LuauLowerBoundsCalculation); LUAU_FASTFLAG(LuauLowerBoundsCalculation);
LUAU_FASTFLAG(LuauErrorRecoveryType); LUAU_FASTFLAG(LuauErrorRecoveryType);
@ -28,7 +27,6 @@ LUAU_FASTFLAGVARIABLE(LuauTxnLogSeesTypePacks2, false)
LUAU_FASTFLAGVARIABLE(LuauTxnLogCheckForInvalidation, false) LUAU_FASTFLAGVARIABLE(LuauTxnLogCheckForInvalidation, false)
LUAU_FASTFLAGVARIABLE(LuauTxnLogRefreshFunctionPointers, false) LUAU_FASTFLAGVARIABLE(LuauTxnLogRefreshFunctionPointers, false)
LUAU_FASTFLAGVARIABLE(LuauTxnLogDontRetryForIndexers, false) LUAU_FASTFLAGVARIABLE(LuauTxnLogDontRetryForIndexers, false)
LUAU_FASTFLAGVARIABLE(LuauUnifierCacheErrors, false)
LUAU_FASTFLAG(LuauAnyInIsOptionalIsOptional) LUAU_FASTFLAG(LuauAnyInIsOptionalIsOptional)
LUAU_FASTFLAG(LuauTypecheckOptPass) LUAU_FASTFLAG(LuauTypecheckOptPass)
@ -474,32 +472,21 @@ void Unifier::tryUnify_(TypeId subTy, TypeId superTy, bool isFunctionCall, bool
if (get<ErrorTypeVar>(subTy)) if (get<ErrorTypeVar>(subTy))
return tryUnifyWithAny(superTy, subTy); return tryUnifyWithAny(superTy, subTy);
bool cacheEnabled;
auto& cache = sharedState.cachedUnify; auto& cache = sharedState.cachedUnify;
// What if the types are immutable and we proved their relation before // What if the types are immutable and we proved their relation before
if (FFlag::LuauUnifierCacheErrors) bool cacheEnabled = !isFunctionCall && !isIntersection && variance == Invariant;
if (cacheEnabled)
{ {
cacheEnabled = !isFunctionCall && !isIntersection && variance == Invariant; if (cache.contains({subTy, superTy}))
if (cacheEnabled)
{
if (cache.contains({subTy, superTy}))
return;
if (auto error = sharedState.cachedUnifyError.find({subTy, superTy}))
{
reportError(TypeError{location, *error});
return;
}
}
}
else
{
cacheEnabled = !isFunctionCall && !isIntersection;
if (cacheEnabled && cache.contains({superTy, subTy}) && (variance == Covariant || cache.contains({subTy, superTy})))
return; return;
if (auto error = sharedState.cachedUnifyError.find({subTy, superTy}))
{
reportError(TypeError{location, *error});
return;
}
} }
// If we have seen this pair of types before, we are currently recursing into cyclic types. // If we have seen this pair of types before, we are currently recursing into cyclic types.
@ -543,12 +530,6 @@ void Unifier::tryUnify_(TypeId subTy, TypeId superTy, bool isFunctionCall, bool
else if (log.getMutable<TableTypeVar>(superTy) && log.getMutable<TableTypeVar>(subTy)) else if (log.getMutable<TableTypeVar>(superTy) && log.getMutable<TableTypeVar>(subTy))
{ {
tryUnifyTables(subTy, superTy, isIntersection); tryUnifyTables(subTy, superTy, isIntersection);
if (!FFlag::LuauUnifierCacheErrors)
{
if (cacheEnabled && errors.empty())
cacheResult_DEPRECATED(subTy, superTy);
}
} }
// tryUnifyWithMetatable assumes its first argument is a MetatableTypeVar. The check is otherwise symmetrical. // tryUnifyWithMetatable assumes its first argument is a MetatableTypeVar. The check is otherwise symmetrical.
@ -568,7 +549,7 @@ void Unifier::tryUnify_(TypeId subTy, TypeId superTy, bool isFunctionCall, bool
else else
reportError(TypeError{location, TypeMismatch{superTy, subTy}}); reportError(TypeError{location, TypeMismatch{superTy, subTy}});
if (FFlag::LuauUnifierCacheErrors && cacheEnabled) if (cacheEnabled)
cacheResult(subTy, superTy, errorCount); cacheResult(subTy, superTy, errorCount);
log.popSeen(superTy, subTy); log.popSeen(superTy, subTy);
@ -705,21 +686,10 @@ void Unifier::tryUnifyTypeWithUnion(TypeId subTy, TypeId superTy, const UnionTyp
{ {
TypeId type = uv->options[i]; TypeId type = uv->options[i];
if (FFlag::LuauUnifierCacheErrors) if (cache.contains({subTy, type}))
{ {
if (cache.contains({subTy, type})) startIndex = i;
{ break;
startIndex = i;
break;
}
}
else
{
if (cache.contains({type, subTy}) && (variance == Covariant || cache.contains({subTy, type})))
{
startIndex = i;
break;
}
} }
} }
} }
@ -807,21 +777,10 @@ void Unifier::tryUnifyIntersectionWithType(TypeId subTy, const IntersectionTypeV
{ {
TypeId type = uv->parts[i]; TypeId type = uv->parts[i];
if (FFlag::LuauUnifierCacheErrors) if (cache.contains({type, superTy}))
{ {
if (cache.contains({type, superTy})) startIndex = i;
{ break;
startIndex = i;
break;
}
}
else
{
if (cache.contains({superTy, type}) && (variance == Covariant || cache.contains({type, superTy})))
{
startIndex = i;
break;
}
} }
} }
} }
@ -896,19 +855,6 @@ void Unifier::cacheResult(TypeId subTy, TypeId superTy, size_t prevErrorCount)
} }
} }
void Unifier::cacheResult_DEPRECATED(TypeId subTy, TypeId superTy)
{
LUAU_ASSERT(!FFlag::LuauUnifierCacheErrors);
if (!canCacheResult(subTy, superTy))
return;
sharedState.cachedUnify.insert({superTy, subTy});
if (variance == Invariant)
sharedState.cachedUnify.insert({subTy, superTy});
}
struct WeirdIter struct WeirdIter
{ {
TypePackId packId; TypePackId packId;
@ -1650,24 +1596,16 @@ void Unifier::tryUnifyTables(TypeId subTy, TypeId superTy, bool isIntersection)
Unifier innerState = makeChildUnifier(); Unifier innerState = makeChildUnifier();
if (FFlag::LuauExtendedIndexerError) innerState.tryUnify_(subTable->indexer->indexType, superTable->indexer->indexType);
{
innerState.tryUnify_(subTable->indexer->indexType, superTable->indexer->indexType);
bool reported = !innerState.errors.empty(); bool reported = !innerState.errors.empty();
checkChildUnifierTypeMismatch(innerState.errors, "[indexer key]", superTy, subTy); checkChildUnifierTypeMismatch(innerState.errors, "[indexer key]", superTy, subTy);
innerState.tryUnify_(subTable->indexer->indexResultType, superTable->indexer->indexResultType); innerState.tryUnify_(subTable->indexer->indexResultType, superTable->indexer->indexResultType);
if (!reported) if (!reported)
checkChildUnifierTypeMismatch(innerState.errors, "[indexer value]", superTy, subTy); checkChildUnifierTypeMismatch(innerState.errors, "[indexer value]", superTy, subTy);
}
else
{
innerState.tryUnifyIndexer(*subTable->indexer, *superTable->indexer);
checkChildUnifierTypeMismatch(innerState.errors, superTy, subTy);
}
if (innerState.errors.empty()) if (innerState.errors.empty())
log.concat(std::move(innerState.log)); log.concat(std::move(innerState.log));
@ -2225,7 +2163,7 @@ void Unifier::tryUnifyWithClass(TypeId subTy, TypeId superTy, bool reversed)
void Unifier::tryUnifyIndexer(const TableIndexer& subIndexer, const TableIndexer& superIndexer) void Unifier::tryUnifyIndexer(const TableIndexer& subIndexer, const TableIndexer& superIndexer)
{ {
LUAU_ASSERT(!FFlag::LuauTableSubtypingVariance2 || !FFlag::LuauExtendedIndexerError); LUAU_ASSERT(!FFlag::LuauTableSubtypingVariance2);
tryUnify_(subIndexer.indexType, superIndexer.indexType); tryUnify_(subIndexer.indexType, superIndexer.indexType);
tryUnify_(subIndexer.indexResultType, superIndexer.indexResultType); tryUnify_(subIndexer.indexResultType, superIndexer.indexResultType);

View file

@ -19,6 +19,7 @@ std::string format(const char* fmt, ...) LUAU_PRINTF_ATTR(1, 2);
std::string vformat(const char* fmt, va_list args); std::string vformat(const char* fmt, va_list args);
void formatAppend(std::string& str, const char* fmt, ...) LUAU_PRINTF_ATTR(2, 3); void formatAppend(std::string& str, const char* fmt, ...) LUAU_PRINTF_ATTR(2, 3);
void vformatAppend(std::string& ret, const char* fmt, va_list args);
std::string join(const std::vector<std::string_view>& segments, std::string_view delimiter); std::string join(const std::vector<std::string_view>& segments, std::string_view delimiter);
std::string join(const std::vector<std::string>& segments, std::string_view delimiter); std::string join(const std::vector<std::string>& segments, std::string_view delimiter);

View file

@ -167,6 +167,7 @@ Parser::Parser(const char* buffer, size_t bufferSize, AstNameTable& names, Alloc
Function top; Function top;
top.vararg = true; top.vararg = true;
functionStack.reserve(8);
functionStack.push_back(top); functionStack.push_back(top);
nameSelf = names.addStatic("self"); nameSelf = names.addStatic("self");
@ -186,6 +187,13 @@ Parser::Parser(const char* buffer, size_t bufferSize, AstNameTable& names, Alloc
// all hot comments parsed after the first non-comment lexeme are special in that they don't affect type checking / linting mode // all hot comments parsed after the first non-comment lexeme are special in that they don't affect type checking / linting mode
hotcommentHeader = false; hotcommentHeader = false;
// preallocate some buffers that are very likely to grow anyway; this works around std::vector's inefficient growth policy for small arrays
localStack.reserve(16);
scratchStat.reserve(16);
scratchExpr.reserve(16);
scratchLocal.reserve(16);
scratchBinding.reserve(16);
} }
bool Parser::blockFollow(const Lexeme& l) bool Parser::blockFollow(const Lexeme& l)

View file

@ -11,7 +11,7 @@
namespace Luau namespace Luau
{ {
static void vformatAppend(std::string& ret, const char* fmt, va_list args) void vformatAppend(std::string& ret, const char* fmt, va_list args)
{ {
va_list argscopy; va_list argscopy;
va_copy(argscopy, args); va_copy(argscopy, args);

View file

@ -579,7 +579,8 @@ static bool compileFile(const char* name, CompileFormat format)
if (format == CompileFormat::Text) if (format == CompileFormat::Text)
{ {
bcb.setDumpFlags(Luau::BytecodeBuilder::Dump_Code | Luau::BytecodeBuilder::Dump_Source | Luau::BytecodeBuilder::Dump_Locals); bcb.setDumpFlags(Luau::BytecodeBuilder::Dump_Code | Luau::BytecodeBuilder::Dump_Source | Luau::BytecodeBuilder::Dump_Locals |
Luau::BytecodeBuilder::Dump_Remarks);
bcb.setDumpSource(*source); bcb.setDumpSource(*source);
} }
@ -636,13 +637,60 @@ static int assertionHandler(const char* expr, const char* file, int line, const
return 1; return 1;
} }
static void setLuauFlags(bool state)
{
for (Luau::FValue<bool>* flag = Luau::FValue<bool>::list; flag; flag = flag->next)
{
if (strncmp(flag->name, "Luau", 4) == 0)
flag->value = state;
}
}
static void setFlag(std::string_view name, bool state)
{
for (Luau::FValue<bool>* flag = Luau::FValue<bool>::list; flag; flag = flag->next)
{
if (name == flag->name)
{
flag->value = state;
return;
}
}
fprintf(stderr, "Warning: --fflag unrecognized flag '%.*s'.\n\n", int(name.length()), name.data());
}
static void applyFlagKeyValue(std::string_view element)
{
if (size_t separator = element.find('='); separator != std::string_view::npos)
{
std::string_view key = element.substr(0, separator);
std::string_view value = element.substr(separator + 1);
if (value == "true")
setFlag(key, true);
else if (value == "false")
setFlag(key, false);
else
fprintf(stderr, "Warning: --fflag unrecognized value '%.*s' for flag '%.*s'.\n\n", int(value.length()), value.data(), int(key.length()),
key.data());
}
else
{
if (element == "true")
setLuauFlags(true);
else if (element == "false")
setLuauFlags(false);
else
setFlag(element, true);
}
}
int replMain(int argc, char** argv) int replMain(int argc, char** argv)
{ {
Luau::assertHandler() = assertionHandler; Luau::assertHandler() = assertionHandler;
for (Luau::FValue<bool>* flag = Luau::FValue<bool>::list; flag; flag = flag->next) setLuauFlags(true);
if (strncmp(flag->name, "Luau", 4) == 0)
flag->value = true;
CliMode mode = CliMode::Unknown; CliMode mode = CliMode::Unknown;
CompileFormat compileFormat{}; CompileFormat compileFormat{};
@ -727,6 +775,22 @@ int replMain(int argc, char** argv)
return 1; return 1;
#endif #endif
} }
else if (strncmp(argv[i], "--fflags=", 9) == 0)
{
std::string_view list = argv[i] + 9;
while (!list.empty())
{
size_t ending = list.find(",");
applyFlagKeyValue(list.substr(0, ending));
if (ending != std::string_view::npos)
list.remove_prefix(ending + 1);
else
break;
}
}
else if (argv[i][0] == '-') else if (argv[i][0] == '-')
{ {
fprintf(stderr, "Error: Unrecognized option '%s'.\n\n", argv[i]); fprintf(stderr, "Error: Unrecognized option '%s'.\n\n", argv[i]);

View file

@ -73,6 +73,12 @@ else()
list(APPEND LUAU_OPTIONS -Wall) # All warnings list(APPEND LUAU_OPTIONS -Wall) # All warnings
endif() endif()
if (CMAKE_CXX_COMPILER_ID STREQUAL "GNU")
# Some gcc versions treat var in `if (type var = val)` as unused
# Some gcc versions treat variables used in constexpr if blocks as unused
list(APPEND LUAU_OPTIONS -Wno-unused)
endif()
# Enabled in CI; we should be warning free on our main compiler versions but don't guarantee being warning free everywhere # Enabled in CI; we should be warning free on our main compiler versions but don't guarantee being warning free everywhere
if(LUAU_WERROR) if(LUAU_WERROR)
if(MSVC) if(MSVC)

View file

@ -3,6 +3,7 @@
#include "Luau/Bytecode.h" #include "Luau/Bytecode.h"
#include "Luau/DenseHash.h" #include "Luau/DenseHash.h"
#include "Luau/StringUtils.h"
#include <string> #include <string>
@ -80,6 +81,8 @@ public:
void pushDebugUpval(StringRef name); void pushDebugUpval(StringRef name);
uint32_t getDebugPC() const; uint32_t getDebugPC() const;
void addDebugRemark(const char* format, ...) LUAU_PRINTF_ATTR(2, 3);
void finalize(); void finalize();
enum DumpFlags enum DumpFlags
@ -88,6 +91,7 @@ public:
Dump_Lines = 1 << 1, Dump_Lines = 1 << 1,
Dump_Source = 1 << 2, Dump_Source = 1 << 2,
Dump_Locals = 1 << 3, Dump_Locals = 1 << 3,
Dump_Remarks = 1 << 4,
}; };
void setDumpFlags(uint32_t flags) void setDumpFlags(uint32_t flags)
@ -228,6 +232,9 @@ private:
DenseHashMap<StringRef, unsigned int, StringRefHash> stringTable; DenseHashMap<StringRef, unsigned int, StringRefHash> stringTable;
DenseHashMap<uint32_t, uint32_t> debugRemarks;
std::string debugRemarkBuffer;
BytecodeEncoder* encoder = nullptr; BytecodeEncoder* encoder = nullptr;
std::string bytecode; std::string bytecode;

View file

@ -181,9 +181,17 @@ BytecodeBuilder::BytecodeBuilder(BytecodeEncoder* encoder)
: constantMap({Constant::Type_Nil, ~0ull}) : constantMap({Constant::Type_Nil, ~0ull})
, tableShapeMap(TableShape()) , tableShapeMap(TableShape())
, stringTable({nullptr, 0}) , stringTable({nullptr, 0})
, debugRemarks(~0u)
, encoder(encoder) , encoder(encoder)
{ {
LUAU_ASSERT(stringTable.find(StringRef{"", 0}) == nullptr); LUAU_ASSERT(stringTable.find(StringRef{"", 0}) == nullptr);
// preallocate some buffers that are very likely to grow anyway; this works around std::vector's inefficient growth policy for small arrays
insns.reserve(32);
lines.reserve(32);
constants.reserve(16);
protos.reserve(16);
functions.reserve(8);
} }
uint32_t BytecodeBuilder::beginFunction(uint8_t numparams, bool isvararg) uint32_t BytecodeBuilder::beginFunction(uint8_t numparams, bool isvararg)
@ -219,8 +227,8 @@ void BytecodeBuilder::endFunction(uint8_t maxstacksize, uint8_t numupvalues)
validate(); validate();
#endif #endif
// very approximate: 4 bytes per instruction for code, 1 byte for debug line, and 1-2 bytes for aux data like constants // very approximate: 4 bytes per instruction for code, 1 byte for debug line, and 1-2 bytes for aux data like constants plus overhead
func.data.reserve(insns.size() * 7); func.data.reserve(32 + insns.size() * 7);
writeFunction(func.data, currentFunction); writeFunction(func.data, currentFunction);
@ -242,6 +250,9 @@ void BytecodeBuilder::endFunction(uint8_t maxstacksize, uint8_t numupvalues)
constantMap.clear(); constantMap.clear();
tableShapeMap.clear(); tableShapeMap.clear();
debugRemarks.clear();
debugRemarkBuffer.clear();
} }
void BytecodeBuilder::setMainFunction(uint32_t fid) void BytecodeBuilder::setMainFunction(uint32_t fid)
@ -505,9 +516,40 @@ uint32_t BytecodeBuilder::getDebugPC() const
return uint32_t(insns.size()); return uint32_t(insns.size());
} }
void BytecodeBuilder::addDebugRemark(const char* format, ...)
{
if ((dumpFlags & Dump_Remarks) == 0)
return;
size_t offset = debugRemarkBuffer.size();
va_list args;
va_start(args, format);
vformatAppend(debugRemarkBuffer, format, args);
va_end(args);
// we null-terminate all remarks to avoid storing remark length
debugRemarkBuffer += '\0';
debugRemarks[uint32_t(insns.size())] = uint32_t(offset);
}
void BytecodeBuilder::finalize() void BytecodeBuilder::finalize()
{ {
LUAU_ASSERT(bytecode.empty()); LUAU_ASSERT(bytecode.empty());
// preallocate space for bytecode blob
size_t capacity = 16;
for (auto& p : stringTable)
capacity += p.first.length + 2;
for (const Function& func : functions)
capacity += func.data.size();
bytecode.reserve(capacity);
// assemble final bytecode blob
bytecode = char(LBC_VERSION); bytecode = char(LBC_VERSION);
writeStringTable(bytecode); writeStringTable(bytecode);
@ -663,6 +705,8 @@ void BytecodeBuilder::writeFunction(std::string& ss, uint32_t id) const
void BytecodeBuilder::writeLineInfo(std::string& ss) const void BytecodeBuilder::writeLineInfo(std::string& ss) const
{ {
LUAU_ASSERT(!lines.empty());
// this function encodes lines inside each span as a 8-bit delta to span baseline // this function encodes lines inside each span as a 8-bit delta to span baseline
// span is always a power of two; depending on the line info input, it may need to be as low as 1 // span is always a power of two; depending on the line info input, it may need to be as low as 1
int span = 1 << 24; int span = 1 << 24;
@ -693,7 +737,17 @@ void BytecodeBuilder::writeLineInfo(std::string& ss) const
} }
// second pass: compute span base // second pass: compute span base
std::vector<int> baseline((lines.size() - 1) / span + 1); int baselineOne = 0;
std::vector<int> baselineScratch;
int* baseline = &baselineOne;
size_t baselineSize = (lines.size() - 1) / span + 1;
if (baselineSize > 1)
{
// avoid heap allocation for single-element baseline which is most functions (<256 lines)
baselineScratch.resize(baselineSize);
baseline = baselineScratch.data();
}
for (size_t offset = 0; offset < lines.size(); offset += span) for (size_t offset = 0; offset < lines.size(); offset += span)
{ {
@ -725,7 +779,7 @@ void BytecodeBuilder::writeLineInfo(std::string& ss) const
int lastLine = 0; int lastLine = 0;
for (size_t i = 0; i < baseline.size(); ++i) for (size_t i = 0; i < baselineSize; ++i)
{ {
writeInt(ss, baseline[i] - lastLine); writeInt(ss, baseline[i] - lastLine);
lastLine = baseline[i]; lastLine = baseline[i];
@ -1695,6 +1749,14 @@ std::string BytecodeBuilder::dumpCurrentFunction() const
continue; continue;
} }
if (dumpFlags & Dump_Remarks)
{
const uint32_t* remark = debugRemarks.find(uint32_t(code - insns.data()));
if (remark)
formatAppend(result, "REMARK %s\n", debugRemarkBuffer.c_str() + *remark);
}
if (dumpFlags & Dump_Source) if (dumpFlags & Dump_Source)
{ {
int line = lines[code - insns.data()]; int line = lines[code - insns.data()];

View file

@ -8,12 +8,17 @@
#include "Builtins.h" #include "Builtins.h"
#include "ConstantFolding.h" #include "ConstantFolding.h"
#include "CostModel.h"
#include "TableShape.h" #include "TableShape.h"
#include "ValueTracking.h" #include "ValueTracking.h"
#include <algorithm> #include <algorithm>
#include <bitset> #include <bitset>
#include <math.h> #include <math.h>
#include <limits.h>
LUAU_FASTINTVARIABLE(LuauCompileLoopUnrollThreshold, 25)
LUAU_FASTINTVARIABLE(LuauCompileLoopUnrollThresholdMaxBoost, 300)
namespace Luau namespace Luau
{ {
@ -77,8 +82,12 @@ struct Compiler
, globals(AstName()) , globals(AstName())
, variables(nullptr) , variables(nullptr)
, constants(nullptr) , constants(nullptr)
, locstants(nullptr)
, tableShapes(nullptr) , tableShapes(nullptr)
{ {
// preallocate some buffers that are very likely to grow anyway; this works around std::vector's inefficient growth policy for small arrays
localStack.reserve(16);
upvals.reserve(16);
} }
uint8_t getLocal(AstLocal* local) uint8_t getLocal(AstLocal* local)
@ -209,7 +218,9 @@ struct Compiler
Function& f = functions[func]; Function& f = functions[func];
f.id = fid; f.id = fid;
f.upvals = std::move(upvals); f.upvals = upvals;
upvals.clear(); // note: instead of std::move above, we copy & clear to preserve capacity for future pushes
return fid; return fid;
} }
@ -2133,10 +2144,119 @@ struct Compiler
pushLocal(stat->vars.data[i], uint8_t(vars + i)); pushLocal(stat->vars.data[i], uint8_t(vars + i));
} }
int getConstantShort(AstExpr* expr)
{
const Constant* c = constants.find(expr);
if (c && c->type == Constant::Type_Number)
{
double n = c->valueNumber;
if (n >= -32767 && n <= 32767 && double(int(n)) == n)
return int(n);
}
return INT_MIN;
}
bool canUnrollForBody(AstStatFor* stat)
{
struct CanUnrollVisitor : AstVisitor
{
bool result = true;
bool visit(AstExpr* node) override
{
// functions may capture loop variable, and our upval handling doesn't handle elided variables (constant)
result = result && !node->is<AstExprFunction>();
return result;
}
bool visit(AstStat* node) override
{
// while we can easily unroll nested loops, our cost model doesn't take unrolling into account so this can result in code explosion
// we also avoid continue/break since they introduce control flow across iterations
result = result && !node->is<AstStatFor>() && !node->is<AstStatContinue>() && !node->is<AstStatBreak>();
return result;
}
};
CanUnrollVisitor canUnroll;
stat->body->visit(&canUnroll);
return canUnroll.result;
}
bool tryCompileUnrolledFor(AstStatFor* stat, int thresholdBase, int thresholdMaxBoost)
{
int from = getConstantShort(stat->from);
int to = getConstantShort(stat->to);
int step = stat->step ? getConstantShort(stat->step) : 1;
// check that limits are reasonably small and trip count can be computed
if (from == INT_MIN || to == INT_MIN || step == INT_MIN || step == 0 || (step < 0 && to > from) || (step > 0 && to < from))
{
bytecode.addDebugRemark("loop unroll failed: invalid iteration count");
return false;
}
if (!canUnrollForBody(stat))
{
bytecode.addDebugRemark("loop unroll failed: unsupported loop body");
return false;
}
int tripCount = (to - from) / step + 1;
if (tripCount > thresholdBase * thresholdMaxBoost / 100)
{
bytecode.addDebugRemark("loop unroll failed: too many iterations (%d)", tripCount);
return false;
}
AstLocal* var = stat->var;
uint64_t costModel = modelCost(stat->body, &var, 1);
// we use a dynamic cost threshold that's based on the fixed limit boosted by the cost advantage we gain due to unrolling
bool varc = true;
int unrolledCost = computeCost(costModel, &varc, 1) * tripCount;
int baselineCost = (computeCost(costModel, nullptr, 0) + 1) * tripCount;
int unrollProfit = (unrolledCost == 0) ? thresholdMaxBoost : std::min(thresholdMaxBoost, 100 * baselineCost / unrolledCost);
int threshold = thresholdBase * unrollProfit / 100;
if (unrolledCost > threshold)
{
bytecode.addDebugRemark(
"loop unroll failed: too expensive (iterations %d, cost %d, profit %.2fx)", tripCount, unrolledCost, double(unrollProfit) / 100);
return false;
}
bytecode.addDebugRemark("loop unroll succeeded (iterations %d, cost %d, profit %.2fx)", tripCount, unrolledCost, double(unrollProfit) / 100);
for (int i = from; step > 0 ? i <= to : i >= to; i += step)
{
// we need to re-fold constants in the loop body with the new value; this reuses computed constant values elsewhere in the tree
locstants[var].type = Constant::Type_Number;
locstants[var].valueNumber = i;
foldConstants(constants, variables, locstants, stat);
compileStat(stat->body);
}
return true;
}
void compileStatFor(AstStatFor* stat) void compileStatFor(AstStatFor* stat)
{ {
RegScope rs(this); RegScope rs(this);
// Optimization: small loops can be unrolled when it is profitable
if (options.optimizationLevel >= 2 && isConstant(stat->to) && isConstant(stat->from) && (!stat->step || isConstant(stat->step)))
if (tryCompileUnrolledFor(stat, FInt::LuauCompileLoopUnrollThreshold, FInt::LuauCompileLoopUnrollThresholdMaxBoost))
return;
size_t oldLocals = localStack.size(); size_t oldLocals = localStack.size();
size_t oldJumps = loopJumps.size(); size_t oldJumps = loopJumps.size();
@ -2826,6 +2946,8 @@ struct Compiler
: self(self) : self(self)
, functions(functions) , functions(functions)
{ {
// preallocate the result; this works around std::vector's inefficient growth policy for small arrays
functions.reserve(16);
} }
bool visit(AstExprFunction* node) override bool visit(AstExprFunction* node) override
@ -2979,6 +3101,7 @@ struct Compiler
DenseHashMap<AstName, Global> globals; DenseHashMap<AstName, Global> globals;
DenseHashMap<AstLocal*, Variable> variables; DenseHashMap<AstLocal*, Variable> variables;
DenseHashMap<AstExpr*, Constant> constants; DenseHashMap<AstExpr*, Constant> constants;
DenseHashMap<AstLocal*, Constant> locstants;
DenseHashMap<AstExprTable*, TableShape> tableShapes; DenseHashMap<AstExprTable*, TableShape> tableShapes;
unsigned int regTop = 0; unsigned int regTop = 0;
@ -3008,7 +3131,7 @@ void compileOrThrow(BytecodeBuilder& bytecode, AstStatBlock* root, const AstName
if (options.optimizationLevel >= 1) if (options.optimizationLevel >= 1)
{ {
// this pass analyzes constantness of expressions // this pass analyzes constantness of expressions
foldConstants(compiler.constants, compiler.variables, root); foldConstants(compiler.constants, compiler.variables, compiler.locstants, root);
// this pass analyzes table assignments to estimate table shapes for initially empty tables // this pass analyzes table assignments to estimate table shapes for initially empty tables
predictTableShapes(compiler.tableShapes, root); predictTableShapes(compiler.tableShapes, root);

View file

@ -191,13 +191,13 @@ struct ConstantVisitor : AstVisitor
{ {
DenseHashMap<AstExpr*, Constant>& constants; DenseHashMap<AstExpr*, Constant>& constants;
DenseHashMap<AstLocal*, Variable>& variables; DenseHashMap<AstLocal*, Variable>& variables;
DenseHashMap<AstLocal*, Constant>& locals;
DenseHashMap<AstLocal*, Constant> locals; ConstantVisitor(
DenseHashMap<AstExpr*, Constant>& constants, DenseHashMap<AstLocal*, Variable>& variables, DenseHashMap<AstLocal*, Constant>& locals)
ConstantVisitor(DenseHashMap<AstExpr*, Constant>& constants, DenseHashMap<AstLocal*, Variable>& variables)
: constants(constants) : constants(constants)
, variables(variables) , variables(variables)
, locals(nullptr) , locals(locals)
{ {
} }
@ -385,9 +385,10 @@ struct ConstantVisitor : AstVisitor
} }
}; };
void foldConstants(DenseHashMap<AstExpr*, Constant>& constants, DenseHashMap<AstLocal*, Variable>& variables, AstNode* root) void foldConstants(DenseHashMap<AstExpr*, Constant>& constants, DenseHashMap<AstLocal*, Variable>& variables,
DenseHashMap<AstLocal*, Constant>& locals, AstNode* root)
{ {
ConstantVisitor visitor{constants, variables}; ConstantVisitor visitor{constants, variables, locals};
root->visit(&visitor); root->visit(&visitor);
} }

View file

@ -42,7 +42,8 @@ struct Constant
} }
}; };
void foldConstants(DenseHashMap<AstExpr*, Constant>& constants, DenseHashMap<AstLocal*, Variable>& variables, AstNode* root); void foldConstants(DenseHashMap<AstExpr*, Constant>& constants, DenseHashMap<AstLocal*, Variable>& variables,
DenseHashMap<AstLocal*, Constant>& locals, AstNode* root);
} // namespace Compile } // namespace Compile
} // namespace Luau } // namespace Luau

View file

@ -14,6 +14,8 @@
#include <string.h> #include <string.h>
LUAU_FASTFLAG(LuauGcWorkTrackFix)
const char* lua_ident = "$Lua: Lua 5.1.4 Copyright (C) 1994-2008 Lua.org, PUC-Rio $\n" const char* lua_ident = "$Lua: Lua 5.1.4 Copyright (C) 1994-2008 Lua.org, PUC-Rio $\n"
"$Authors: R. Ierusalimschy, L. H. de Figueiredo & W. Celes $\n" "$Authors: R. Ierusalimschy, L. H. de Figueiredo & W. Celes $\n"
"$URL: www.lua.org $\n"; "$URL: www.lua.org $\n";
@ -1050,6 +1052,7 @@ int lua_gc(lua_State* L, int what, int data)
{ {
size_t prevthreshold = g->GCthreshold; size_t prevthreshold = g->GCthreshold;
size_t amount = (cast_to(size_t, data) << 10); size_t amount = (cast_to(size_t, data) << 10);
ptrdiff_t oldcredit = g->gcstate == GCSpause ? 0 : g->GCthreshold - g->totalbytes;
// temporarily adjust the threshold so that we can perform GC work // temporarily adjust the threshold so that we can perform GC work
if (amount <= g->totalbytes) if (amount <= g->totalbytes)
@ -1069,9 +1072,9 @@ int lua_gc(lua_State* L, int what, int data)
while (g->GCthreshold <= g->totalbytes) while (g->GCthreshold <= g->totalbytes)
{ {
luaC_step(L, false); size_t stepsize = luaC_step(L, false);
actualwork += g->gcstepsize; actualwork += FFlag::LuauGcWorkTrackFix ? stepsize : g->gcstepsize;
if (g->gcstate == GCSpause) if (g->gcstate == GCSpause)
{ /* end of cycle? */ { /* end of cycle? */
@ -1107,11 +1110,20 @@ int lua_gc(lua_State* L, int what, int data)
// if cycle hasn't finished, advance threshold forward for the amount of extra work performed // if cycle hasn't finished, advance threshold forward for the amount of extra work performed
if (g->gcstate != GCSpause) if (g->gcstate != GCSpause)
{ {
// if a new cycle was triggered by explicit step, we ignore old threshold as that shows an incorrect 'credit' of GC work if (FFlag::LuauGcWorkTrackFix)
if (waspaused) {
g->GCthreshold = g->totalbytes + actualwork; // if a new cycle was triggered by explicit step, old 'credit' of GC work is 0
ptrdiff_t newthreshold = g->totalbytes + actualwork + oldcredit;
g->GCthreshold = newthreshold < 0 ? 0 : newthreshold;
}
else else
g->GCthreshold = prevthreshold + actualwork; {
// if a new cycle was triggered by explicit step, we ignore old threshold as that shows an incorrect 'credit' of GC work
if (waspaused)
g->GCthreshold = g->totalbytes + actualwork;
else
g->GCthreshold = prevthreshold + actualwork;
}
} }
break; break;
} }

View file

@ -13,9 +13,10 @@
#include <string.h> #include <string.h>
#define GC_SWEEPMAX 40 LUAU_FASTFLAGVARIABLE(LuauGcWorkTrackFix, false)
#define GC_SWEEPCOST 10 LUAU_FASTFLAGVARIABLE(LuauGcSweepCostFix, false)
#define GC_SWEEPPAGESTEPCOST 4
#define GC_SWEEPPAGESTEPCOST (FFlag::LuauGcSweepCostFix ? 16 : 4)
#define GC_INTERRUPT(state) \ #define GC_INTERRUPT(state) \
{ \ { \
@ -64,7 +65,7 @@ static void recordGcStateStep(global_State* g, int startgcstate, double seconds,
case GCSpropagate: case GCSpropagate:
case GCSpropagateagain: case GCSpropagateagain:
g->gcmetrics.currcycle.marktime += seconds; g->gcmetrics.currcycle.marktime += seconds;
g->gcmetrics.currcycle.markrequests += g->gcstepsize; g->gcmetrics.currcycle.markwork += work;
if (assist) if (assist)
g->gcmetrics.currcycle.markassisttime += seconds; g->gcmetrics.currcycle.markassisttime += seconds;
@ -74,7 +75,7 @@ static void recordGcStateStep(global_State* g, int startgcstate, double seconds,
break; break;
case GCSsweep: case GCSsweep:
g->gcmetrics.currcycle.sweeptime += seconds; g->gcmetrics.currcycle.sweeptime += seconds;
g->gcmetrics.currcycle.sweeprequests += g->gcstepsize; g->gcmetrics.currcycle.sweepwork += work;
if (assist) if (assist)
g->gcmetrics.currcycle.sweepassisttime += seconds; g->gcmetrics.currcycle.sweepassisttime += seconds;
@ -87,13 +88,11 @@ static void recordGcStateStep(global_State* g, int startgcstate, double seconds,
{ {
g->gcmetrics.stepassisttimeacc += seconds; g->gcmetrics.stepassisttimeacc += seconds;
g->gcmetrics.currcycle.assistwork += work; g->gcmetrics.currcycle.assistwork += work;
g->gcmetrics.currcycle.assistrequests += g->gcstepsize;
} }
else else
{ {
g->gcmetrics.stepexplicittimeacc += seconds; g->gcmetrics.stepexplicittimeacc += seconds;
g->gcmetrics.currcycle.explicitwork += work; g->gcmetrics.currcycle.explicitwork += work;
g->gcmetrics.currcycle.explicitrequests += g->gcstepsize;
} }
} }
@ -878,11 +877,11 @@ static size_t getheaptrigger(global_State* g, size_t heapgoal)
return heaptrigger < int64_t(g->totalbytes) ? g->totalbytes : (heaptrigger > int64_t(heapgoal) ? heapgoal : size_t(heaptrigger)); return heaptrigger < int64_t(g->totalbytes) ? g->totalbytes : (heaptrigger > int64_t(heapgoal) ? heapgoal : size_t(heaptrigger));
} }
void luaC_step(lua_State* L, bool assist) size_t luaC_step(lua_State* L, bool assist)
{ {
global_State* g = L->global; global_State* g = L->global;
int lim = (g->gcstepsize / 100) * g->gcstepmul; /* how much to work */ int lim = FFlag::LuauGcWorkTrackFix ? g->gcstepsize * g->gcstepmul / 100 : (g->gcstepsize / 100) * g->gcstepmul; /* how much to work */
LUAU_ASSERT(g->totalbytes >= g->GCthreshold); LUAU_ASSERT(g->totalbytes >= g->GCthreshold);
size_t debt = g->totalbytes - g->GCthreshold; size_t debt = g->totalbytes - g->GCthreshold;
@ -902,12 +901,13 @@ void luaC_step(lua_State* L, bool assist)
int lastgcstate = g->gcstate; int lastgcstate = g->gcstate;
size_t work = gcstep(L, lim); size_t work = gcstep(L, lim);
(void)work;
#ifdef LUAI_GCMETRICS #ifdef LUAI_GCMETRICS
recordGcStateStep(g, lastgcstate, lua_clock() - lasttimestamp, assist, work); recordGcStateStep(g, lastgcstate, lua_clock() - lasttimestamp, assist, work);
#endif #endif
size_t actualstepsize = work * 100 / g->gcstepmul;
// at the end of the last cycle // at the end of the last cycle
if (g->gcstate == GCSpause) if (g->gcstate == GCSpause)
{ {
@ -927,14 +927,16 @@ void luaC_step(lua_State* L, bool assist)
} }
else else
{ {
g->GCthreshold = g->totalbytes + g->gcstepsize; g->GCthreshold = g->totalbytes + (FFlag::LuauGcWorkTrackFix ? actualstepsize : g->gcstepsize);
// compensate if GC is "behind schedule" (has some debt to pay) // compensate if GC is "behind schedule" (has some debt to pay)
if (g->GCthreshold > debt) if (FFlag::LuauGcWorkTrackFix ? g->GCthreshold >= debt : g->GCthreshold > debt)
g->GCthreshold -= debt; g->GCthreshold -= debt;
} }
GC_INTERRUPT(lastgcstate); GC_INTERRUPT(lastgcstate);
return actualstepsize;
} }
void luaC_fullgc(lua_State* L) void luaC_fullgc(lua_State* L)

View file

@ -133,7 +133,7 @@
#define luaC_init(L, o, tt) luaC_initobj(L, cast_to(GCObject*, (o)), tt) #define luaC_init(L, o, tt) luaC_initobj(L, cast_to(GCObject*, (o)), tt)
LUAI_FUNC void luaC_freeall(lua_State* L); LUAI_FUNC void luaC_freeall(lua_State* L);
LUAI_FUNC void luaC_step(lua_State* L, bool assist); LUAI_FUNC size_t luaC_step(lua_State* L, bool assist);
LUAI_FUNC void luaC_fullgc(lua_State* L); LUAI_FUNC void luaC_fullgc(lua_State* L);
LUAI_FUNC void luaC_initobj(lua_State* L, GCObject* o, uint8_t tt); LUAI_FUNC void luaC_initobj(lua_State* L, GCObject* o, uint8_t tt);
LUAI_FUNC void luaC_initupval(lua_State* L, UpVal* uv); LUAI_FUNC void luaC_initupval(lua_State* L, UpVal* uv);

View file

@ -106,7 +106,7 @@ struct GCCycleMetrics
double markassisttime = 0.0; double markassisttime = 0.0;
double markmaxexplicittime = 0.0; double markmaxexplicittime = 0.0;
size_t markexplicitsteps = 0; size_t markexplicitsteps = 0;
size_t markrequests = 0; size_t markwork = 0;
double atomicstarttimestamp = 0.0; double atomicstarttimestamp = 0.0;
size_t atomicstarttotalsizebytes = 0; size_t atomicstarttotalsizebytes = 0;
@ -122,10 +122,7 @@ struct GCCycleMetrics
double sweepassisttime = 0.0; double sweepassisttime = 0.0;
double sweepmaxexplicittime = 0.0; double sweepmaxexplicittime = 0.0;
size_t sweepexplicitsteps = 0; size_t sweepexplicitsteps = 0;
size_t sweeprequests = 0; size_t sweepwork = 0;
size_t assistrequests = 0;
size_t explicitrequests = 0;
size_t assistwork = 0; size_t assistwork = 0;
size_t explicitwork = 0; size_t explicitwork = 0;

View file

@ -814,13 +814,12 @@ def run(args, argsubcb):
analyzeResult('', mainResult, compareResults) analyzeResult('', mainResult, compareResults)
else: else:
for subdir, dirs, files in os.walk(arguments.folder): all_files = [subdir + os.sep + filename for subdir, dirs, files in os.walk(arguments.folder) for filename in files]
for filename in files: for filepath in sorted(all_files):
filepath = subdir + os.sep + filename subdir, filename = os.path.split(filepath)
if filename.endswith(".lua"):
if filename.endswith(".lua"): if arguments.run_test == None or re.match(arguments.run_test, filename[:-4]):
if arguments.run_test == None or re.match(arguments.run_test, filename[:-4]): runTest(subdir, filename, filepath)
runTest(subdir, filename, filepath)
if arguments.sort and len(plotValueLists) > 1: if arguments.sort and len(plotValueLists) > 1:
rearrange(rearrangeSortKeyForComparison) rearrange(rearrangeSortKeyForComparison)

View file

@ -103,7 +103,7 @@ int registerTypes(Luau::TypeChecker& env)
// Vector3 stub // Vector3 stub
TypeId vector3MetaType = arena.addType(TableTypeVar{}); TypeId vector3MetaType = arena.addType(TableTypeVar{});
TypeId vector3InstanceType = arena.addType(ClassTypeVar{"Vector3", {}, nullopt, vector3MetaType, {}, {}}); TypeId vector3InstanceType = arena.addType(ClassTypeVar{"Vector3", {}, nullopt, vector3MetaType, {}, {}, "Test"});
getMutable<ClassTypeVar>(vector3InstanceType)->props = { getMutable<ClassTypeVar>(vector3InstanceType)->props = {
{"X", {env.numberType}}, {"X", {env.numberType}},
{"Y", {env.numberType}}, {"Y", {env.numberType}},
@ -117,7 +117,7 @@ int registerTypes(Luau::TypeChecker& env)
env.globalScope->exportedTypeBindings["Vector3"] = TypeFun{{}, vector3InstanceType}; env.globalScope->exportedTypeBindings["Vector3"] = TypeFun{{}, vector3InstanceType};
// Instance stub // Instance stub
TypeId instanceType = arena.addType(ClassTypeVar{"Instance", {}, nullopt, nullopt, {}, {}}); TypeId instanceType = arena.addType(ClassTypeVar{"Instance", {}, nullopt, nullopt, {}, {}, "Test"});
getMutable<ClassTypeVar>(instanceType)->props = { getMutable<ClassTypeVar>(instanceType)->props = {
{"Name", {env.stringType}}, {"Name", {env.stringType}},
}; };
@ -125,7 +125,7 @@ int registerTypes(Luau::TypeChecker& env)
env.globalScope->exportedTypeBindings["Instance"] = TypeFun{{}, instanceType}; env.globalScope->exportedTypeBindings["Instance"] = TypeFun{{}, instanceType};
// Part stub // Part stub
TypeId partType = arena.addType(ClassTypeVar{"Part", {}, instanceType, nullopt, {}, {}}); TypeId partType = arena.addType(ClassTypeVar{"Part", {}, instanceType, nullopt, {}, {}, "Test"});
getMutable<ClassTypeVar>(partType)->props = { getMutable<ClassTypeVar>(partType)->props = {
{"Position", {vector3InstanceType}}, {"Position", {vector3InstanceType}},
}; };
@ -173,7 +173,7 @@ struct FuzzConfigResolver : Luau::ConfigResolver
{ {
FuzzConfigResolver() FuzzConfigResolver()
{ {
defaultConfig.mode = Luau::Mode::Nonstrict; // typecheckTwice option will cover Strict mode defaultConfig.mode = Luau::Mode::Nonstrict;
defaultConfig.enabledLint.warningMask = ~0ull; defaultConfig.enabledLint.warningMask = ~0ull;
defaultConfig.parseOptions.captureComments = true; defaultConfig.parseOptions.captureComments = true;
} }
@ -275,6 +275,11 @@ DEFINE_PROTO_FUZZER(const luau::ModuleSet& message)
// lint (note that we need access to types so we need to do this with typeck in scope) // lint (note that we need access to types so we need to do this with typeck in scope)
if (kFuzzLinter && result.errors.empty()) if (kFuzzLinter && result.errors.empty())
frontend.lint(name, std::nullopt); frontend.lint(name, std::nullopt);
// Second pass in strict mode (forced by auto-complete)
Luau::FrontendOptions opts;
opts.forAutocomplete = true;
frontend.check(name, opts);
} }
catch (std::exception&) catch (std::exception&)
{ {

View file

@ -3034,4 +3034,40 @@ string:@1
CHECK(ac.entryMap["sub"].wrongIndexType == true); CHECK(ac.entryMap["sub"].wrongIndexType == true);
} }
TEST_CASE_FIXTURE(ACFixture, "source_module_preservation_and_invalidation")
{
check(R"(
local a = { x = 2, y = 4 }
a.@1
)");
frontend.clear();
auto ac = autocomplete('1');
CHECK(ac.entryMap.count("x"));
CHECK(ac.entryMap.count("y"));
frontend.check("MainModule", {});
ac = autocomplete('1');
CHECK(ac.entryMap.count("x"));
CHECK(ac.entryMap.count("y"));
frontend.markDirty("MainModule", nullptr);
ac = autocomplete('1');
CHECK(ac.entryMap.count("x"));
CHECK(ac.entryMap.count("y"));
frontend.check("MainModule", {});
ac = autocomplete('1');
CHECK(ac.entryMap.count("x"));
CHECK(ac.entryMap.count("y"));
}
TEST_SUITE_END(); TEST_SUITE_END();

View file

@ -17,11 +17,13 @@ std::string rep(const std::string& s, size_t n);
using namespace Luau; using namespace Luau;
static std::string compileFunction(const char* source, uint32_t id) static std::string compileFunction(const char* source, uint32_t id, int optimizationLevel = 1)
{ {
Luau::BytecodeBuilder bcb; Luau::BytecodeBuilder bcb;
bcb.setDumpFlags(Luau::BytecodeBuilder::Dump_Code); bcb.setDumpFlags(Luau::BytecodeBuilder::Dump_Code);
Luau::compileOrThrow(bcb, source); Luau::CompileOptions options;
options.optimizationLevel = optimizationLevel;
Luau::compileOrThrow(bcb, source, options);
return bcb.dumpFunction(id); return bcb.dumpFunction(id);
} }
@ -2689,6 +2691,27 @@ local 8: reg 3, start pc 34 line 21, end pc 34 line 21
)"); )");
} }
TEST_CASE("DebugRemarks")
{
Luau::BytecodeBuilder bcb;
bcb.setDumpFlags(Luau::BytecodeBuilder::Dump_Code | Luau::BytecodeBuilder::Dump_Remarks);
uint32_t fid = bcb.beginFunction(0);
bcb.addDebugRemark("test remark #%d", 42);
bcb.emitABC(LOP_RETURN, 0, 1, 0);
bcb.endFunction(0, 0);
bcb.setMainFunction(fid);
bcb.finalize();
CHECK_EQ("\n" + bcb.dumpFunction(0), R"(
REMARK test remark #42
RETURN R0 0
)");
}
TEST_CASE("AssignmentConflict") TEST_CASE("AssignmentConflict")
{ {
// assignments are left to right // assignments are left to right
@ -4076,4 +4099,336 @@ RETURN R1 6
)"); )");
} }
TEST_CASE("LoopUnrollBasic")
{
// forward loops
CHECK_EQ("\n" + compileFunction(R"(
local t = {}
for i=1,2 do
t[i] = i
end
return t
)",
0, 2),
R"(
NEWTABLE R0 0 2
LOADN R1 1
SETTABLEN R1 R0 1
LOADN R1 2
SETTABLEN R1 R0 2
RETURN R0 1
)");
// backward loops
CHECK_EQ("\n" + compileFunction(R"(
local t = {}
for i=2,1,-1 do
t[i] = i
end
return t
)",
0, 2),
R"(
NEWTABLE R0 0 0
LOADN R1 2
SETTABLEN R1 R0 2
LOADN R1 1
SETTABLEN R1 R0 1
RETURN R0 1
)");
// loops with step that doesn't divide to-from
CHECK_EQ("\n" + compileFunction(R"(
local t = {}
for i=1,4,2 do
t[i] = i
end
return t
)",
0, 2),
R"(
NEWTABLE R0 0 0
LOADN R1 1
SETTABLEN R1 R0 1
LOADN R1 3
SETTABLEN R1 R0 3
RETURN R0 1
)");
}
TEST_CASE("LoopUnrollUnsupported")
{
// can't unroll loops with non-constant bounds
CHECK_EQ("\n" + compileFunction(R"(
for i=x,y,z do
end
)",
0, 2),
R"(
GETIMPORT R2 1
GETIMPORT R0 3
GETIMPORT R1 5
FORNPREP R0 +1
FORNLOOP R0 -1
RETURN R0 0
)");
// can't unroll loops with bounds where we can't compute trip count
CHECK_EQ("\n" + compileFunction(R"(
for i=2,1 do
end
)",
0, 2),
R"(
LOADN R2 2
LOADN R0 1
LOADN R1 1
FORNPREP R0 +1
FORNLOOP R0 -1
RETURN R0 0
)");
// can't unroll loops with bounds that might be imprecise (non-integer)
CHECK_EQ("\n" + compileFunction(R"(
for i=1,2,0.1 do
end
)",
0, 2),
R"(
LOADN R2 1
LOADN R0 2
LOADK R1 K0
FORNPREP R0 +1
FORNLOOP R0 -1
RETURN R0 0
)");
// can't unroll loops if the bounds are too large, as it might overflow trip count math
CHECK_EQ("\n" + compileFunction(R"(
for i=4294967295,4294967296 do
end
)",
0, 2),
R"(
LOADK R2 K0
LOADK R0 K1
LOADN R1 1
FORNPREP R0 +1
FORNLOOP R0 -1
RETURN R0 0
)");
// can't unroll loops if the body has loop control flow or nested loops
CHECK_EQ("\n" + compileFunction(R"(
for i=1,1 do
for j=1,1 do
if i == 1 then
continue
else
break
end
end
end
)",
0, 2),
R"(
LOADN R2 1
LOADN R0 1
LOADN R1 1
FORNPREP R0 +11
LOADN R5 1
LOADN R3 1
LOADN R4 1
FORNPREP R3 +6
JUMPIFNOTEQK R2 K0 +5
JUMP +2
JUMP +1
JUMP +1
FORNLOOP R3 -6
FORNLOOP R0 -11
RETURN R0 0
)");
// can't unroll loops if the body has functions that refer to loop variables
CHECK_EQ("\n" + compileFunction(R"(
for i=1,1 do
local x = function() return i end
end
)",
1, 2),
R"(
LOADN R2 1
LOADN R0 1
LOADN R1 1
FORNPREP R0 +3
NEWCLOSURE R3 P0
CAPTURE VAL R2
FORNLOOP R0 -3
RETURN R0 0
)");
}
TEST_CASE("LoopUnrollCost")
{
ScopedFastInt sfis[] = {
{"LuauCompileLoopUnrollThreshold", 25},
{"LuauCompileLoopUnrollThresholdMaxBoost", 300},
};
// loops with short body
CHECK_EQ("\n" + compileFunction(R"(
local t = {}
for i=1,10 do
t[i] = i
end
return t
)",
0, 2),
R"(
NEWTABLE R0 0 10
LOADN R1 1
SETTABLEN R1 R0 1
LOADN R1 2
SETTABLEN R1 R0 2
LOADN R1 3
SETTABLEN R1 R0 3
LOADN R1 4
SETTABLEN R1 R0 4
LOADN R1 5
SETTABLEN R1 R0 5
LOADN R1 6
SETTABLEN R1 R0 6
LOADN R1 7
SETTABLEN R1 R0 7
LOADN R1 8
SETTABLEN R1 R0 8
LOADN R1 9
SETTABLEN R1 R0 9
LOADN R1 10
SETTABLEN R1 R0 10
RETURN R0 1
)");
// loops with body that's too long
CHECK_EQ("\n" + compileFunction(R"(
local t = {}
for i=1,100 do
t[i] = i
end
return t
)",
0, 2),
R"(
NEWTABLE R0 0 0
LOADN R3 1
LOADN R1 100
LOADN R2 1
FORNPREP R1 +2
SETTABLE R3 R0 R3
FORNLOOP R1 -2
RETURN R0 1
)");
// loops with body that's long but has a high boost factor due to constant folding
CHECK_EQ("\n" + compileFunction(R"(
local t = {}
for i=1,30 do
t[i] = i * i * i
end
return t
)",
0, 2),
R"(
NEWTABLE R0 0 0
LOADN R1 1
SETTABLEN R1 R0 1
LOADN R1 8
SETTABLEN R1 R0 2
LOADN R1 27
SETTABLEN R1 R0 3
LOADN R1 64
SETTABLEN R1 R0 4
LOADN R1 125
SETTABLEN R1 R0 5
LOADN R1 216
SETTABLEN R1 R0 6
LOADN R1 343
SETTABLEN R1 R0 7
LOADN R1 512
SETTABLEN R1 R0 8
LOADN R1 729
SETTABLEN R1 R0 9
LOADN R1 1000
SETTABLEN R1 R0 10
LOADN R1 1331
SETTABLEN R1 R0 11
LOADN R1 1728
SETTABLEN R1 R0 12
LOADN R1 2197
SETTABLEN R1 R0 13
LOADN R1 2744
SETTABLEN R1 R0 14
LOADN R1 3375
SETTABLEN R1 R0 15
LOADN R1 4096
SETTABLEN R1 R0 16
LOADN R1 4913
SETTABLEN R1 R0 17
LOADN R1 5832
SETTABLEN R1 R0 18
LOADN R1 6859
SETTABLEN R1 R0 19
LOADN R1 8000
SETTABLEN R1 R0 20
LOADN R1 9261
SETTABLEN R1 R0 21
LOADN R1 10648
SETTABLEN R1 R0 22
LOADN R1 12167
SETTABLEN R1 R0 23
LOADN R1 13824
SETTABLEN R1 R0 24
LOADN R1 15625
SETTABLEN R1 R0 25
LOADN R1 17576
SETTABLEN R1 R0 26
LOADN R1 19683
SETTABLEN R1 R0 27
LOADN R1 21952
SETTABLEN R1 R0 28
LOADN R1 24389
SETTABLEN R1 R0 29
LOADN R1 27000
SETTABLEN R1 R0 30
RETURN R0 1
)");
// loops with body that's long and doesn't have a high boost factor
CHECK_EQ("\n" + compileFunction(R"(
local t = {}
for i=1,10 do
t[i] = math.abs(math.sin(i))
end
return t
)",
0, 2),
R"(
NEWTABLE R0 0 10
LOADN R3 1
LOADN R1 10
LOADN R2 1
FORNPREP R1 +11
FASTCALL1 24 R3 +3
MOVE R6 R3
GETIMPORT R5 2
CALL R5 1 -1
FASTCALL 2 +2
GETIMPORT R4 4
CALL R4 -1 1
SETTABLE R4 R0 R3
FORNLOOP R1 -11
RETURN R0 1
)");
}
TEST_SUITE_END(); TEST_SUITE_END();

View file

@ -98,4 +98,129 @@ end
CHECK_EQ(2, Luau::Compile::computeCost(model, args2, 1)); CHECK_EQ(2, Luau::Compile::computeCost(model, args2, 1));
} }
TEST_CASE("ImportCall")
{
uint64_t model = modelFunction(R"(
function test(a)
return Instance.new(a)
end
)");
const bool args1[] = {false};
const bool args2[] = {true};
CHECK_EQ(6, Luau::Compile::computeCost(model, args1, 1));
CHECK_EQ(6, Luau::Compile::computeCost(model, args2, 1));
}
TEST_CASE("FastCall")
{
uint64_t model = modelFunction(R"(
function test(a)
return math.abs(a + 1)
end
)");
const bool args1[] = {false};
const bool args2[] = {true};
// note: we currently don't treat fast calls differently from cost model perspective
CHECK_EQ(6, Luau::Compile::computeCost(model, args1, 1));
CHECK_EQ(5, Luau::Compile::computeCost(model, args2, 1));
}
TEST_CASE("ControlFlow")
{
uint64_t model = modelFunction(R"(
function test(a)
while a < 0 do
a += 1
end
for i=1,2 do
a += 1
end
for i in pairs({}) do
a += 1
if a % 2 == 0 then continue end
end
repeat
a += 1
if a % 2 == 0 then break end
until a > 10
return a
end
)");
const bool args1[] = {false};
const bool args2[] = {true};
CHECK_EQ(38, Luau::Compile::computeCost(model, args1, 1));
CHECK_EQ(37, Luau::Compile::computeCost(model, args2, 1));
}
TEST_CASE("Conditional")
{
uint64_t model = modelFunction(R"(
function test(a)
return if a < 0 then -a else a
end
)");
const bool args1[] = {false};
const bool args2[] = {true};
CHECK_EQ(4, Luau::Compile::computeCost(model, args1, 1));
CHECK_EQ(2, Luau::Compile::computeCost(model, args2, 1));
}
TEST_CASE("VarArgs")
{
uint64_t model = modelFunction(R"(
function test(...)
return select('#', ...) :: number
end
)");
CHECK_EQ(8, Luau::Compile::computeCost(model, nullptr, 0));
}
TEST_CASE("TablesFunctions")
{
uint64_t model = modelFunction(R"(
function test()
return { 42, op = function() end }
end
)");
CHECK_EQ(22, Luau::Compile::computeCost(model, nullptr, 0));
}
TEST_CASE("CostOverflow")
{
uint64_t model = modelFunction(R"(
function test()
return {{{{{{{{{{{{{{{}}}}}}}}}}}}}}}
end
)");
CHECK_EQ(127, Luau::Compile::computeCost(model, nullptr, 0));
}
TEST_CASE("TableAssign")
{
uint64_t model = modelFunction(R"(
function test(a)
for i=1,#a do
a[i] = i
end
end
)");
const bool args1[] = {false};
const bool args2[] = {true};
CHECK_EQ(4, Luau::Compile::computeCost(model, args1, 1));
CHECK_EQ(3, Luau::Compile::computeCost(model, args2, 1));
}
TEST_SUITE_END(); TEST_SUITE_END();

View file

@ -9,6 +9,46 @@
using namespace Luau; using namespace Luau;
struct JsonEncoderFixture
{
Allocator allocator;
AstNameTable names{allocator};
ParseResult parse(std::string_view src)
{
ParseOptions opts;
opts.allowDeclarationSyntax = true;
return Parser::parse(src.data(), src.size(), names, allocator, opts);
}
AstStatBlock* expectParse(std::string_view src)
{
ParseResult res = parse(src);
REQUIRE(res.errors.size() == 0);
return res.root;
}
AstStat* expectParseStatement(std::string_view src)
{
AstStatBlock* root = expectParse(src);
REQUIRE(1 == root->body.size);
return root->body.data[0];
}
AstExpr* expectParseExpr(std::string_view src)
{
std::string s = "a = ";
s.append(src);
AstStatBlock* root = expectParse(s);
AstStatAssign* statAssign = root->body.data[0]->as<AstStatAssign>();
REQUIRE(statAssign != nullptr);
REQUIRE(statAssign->values.size == 1);
return statAssign->values.data[0];
}
};
TEST_SUITE_BEGIN("JsonEncoderTests"); TEST_SUITE_BEGIN("JsonEncoderTests");
TEST_CASE("encode_constants") TEST_CASE("encode_constants")
@ -51,7 +91,7 @@ TEST_CASE("encode_AstStatBlock")
toJson(&block)); toJson(&block));
} }
TEST_CASE("encode_tables") TEST_CASE_FIXTURE(JsonEncoderFixture, "encode_tables")
{ {
std::string src = R"( std::string src = R"(
local x: { local x: {
@ -61,16 +101,294 @@ TEST_CASE("encode_tables")
} }
)"; )";
Allocator allocator; AstStatBlock* root = expectParse(src);
AstNameTable names(allocator); std::string json = toJson(root);
ParseResult parseResult = Parser::parse(src.c_str(), src.length(), names, allocator);
REQUIRE(parseResult.errors.size() == 0);
std::string json = toJson(parseResult.root);
CHECK( CHECK(
json == json ==
R"({"type":"AstStatBlock","location":"0,0 - 6,4","body":[{"type":"AstStatLocal","location":"1,8 - 5,9","vars":[{"type":{"type":"AstTypeTable","location":"1,17 - 3,9","props":[{"name":"foo","location":"2,12 - 2,15","type":{"type":"AstTypeReference","location":"2,17 - 2,23","name":"number","parameters":[]}}],"indexer":false},"name":"x","location":"1,14 - 1,15"}],"values":[{"type":"AstExprTable","location":"3,12 - 5,9","items":[{"kind":"record","key":{"type":"AstExprConstantString","location":"4,12 - 4,15","value":"foo"},"value":{"type":"AstExprConstantNumber","location":"4,18 - 4,21","value":123}}]}]}]})"); R"({"type":"AstStatBlock","location":"0,0 - 6,4","body":[{"type":"AstStatLocal","location":"1,8 - 5,9","vars":[{"type":{"type":"AstTypeTable","location":"1,17 - 3,9","props":[{"name":"foo","location":"2,12 - 2,15","type":{"type":"AstTypeReference","location":"2,17 - 2,23","name":"number","parameters":[]}}],"indexer":false},"name":"x","location":"1,14 - 1,15"}],"values":[{"type":"AstExprTable","location":"3,12 - 5,9","items":[{"kind":"record","key":{"type":"AstExprConstantString","location":"4,12 - 4,15","value":"foo"},"value":{"type":"AstExprConstantNumber","location":"4,18 - 4,21","value":123}}]}]}]})");
} }
TEST_CASE("encode_AstExprGroup")
{
AstExprConstantNumber number{Location{}, 5.0};
AstExprGroup group{Location{}, &number};
std::string json = toJson(&group);
const std::string expected = R"({"type":"AstExprGroup","location":"0,0 - 0,0","expr":{"type":"AstExprConstantNumber","location":"0,0 - 0,0","value":5}})";
CHECK(json == expected);
}
TEST_CASE("encode_AstExprGlobal")
{
AstExprGlobal global{Location{}, AstName{"print"}};
std::string json = toJson(&global);
std::string expected = R"({"type":"AstExprGlobal","location":"0,0 - 0,0","global":"print"})";
CHECK(json == expected);
}
TEST_CASE("encode_AstExprLocal")
{
AstLocal local{AstName{"foo"}, Location{}, nullptr, 0, 0, nullptr};
AstExprLocal exprLocal{Location{}, &local, false};
CHECK(toJson(&exprLocal) == R"({"type":"AstExprLocal","location":"0,0 - 0,0","local":{"type":null,"name":"foo","location":"0,0 - 0,0"}})");
}
TEST_CASE("encode_AstExprVarargs")
{
AstExprVarargs varargs{Location{}};
CHECK(toJson(&varargs) == R"({"type":"AstExprVarargs","location":"0,0 - 0,0"})");
}
TEST_CASE_FIXTURE(JsonEncoderFixture, "encode_AstExprCall")
{
AstExpr* expr = expectParseExpr("foo(1, 2, 3)");
std::string_view expected = R"({"type":"AstExprCall","location":"0,4 - 0,16","func":{"type":"AstExprGlobal","location":"0,4 - 0,7","global":"foo"},"args":[{"type":"AstExprConstantNumber","location":"0,8 - 0,9","value":1},{"type":"AstExprConstantNumber","location":"0,11 - 0,12","value":2},{"type":"AstExprConstantNumber","location":"0,14 - 0,15","value":3}],"self":false,"argLocation":"0,8 - 0,16"})";
CHECK(toJson(expr) == expected);
}
TEST_CASE_FIXTURE(JsonEncoderFixture, "encode_AstExprIndexName")
{
AstExpr* expr = expectParseExpr("foo.bar");
std::string_view expected = R"({"type":"AstExprIndexName","location":"0,4 - 0,11","expr":{"type":"AstExprGlobal","location":"0,4 - 0,7","global":"foo"},"index":"bar","indexLocation":"0,8 - 0,11","op":"."})";
CHECK(toJson(expr) == expected);
}
TEST_CASE_FIXTURE(JsonEncoderFixture, "encode_AstExprIndexExpr")
{
AstExpr* expr = expectParseExpr("foo['bar']");
std::string_view expected = R"({"type":"AstExprIndexExpr","location":"0,4 - 0,14","expr":{"type":"AstExprGlobal","location":"0,4 - 0,7","global":"foo"},"index":{"type":"AstExprConstantString","location":"0,8 - 0,13","value":"bar"}})";
CHECK(toJson(expr) == expected);
}
TEST_CASE_FIXTURE(JsonEncoderFixture, "encode_AstExprFunction")
{
AstExpr* expr = expectParseExpr("function (a) return a end");
std::string_view expected = R"({"type":"AstExprFunction","location":"0,4 - 0,29","generics":[],"genericPacks":[],"args":[{"type":null,"name":"a","location":"0,14 - 0,15"}],"vararg":false,"varargLocation":"0,0 - 0,0","body":{"type":"AstStatBlock","location":"0,16 - 0,26","body":[{"type":"AstStatReturn","location":"0,17 - 0,25","list":[{"type":"AstExprLocal","location":"0,24 - 0,25","local":{"type":null,"name":"a","location":"0,14 - 0,15"}}]}]},"functionDepth":1,"debugname":"","hasEnd":true})";
CHECK(toJson(expr) == expected);
}
TEST_CASE_FIXTURE(JsonEncoderFixture, "encode_AstExprTable")
{
AstExpr* expr = expectParseExpr("{true, key=true, [key2]=true}");
std::string_view expected = R"({"type":"AstExprTable","location":"0,4 - 0,33","items":[{"kind":"item","value":{"type":"AstExprConstantBool","location":"0,5 - 0,9","value":true}},{"kind":"record","key":{"type":"AstExprConstantString","location":"0,11 - 0,14","value":"key"},"value":{"type":"AstExprConstantBool","location":"0,15 - 0,19","value":true}},{"kind":"general","key":{"type":"AstExprGlobal","location":"0,22 - 0,26","global":"key2"},"value":{"type":"AstExprConstantBool","location":"0,28 - 0,32","value":true}}]})";
CHECK(toJson(expr) == expected);
}
TEST_CASE_FIXTURE(JsonEncoderFixture, "encode_AstExprUnary")
{
AstExpr* expr = expectParseExpr("-b");
std::string_view expected = R"({"type":"AstExprUnary","location":"0,4 - 0,6","op":"minus","expr":{"type":"AstExprGlobal","location":"0,5 - 0,6","global":"b"}})";
CHECK(toJson(expr) == expected);
}
TEST_CASE_FIXTURE(JsonEncoderFixture, "encode_AstExprBinary")
{
AstExpr* expr = expectParseExpr("b + c");
std::string_view expected = R"({"type":"AstExprBinary","location":"0,4 - 0,9","op":"Add","left":{"type":"AstExprGlobal","location":"0,4 - 0,5","global":"b"},"right":{"type":"AstExprGlobal","location":"0,8 - 0,9","global":"c"}})";
CHECK(toJson(expr) == expected);
}
TEST_CASE_FIXTURE(JsonEncoderFixture, "encode_AstExprTypeAssertion")
{
AstExpr* expr = expectParseExpr("b :: any");
std::string_view expected = R"({"type":"AstExprTypeAssertion","location":"0,4 - 0,12","expr":{"type":"AstExprGlobal","location":"0,4 - 0,5","global":"b"},"annotation":{"type":"AstTypeReference","location":"0,9 - 0,12","name":"any","parameters":[]}})";
CHECK(toJson(expr) == expected);
}
TEST_CASE_FIXTURE(JsonEncoderFixture, "encode_AstExprError")
{
std::string_view src = "a = ";
ParseResult parseResult = Parser::parse(src.data(), src.size(), names, allocator);
REQUIRE(1 == parseResult.root->body.size);
AstStatAssign* statAssign = parseResult.root->body.data[0]->as<AstStatAssign>();
REQUIRE(statAssign != nullptr);
REQUIRE(1 == statAssign->values.size);
AstExpr* expr = statAssign->values.data[0];
std::string_view expected = R"({"type":"AstExprError","location":"0,4 - 0,4","expressions":[],"messageIndex":0})";
CHECK(toJson(expr) == expected);
}
TEST_CASE_FIXTURE(JsonEncoderFixture, "encode_AstStatIf")
{
AstStat* statement = expectParseStatement("if true then else end");
std::string_view expected = R"({"type":"AstStatIf","location":"0,0 - 0,21","condition":{"type":"AstExprConstantBool","location":"0,3 - 0,7","value":true},"thenbody":{"type":"AstStatBlock","location":"0,12 - 0,13","body":[]},"elsebody":{"type":"AstStatBlock","location":"0,17 - 0,18","body":[]},"hasThen":true,"hasEnd":true})";
CHECK(toJson(statement) == expected);
}
TEST_CASE_FIXTURE(JsonEncoderFixture, "encode_AstStatWhile")
{
AstStat* statement = expectParseStatement("while true do end");
std::string_view expected = R"({"type":"AtStatWhile","location":"0,0 - 0,17","condition":{"type":"AstExprConstantBool","location":"0,6 - 0,10","value":true},"body":{"type":"AstStatBlock","location":"0,13 - 0,14","body":[]},"hasDo":true,"hasEnd":true})";
CHECK(toJson(statement) == expected);
}
TEST_CASE_FIXTURE(JsonEncoderFixture, "encode_AstStatRepeat")
{
AstStat* statement = expectParseStatement("repeat until true");
std::string_view expected = R"({"type":"AstStatRepeat","location":"0,0 - 0,17","condition":{"type":"AstExprConstantBool","location":"0,13 - 0,17","value":true},"body":{"type":"AstStatBlock","location":"0,6 - 0,7","body":[]},"hasUntil":true})";
CHECK(toJson(statement) == expected);
}
TEST_CASE_FIXTURE(JsonEncoderFixture, "encode_AstStatBreak")
{
AstStat* statement = expectParseStatement("while true do break end");
std::string_view expected = R"({"type":"AtStatWhile","location":"0,0 - 0,23","condition":{"type":"AstExprConstantBool","location":"0,6 - 0,10","value":true},"body":{"type":"AstStatBlock","location":"0,13 - 0,20","body":[{"type":"AstStatBreak","location":"0,14 - 0,19"}]},"hasDo":true,"hasEnd":true})";
CHECK(toJson(statement) == expected);
}
TEST_CASE_FIXTURE(JsonEncoderFixture, "encode_AstStatContinue")
{
AstStat* statement = expectParseStatement("while true do continue end");
std::string_view expected = R"({"type":"AtStatWhile","location":"0,0 - 0,26","condition":{"type":"AstExprConstantBool","location":"0,6 - 0,10","value":true},"body":{"type":"AstStatBlock","location":"0,13 - 0,23","body":[{"type":"AstStatContinue","location":"0,14 - 0,22"}]},"hasDo":true,"hasEnd":true})";
CHECK(toJson(statement) == expected);
}
TEST_CASE_FIXTURE(JsonEncoderFixture, "encode_AstStatFor")
{
AstStat* statement = expectParseStatement("for a=0,1 do end");
std::string_view expected = R"({"type":"AstStatFor","location":"0,0 - 0,16","var":{"type":null,"name":"a","location":"0,4 - 0,5"},"from":{"type":"AstExprConstantNumber","location":"0,6 - 0,7","value":0},"to":{"type":"AstExprConstantNumber","location":"0,8 - 0,9","value":1},"body":{"type":"AstStatBlock","location":"0,12 - 0,13","body":[]},"hasDo":true,"hasEnd":true})";
CHECK(toJson(statement) == expected);
}
TEST_CASE_FIXTURE(JsonEncoderFixture, "encode_AstStatForIn")
{
AstStat* statement = expectParseStatement("for a in b do end");
std::string_view expected = R"({"type":"AstStatForIn","location":"0,0 - 0,17","vars":[{"type":null,"name":"a","location":"0,4 - 0,5"}],"values":[{"type":"AstExprGlobal","location":"0,9 - 0,10","global":"b"}],"body":{"type":"AstStatBlock","location":"0,13 - 0,14","body":[]},"hasIn":true,"hasDo":true,"hasEnd":true})";
CHECK(toJson(statement) == expected);
}
TEST_CASE_FIXTURE(JsonEncoderFixture, "encode_AstStatCompoundAssign")
{
AstStat* statement = expectParseStatement("a += b");
std::string_view expected = R"({"type":"AstStatCompoundAssign","location":"0,0 - 0,6","op":"Add","var":{"type":"AstExprGlobal","location":"0,0 - 0,1","global":"a"},"value":{"type":"AstExprGlobal","location":"0,5 - 0,6","global":"b"}})";
CHECK(toJson(statement) == expected);
}
TEST_CASE_FIXTURE(JsonEncoderFixture, "encode_AstStatLocalFunction")
{
AstStat* statement = expectParseStatement("local function a(b) return end");
std::string_view expected = R"({"type":"AstStatLocalFunction","location":"0,0 - 0,30","name":{"type":null,"name":"a","location":"0,15 - 0,16"},"func":{"type":"AstExprFunction","location":"0,0 - 0,30","generics":[],"genericPacks":[],"args":[{"type":null,"name":"b","location":"0,17 - 0,18"}],"vararg":false,"varargLocation":"0,0 - 0,0","body":{"type":"AstStatBlock","location":"0,19 - 0,27","body":[{"type":"AstStatReturn","location":"0,20 - 0,26","list":[]}]},"functionDepth":1,"debugname":"a","hasEnd":true}})";
CHECK(toJson(statement) == expected);
}
TEST_CASE_FIXTURE(JsonEncoderFixture, "encode_AstStatTypeAlias")
{
AstStat* statement = expectParseStatement("type A = B");
std::string_view expected = R"({"type":"AstStatTypeAlias","location":"0,0 - 0,10","name":"A","generics":[],"genericPacks":[],"type":{"type":"AstTypeReference","location":"0,9 - 0,10","name":"B","parameters":[]},"exported":false})";
CHECK(toJson(statement) == expected);
}
TEST_CASE_FIXTURE(JsonEncoderFixture, "encode_AstStatDeclareFunction")
{
AstStat* statement = expectParseStatement("declare function foo(x: number): string");
std::string_view expected = R"({"type":"AstStatDeclareFunction","location":"0,0 - 0,39","name":"foo","params":{"types":[{"type":"AstTypeReference","location":"0,24 - 0,30","name":"number","parameters":[]}]},"retTypes":{"types":[{"type":"AstTypeReference","location":"0,33 - 0,39","name":"string","parameters":[]}]},"generics":[],"genericPacks":[]})";
CHECK(toJson(statement) == expected);
}
TEST_CASE_FIXTURE(JsonEncoderFixture, "encode_AstStatDeclareClass")
{
AstStatBlock* root = expectParse(R"(
declare class Foo
prop: number
function method(self, foo: number): string
end
declare class Bar extends Foo
prop2: string
end
)");
REQUIRE(2 == root->body.size);
std::string_view expected1 = R"({"type":"AstStatDeclareClass","location":"1,22 - 4,11","name":"Foo","props":[{"name":"prop","type":{"type":"AstTypeReference","location":"2,18 - 2,24","name":"number","parameters":[]}},{"name":"method","type":{"type":"AstTypeFunction","location":"3,21 - 4,11","generics":[],"genericPacks":[],"argTypes":{"types":[{"type":"AstTypeReference","location":"3,39 - 3,45","name":"number","parameters":[]}]},"returnTypes":{"types":[{"type":"AstTypeReference","location":"3,48 - 3,54","name":"string","parameters":[]}]}}}]})";
CHECK(toJson(root->body.data[0]) == expected1);
std::string_view expected2 = R"({"type":"AstStatDeclareClass","location":"6,22 - 8,11","name":"Bar","superName":"Foo","props":[{"name":"prop2","type":{"type":"AstTypeReference","location":"7,19 - 7,25","name":"string","parameters":[]}}]})";
CHECK(toJson(root->body.data[1]) == expected2);
}
TEST_CASE_FIXTURE(JsonEncoderFixture, "encode_annotation")
{
AstStat* statement = expectParseStatement("type T = ((number) -> (string | nil)) & ((string) -> ())");
std::string_view expected = R"({"type":"AstStatTypeAlias","location":"0,0 - 0,55","name":"T","generics":[],"genericPacks":[],"type":{"type":"AstTypeIntersection","location":"0,9 - 0,55","types":[{"type":"AstTypeFunction","location":"0,10 - 0,35","generics":[],"genericPacks":[],"argTypes":{"types":[{"type":"AstTypeReference","location":"0,11 - 0,17","name":"number","parameters":[]}]},"returnTypes":{"types":[{"type":"AstTypeUnion","location":"0,23 - 0,35","types":[{"type":"AstTypeReference","location":"0,23 - 0,29","name":"string","parameters":[]},{"type":"AstTypeReference","location":"0,32 - 0,35","name":"nil","parameters":[]}]}]}},{"type":"AstTypeFunction","location":"0,41 - 0,55","generics":[],"genericPacks":[],"argTypes":{"types":[{"type":"AstTypeReference","location":"0,42 - 0,48","name":"string","parameters":[]}]},"returnTypes":{"types":[]}}]},"exported":false})";
CHECK(toJson(statement) == expected);
}
TEST_CASE_FIXTURE(JsonEncoderFixture, "encode_AstTypeError")
{
ParseResult parseResult = parse("type T = ");
REQUIRE(1 == parseResult.root->body.size);
AstStat* statement = parseResult.root->body.data[0];
std::string_view expected = R"({"type":"AstStatTypeAlias","location":"0,0 - 0,9","name":"T","generics":[],"genericPacks":[],"type":{"type":"AstTypeError","location":"0,8 - 0,9","types":[],"messageIndex":0},"exported":false})";
CHECK(toJson(statement) == expected);
}
TEST_CASE_FIXTURE(JsonEncoderFixture, "encode_AstTypePackExplicit")
{
AstStatBlock* root = expectParse(R"(
type A<T...> = () -> T...
local a: A<(number, string)>
)");
CHECK(2 == root->body.size);
std::string_view expected = R"({"type":"AstStatLocal","location":"2,8 - 2,36","vars":[{"type":{"type":"AstTypeReference","location":"2,17 - 2,36","name":"A","parameters":[{"type":"AstTypePackExplicit","location":"2,19 - 2,20","typeList":{"types":[{"type":"AstTypeReference","location":"2,20 - 2,26","name":"number","parameters":[]},{"type":"AstTypeReference","location":"2,28 - 2,34","name":"string","parameters":[]}]}}]},"name":"a","location":"2,14 - 2,15"}],"values":[]})";
CHECK(toJson(root->body.data[1]) == expected);
}
TEST_SUITE_END(); TEST_SUITE_END();

View file

@ -1436,7 +1436,7 @@ TEST_CASE_FIXTURE(Fixture, "LintHygieneUAF")
TEST_CASE_FIXTURE(Fixture, "DeprecatedApi") TEST_CASE_FIXTURE(Fixture, "DeprecatedApi")
{ {
unfreeze(typeChecker.globalTypes); unfreeze(typeChecker.globalTypes);
TypeId instanceType = typeChecker.globalTypes.addType(ClassTypeVar{"Instance", {}, std::nullopt, std::nullopt, {}, {}}); TypeId instanceType = typeChecker.globalTypes.addType(ClassTypeVar{"Instance", {}, std::nullopt, std::nullopt, {}, {}, "Test"});
persist(instanceType); persist(instanceType);
typeChecker.globalScope->exportedTypeBindings["Instance"] = TypeFun{{}, instanceType}; typeChecker.globalScope->exportedTypeBindings["Instance"] = TypeFun{{}, instanceType};

View file

@ -173,13 +173,13 @@ TEST_CASE_FIXTURE(Fixture, "clone_class")
{ {
{"__add", {typeChecker.anyType}}, {"__add", {typeChecker.anyType}},
}, },
std::nullopt, std::nullopt, {}, {}}}; std::nullopt, std::nullopt, {}, {}, "Test"}};
TypeVar exampleClass{ClassTypeVar{"ExampleClass", TypeVar exampleClass{ClassTypeVar{"ExampleClass",
{ {
{"PropOne", {typeChecker.numberType}}, {"PropOne", {typeChecker.numberType}},
{"PropTwo", {typeChecker.stringType}}, {"PropTwo", {typeChecker.stringType}},
}, },
std::nullopt, &exampleMetaClass, {}, {}}}; std::nullopt, &exampleMetaClass, {}, {}, "Test"}};
TypeArena dest; TypeArena dest;
CloneState cloneState; CloneState cloneState;
@ -196,9 +196,12 @@ TEST_CASE_FIXTURE(Fixture, "clone_class")
CHECK_EQ("ExampleClassMeta", metatable->name); CHECK_EQ("ExampleClassMeta", metatable->name);
} }
TEST_CASE_FIXTURE(Fixture, "clone_sanitize_free_types") TEST_CASE_FIXTURE(Fixture, "clone_free_types")
{ {
ScopedFastFlag sff{"LuauErrorRecoveryType", true}; ScopedFastFlag sff[]{
{"LuauErrorRecoveryType", true},
{"LuauLosslessClone", true},
};
TypeVar freeTy(FreeTypeVar{TypeLevel{}}); TypeVar freeTy(FreeTypeVar{TypeLevel{}});
TypePackVar freeTp(FreeTypePack{TypeLevel{}}); TypePackVar freeTp(FreeTypePack{TypeLevel{}});
@ -207,17 +210,17 @@ TEST_CASE_FIXTURE(Fixture, "clone_sanitize_free_types")
CloneState cloneState; CloneState cloneState;
TypeId clonedTy = clone(&freeTy, dest, cloneState); TypeId clonedTy = clone(&freeTy, dest, cloneState);
CHECK_EQ("any", toString(clonedTy)); CHECK(get<FreeTypeVar>(clonedTy));
CHECK(cloneState.encounteredFreeType);
cloneState = {}; cloneState = {};
TypePackId clonedTp = clone(&freeTp, dest, cloneState); TypePackId clonedTp = clone(&freeTp, dest, cloneState);
CHECK_EQ("...any", toString(clonedTp)); CHECK(get<FreeTypePack>(clonedTp));
CHECK(cloneState.encounteredFreeType);
} }
TEST_CASE_FIXTURE(Fixture, "clone_seal_free_tables") TEST_CASE_FIXTURE(Fixture, "clone_free_tables")
{ {
ScopedFastFlag sff{"LuauLosslessClone", true};
TypeVar tableTy{TableTypeVar{}}; TypeVar tableTy{TableTypeVar{}};
TableTypeVar* ttv = getMutable<TableTypeVar>(&tableTy); TableTypeVar* ttv = getMutable<TableTypeVar>(&tableTy);
ttv->state = TableState::Free; ttv->state = TableState::Free;
@ -227,8 +230,7 @@ TEST_CASE_FIXTURE(Fixture, "clone_seal_free_tables")
TypeId cloned = clone(&tableTy, dest, cloneState); TypeId cloned = clone(&tableTy, dest, cloneState);
const TableTypeVar* clonedTtv = get<TableTypeVar>(cloned); const TableTypeVar* clonedTtv = get<TableTypeVar>(cloned);
CHECK_EQ(clonedTtv->state, TableState::Sealed); CHECK_EQ(clonedTtv->state, TableState::Free);
CHECK(cloneState.encounteredFreeType);
} }
TEST_CASE_FIXTURE(Fixture, "clone_constrained_intersection") TEST_CASE_FIXTURE(Fixture, "clone_constrained_intersection")

View file

@ -13,6 +13,29 @@ using namespace Luau;
TEST_SUITE_BEGIN("NonstrictModeTests"); TEST_SUITE_BEGIN("NonstrictModeTests");
TEST_CASE_FIXTURE(Fixture, "function_returns_number_or_string")
{
ScopedFastFlag sff[]{
{"LuauReturnTypeInferenceInNonstrict", true},
{"LuauLowerBoundsCalculation", true}
};
CheckResult result = check(R"(
--!nonstrict
local function f()
if math.random() > 0.5 then
return 5
else
return "hi"
end
end
)");
LUAU_REQUIRE_NO_ERRORS(result);
CHECK("() -> number | string" == toString(requireType("f")));
}
TEST_CASE_FIXTURE(Fixture, "infer_nullary_function") TEST_CASE_FIXTURE(Fixture, "infer_nullary_function")
{ {
CheckResult result = check(R"( CheckResult result = check(R"(
@ -35,8 +58,13 @@ TEST_CASE_FIXTURE(Fixture, "infer_nullary_function")
REQUIRE_EQ(0, rets.size()); REQUIRE_EQ(0, rets.size());
} }
TEST_CASE_FIXTURE(Fixture, "infer_the_maximum_number_of_values_the_function_could_return") TEST_CASE_FIXTURE(Fixture, "first_return_type_dictates_number_of_return_types")
{ {
ScopedFastFlag sff[]{
{"LuauReturnTypeInferenceInNonstrict", true},
{"LuauLowerBoundsCalculation", true},
};
CheckResult result = check(R"( CheckResult result = check(R"(
--!nonstrict --!nonstrict
function getMinCardCountForWidth(width) function getMinCardCountForWidth(width)
@ -51,22 +79,18 @@ TEST_CASE_FIXTURE(Fixture, "infer_the_maximum_number_of_values_the_function_coul
TypeId t = requireType("getMinCardCountForWidth"); TypeId t = requireType("getMinCardCountForWidth");
REQUIRE(t); REQUIRE(t);
REQUIRE_EQ("(any) -> (...any)", toString(t)); REQUIRE_EQ("(any) -> number", toString(t));
} }
#if 0
// Maybe we want this?
TEST_CASE_FIXTURE(Fixture, "return_annotation_is_still_checked") TEST_CASE_FIXTURE(Fixture, "return_annotation_is_still_checked")
{ {
CheckResult result = check(R"( CheckResult result = check(R"(
--!nonstrict
function foo(x): number return 'hello' end function foo(x): number return 'hello' end
)"); )");
LUAU_REQUIRE_ERROR_COUNT(1, result); LUAU_REQUIRE_ERROR_COUNT(1, result);
REQUIRE_NE(*typeChecker.anyType, *requireType("foo"));
} }
#endif
TEST_CASE_FIXTURE(Fixture, "function_parameters_are_any") TEST_CASE_FIXTURE(Fixture, "function_parameters_are_any")
{ {
@ -256,6 +280,12 @@ TEST_CASE_FIXTURE(Fixture, "delay_function_does_not_require_its_argument_to_retu
TEST_CASE_FIXTURE(Fixture, "inconsistent_module_return_types_are_ok") TEST_CASE_FIXTURE(Fixture, "inconsistent_module_return_types_are_ok")
{ {
ScopedFastFlag sff[]{
{"LuauReturnTypeInferenceInNonstrict", true},
{"LuauLowerBoundsCalculation", true},
{"LuauSealExports", true},
};
CheckResult result = check(R"( CheckResult result = check(R"(
--!nonstrict --!nonstrict
@ -272,7 +302,7 @@ TEST_CASE_FIXTURE(Fixture, "inconsistent_module_return_types_are_ok")
LUAU_REQUIRE_NO_ERRORS(result); LUAU_REQUIRE_NO_ERRORS(result);
REQUIRE_EQ("any", toString(getMainModule()->getModuleScope()->returnType)); REQUIRE_EQ("((any) -> string) | {| foo: any |}", toString(getMainModule()->getModuleScope()->returnType));
} }
TEST_CASE_FIXTURE(Fixture, "returning_insufficient_return_values") TEST_CASE_FIXTURE(Fixture, "returning_insufficient_return_values")

View file

@ -21,7 +21,7 @@ void createSomeClasses(TypeChecker& typeChecker)
unfreeze(arena); unfreeze(arena);
TypeId parentType = arena.addType(ClassTypeVar{"Parent", {}, std::nullopt, std::nullopt, {}, nullptr}); TypeId parentType = arena.addType(ClassTypeVar{"Parent", {}, std::nullopt, std::nullopt, {}, nullptr, "Test"});
ClassTypeVar* parentClass = getMutable<ClassTypeVar>(parentType); ClassTypeVar* parentClass = getMutable<ClassTypeVar>(parentType);
parentClass->props["method"] = {makeFunction(arena, parentType, {}, {})}; parentClass->props["method"] = {makeFunction(arena, parentType, {}, {})};
@ -31,7 +31,7 @@ void createSomeClasses(TypeChecker& typeChecker)
addGlobalBinding(typeChecker, "Parent", {parentType}); addGlobalBinding(typeChecker, "Parent", {parentType});
typeChecker.globalScope->exportedTypeBindings["Parent"] = TypeFun{{}, parentType}; typeChecker.globalScope->exportedTypeBindings["Parent"] = TypeFun{{}, parentType};
TypeId childType = arena.addType(ClassTypeVar{"Child", {}, parentType, std::nullopt, {}, nullptr}); TypeId childType = arena.addType(ClassTypeVar{"Child", {}, parentType, std::nullopt, {}, nullptr, "Test"});
ClassTypeVar* childClass = getMutable<ClassTypeVar>(childType); ClassTypeVar* childClass = getMutable<ClassTypeVar>(childType);
childClass->props["virtual_method"] = {makeFunction(arena, childType, {}, {})}; childClass->props["virtual_method"] = {makeFunction(arena, childType, {}, {})};
@ -39,7 +39,7 @@ void createSomeClasses(TypeChecker& typeChecker)
addGlobalBinding(typeChecker, "Child", {childType}); addGlobalBinding(typeChecker, "Child", {childType});
typeChecker.globalScope->exportedTypeBindings["Child"] = TypeFun{{}, childType}; typeChecker.globalScope->exportedTypeBindings["Child"] = TypeFun{{}, childType};
TypeId unrelatedType = arena.addType(ClassTypeVar{"Unrelated", {}, std::nullopt, std::nullopt, {}, nullptr}); TypeId unrelatedType = arena.addType(ClassTypeVar{"Unrelated", {}, std::nullopt, std::nullopt, {}, nullptr, "Test"});
addGlobalBinding(typeChecker, "Unrelated", {unrelatedType}); addGlobalBinding(typeChecker, "Unrelated", {unrelatedType});
typeChecker.globalScope->exportedTypeBindings["Unrelated"] = TypeFun{{}, unrelatedType}; typeChecker.globalScope->exportedTypeBindings["Unrelated"] = TypeFun{{}, unrelatedType};
@ -400,7 +400,6 @@ TEST_CASE_FIXTURE(NormalizeFixture, "table_with_table_prop")
CHECK_EQ("{| x: {| y: number & string |} |}", toString(requireType("a"))); CHECK_EQ("{| x: {| y: number & string |} |}", toString(requireType("a")));
} }
#if 0
TEST_CASE_FIXTURE(NormalizeFixture, "tables") TEST_CASE_FIXTURE(NormalizeFixture, "tables")
{ {
check(R"( check(R"(
@ -428,6 +427,7 @@ TEST_CASE_FIXTURE(NormalizeFixture, "tables")
CHECK(!isSubtype(b, d)); CHECK(!isSubtype(b, d));
} }
#if 0
TEST_CASE_FIXTURE(NormalizeFixture, "table_indexers_are_invariant") TEST_CASE_FIXTURE(NormalizeFixture, "table_indexers_are_invariant")
{ {
check(R"( check(R"(
@ -619,6 +619,7 @@ TEST_CASE_FIXTURE(Fixture, "normalize_module_return_type")
{ {
ScopedFastFlag sff[] = { ScopedFastFlag sff[] = {
{"LuauLowerBoundsCalculation", true}, {"LuauLowerBoundsCalculation", true},
{"LuauReturnTypeInferenceInNonstrict", true},
}; };
check(R"( check(R"(
@ -639,7 +640,7 @@ TEST_CASE_FIXTURE(Fixture, "normalize_module_return_type")
end end
)"); )");
CHECK_EQ("(any, any) -> (...any)", toString(getMainModule()->getModuleScope()->returnType)); CHECK_EQ("(any, any) -> (any, any) -> any", toString(getMainModule()->getModuleScope()->returnType));
} }
TEST_CASE_FIXTURE(Fixture, "return_type_is_not_a_constrained_intersection") TEST_CASE_FIXTURE(Fixture, "return_type_is_not_a_constrained_intersection")
@ -950,6 +951,27 @@ TEST_CASE_FIXTURE(Fixture, "nested_table_normalization_with_non_table__no_ice")
LUAU_REQUIRE_NO_ERRORS(result); LUAU_REQUIRE_NO_ERRORS(result);
} }
TEST_CASE_FIXTURE(Fixture, "visiting_a_type_twice_is_not_considered_normal")
{
ScopedFastFlag sff{"LuauLowerBoundsCalculation", true};
CheckResult result = check(R"(
--!strict
function f(a, b)
local function g()
if math.random() > 0.5 then
return a()
else
return b
end
end
end
)");
LUAU_REQUIRE_NO_ERRORS(result);
CHECK_EQ("<a>(() -> a, a) -> ()", toString(requireType("f")));
}
TEST_CASE_FIXTURE(Fixture, "fuzz_failure_instersection_combine_must_follow") TEST_CASE_FIXTURE(Fixture, "fuzz_failure_instersection_combine_must_follow")
{ {
ScopedFastFlag flags[] = { ScopedFastFlag flags[] = {
@ -964,4 +986,16 @@ TEST_CASE_FIXTURE(Fixture, "fuzz_failure_instersection_combine_must_follow")
LUAU_REQUIRE_NO_ERRORS(result); LUAU_REQUIRE_NO_ERRORS(result);
} }
TEST_CASE_FIXTURE(Fixture, "fuzz_failure_bound_type_is_normal_but_not_its_bounded_to")
{
ScopedFastFlag sff{"LuauLowerBoundsCalculation", true};
CheckResult result = check(R"(
type t252 = ((t0<t252...>)|(any))|(any)
type t0 = t252<t0<any,t24...>,t24...>
)");
LUAU_REQUIRE_ERRORS(result);
}
TEST_SUITE_END(); TEST_SUITE_END();

View file

@ -21,13 +21,13 @@ struct ToDotClassFixture : Fixture
TypeId baseClassMetaType = arena.addType(TableTypeVar{}); TypeId baseClassMetaType = arena.addType(TableTypeVar{});
TypeId baseClassInstanceType = arena.addType(ClassTypeVar{"BaseClass", {}, std::nullopt, baseClassMetaType, {}, {}}); TypeId baseClassInstanceType = arena.addType(ClassTypeVar{"BaseClass", {}, std::nullopt, baseClassMetaType, {}, {}, "Test"});
getMutable<ClassTypeVar>(baseClassInstanceType)->props = { getMutable<ClassTypeVar>(baseClassInstanceType)->props = {
{"BaseField", {typeChecker.numberType}}, {"BaseField", {typeChecker.numberType}},
}; };
typeChecker.globalScope->exportedTypeBindings["BaseClass"] = TypeFun{{}, baseClassInstanceType}; typeChecker.globalScope->exportedTypeBindings["BaseClass"] = TypeFun{{}, baseClassInstanceType};
TypeId childClassInstanceType = arena.addType(ClassTypeVar{"ChildClass", {}, baseClassInstanceType, std::nullopt, {}, {}}); TypeId childClassInstanceType = arena.addType(ClassTypeVar{"ChildClass", {}, baseClassInstanceType, std::nullopt, {}, {}, "Test"});
getMutable<ClassTypeVar>(childClassInstanceType)->props = { getMutable<ClassTypeVar>(childClassInstanceType)->props = {
{"ChildField", {typeChecker.stringType}}, {"ChildField", {typeChecker.stringType}},
}; };

View file

@ -661,4 +661,21 @@ type t4 = false
CHECK_EQ(code, transpile(code, {}, true).code); CHECK_EQ(code, transpile(code, {}, true).code);
} }
TEST_CASE_FIXTURE(Fixture, "transpile_array_types")
{
std::string code = R"(
type t1 = {number}
type t2 = {[string]: number}
)";
CHECK_EQ(code, transpile(code, {}, true).code);
}
TEST_CASE_FIXTURE(Fixture, "transpile_for_in_multiple_types")
{
std::string code = "for k:string,v:boolean in next,{}do end";
CHECK_EQ(code, transpile(code, {}, true).code);
}
TEST_SUITE_END(); TEST_SUITE_END();

View file

@ -19,13 +19,13 @@ struct ClassFixture : Fixture
unfreeze(arena); unfreeze(arena);
TypeId baseClassInstanceType = arena.addType(ClassTypeVar{"BaseClass", {}, nullopt, nullopt, {}, {}}); TypeId baseClassInstanceType = arena.addType(ClassTypeVar{"BaseClass", {}, nullopt, nullopt, {}, {}, "Test"});
getMutable<ClassTypeVar>(baseClassInstanceType)->props = { getMutable<ClassTypeVar>(baseClassInstanceType)->props = {
{"BaseMethod", {makeFunction(arena, baseClassInstanceType, {numberType}, {})}}, {"BaseMethod", {makeFunction(arena, baseClassInstanceType, {numberType}, {})}},
{"BaseField", {numberType}}, {"BaseField", {numberType}},
}; };
TypeId baseClassType = arena.addType(ClassTypeVar{"BaseClass", {}, nullopt, nullopt, {}, {}}); TypeId baseClassType = arena.addType(ClassTypeVar{"BaseClass", {}, nullopt, nullopt, {}, {}, "Test"});
getMutable<ClassTypeVar>(baseClassType)->props = { getMutable<ClassTypeVar>(baseClassType)->props = {
{"StaticMethod", {makeFunction(arena, nullopt, {}, {numberType})}}, {"StaticMethod", {makeFunction(arena, nullopt, {}, {numberType})}},
{"Clone", {makeFunction(arena, nullopt, {baseClassInstanceType}, {baseClassInstanceType})}}, {"Clone", {makeFunction(arena, nullopt, {baseClassInstanceType}, {baseClassInstanceType})}},
@ -34,39 +34,39 @@ struct ClassFixture : Fixture
typeChecker.globalScope->exportedTypeBindings["BaseClass"] = TypeFun{{}, baseClassInstanceType}; typeChecker.globalScope->exportedTypeBindings["BaseClass"] = TypeFun{{}, baseClassInstanceType};
addGlobalBinding(typeChecker, "BaseClass", baseClassType, "@test"); addGlobalBinding(typeChecker, "BaseClass", baseClassType, "@test");
TypeId childClassInstanceType = arena.addType(ClassTypeVar{"ChildClass", {}, baseClassInstanceType, nullopt, {}, {}}); TypeId childClassInstanceType = arena.addType(ClassTypeVar{"ChildClass", {}, baseClassInstanceType, nullopt, {}, {}, "Test"});
getMutable<ClassTypeVar>(childClassInstanceType)->props = { getMutable<ClassTypeVar>(childClassInstanceType)->props = {
{"Method", {makeFunction(arena, childClassInstanceType, {}, {typeChecker.stringType})}}, {"Method", {makeFunction(arena, childClassInstanceType, {}, {typeChecker.stringType})}},
}; };
TypeId childClassType = arena.addType(ClassTypeVar{"ChildClass", {}, baseClassType, nullopt, {}, {}}); TypeId childClassType = arena.addType(ClassTypeVar{"ChildClass", {}, baseClassType, nullopt, {}, {}, "Test"});
getMutable<ClassTypeVar>(childClassType)->props = { getMutable<ClassTypeVar>(childClassType)->props = {
{"New", {makeFunction(arena, nullopt, {}, {childClassInstanceType})}}, {"New", {makeFunction(arena, nullopt, {}, {childClassInstanceType})}},
}; };
typeChecker.globalScope->exportedTypeBindings["ChildClass"] = TypeFun{{}, childClassInstanceType}; typeChecker.globalScope->exportedTypeBindings["ChildClass"] = TypeFun{{}, childClassInstanceType};
addGlobalBinding(typeChecker, "ChildClass", childClassType, "@test"); addGlobalBinding(typeChecker, "ChildClass", childClassType, "@test");
TypeId grandChildInstanceType = arena.addType(ClassTypeVar{"GrandChild", {}, childClassInstanceType, nullopt, {}, {}}); TypeId grandChildInstanceType = arena.addType(ClassTypeVar{"GrandChild", {}, childClassInstanceType, nullopt, {}, {}, "Test"});
getMutable<ClassTypeVar>(grandChildInstanceType)->props = { getMutable<ClassTypeVar>(grandChildInstanceType)->props = {
{"Method", {makeFunction(arena, grandChildInstanceType, {}, {typeChecker.stringType})}}, {"Method", {makeFunction(arena, grandChildInstanceType, {}, {typeChecker.stringType})}},
}; };
TypeId grandChildType = arena.addType(ClassTypeVar{"GrandChild", {}, baseClassType, nullopt, {}, {}}); TypeId grandChildType = arena.addType(ClassTypeVar{"GrandChild", {}, baseClassType, nullopt, {}, {}, "Test"});
getMutable<ClassTypeVar>(grandChildType)->props = { getMutable<ClassTypeVar>(grandChildType)->props = {
{"New", {makeFunction(arena, nullopt, {}, {grandChildInstanceType})}}, {"New", {makeFunction(arena, nullopt, {}, {grandChildInstanceType})}},
}; };
typeChecker.globalScope->exportedTypeBindings["GrandChild"] = TypeFun{{}, grandChildInstanceType}; typeChecker.globalScope->exportedTypeBindings["GrandChild"] = TypeFun{{}, grandChildInstanceType};
addGlobalBinding(typeChecker, "GrandChild", childClassType, "@test"); addGlobalBinding(typeChecker, "GrandChild", childClassType, "@test");
TypeId anotherChildInstanceType = arena.addType(ClassTypeVar{"AnotherChild", {}, baseClassInstanceType, nullopt, {}, {}}); TypeId anotherChildInstanceType = arena.addType(ClassTypeVar{"AnotherChild", {}, baseClassInstanceType, nullopt, {}, {}, "Test"});
getMutable<ClassTypeVar>(anotherChildInstanceType)->props = { getMutable<ClassTypeVar>(anotherChildInstanceType)->props = {
{"Method", {makeFunction(arena, anotherChildInstanceType, {}, {typeChecker.stringType})}}, {"Method", {makeFunction(arena, anotherChildInstanceType, {}, {typeChecker.stringType})}},
}; };
TypeId anotherChildType = arena.addType(ClassTypeVar{"AnotherChild", {}, baseClassType, nullopt, {}, {}}); TypeId anotherChildType = arena.addType(ClassTypeVar{"AnotherChild", {}, baseClassType, nullopt, {}, {}, "Test"});
getMutable<ClassTypeVar>(anotherChildType)->props = { getMutable<ClassTypeVar>(anotherChildType)->props = {
{"New", {makeFunction(arena, nullopt, {}, {anotherChildInstanceType})}}, {"New", {makeFunction(arena, nullopt, {}, {anotherChildInstanceType})}},
}; };
@ -75,13 +75,13 @@ struct ClassFixture : Fixture
TypeId vector2MetaType = arena.addType(TableTypeVar{}); TypeId vector2MetaType = arena.addType(TableTypeVar{});
TypeId vector2InstanceType = arena.addType(ClassTypeVar{"Vector2", {}, nullopt, vector2MetaType, {}, {}}); TypeId vector2InstanceType = arena.addType(ClassTypeVar{"Vector2", {}, nullopt, vector2MetaType, {}, {}, "Test"});
getMutable<ClassTypeVar>(vector2InstanceType)->props = { getMutable<ClassTypeVar>(vector2InstanceType)->props = {
{"X", {numberType}}, {"X", {numberType}},
{"Y", {numberType}}, {"Y", {numberType}},
}; };
TypeId vector2Type = arena.addType(ClassTypeVar{"Vector2", {}, nullopt, nullopt, {}, {}}); TypeId vector2Type = arena.addType(ClassTypeVar{"Vector2", {}, nullopt, nullopt, {}, {}, "Test"});
getMutable<ClassTypeVar>(vector2Type)->props = { getMutable<ClassTypeVar>(vector2Type)->props = {
{"New", {makeFunction(arena, nullopt, {numberType, numberType}, {vector2InstanceType})}}, {"New", {makeFunction(arena, nullopt, {numberType, numberType}, {vector2InstanceType})}},
}; };
@ -468,4 +468,18 @@ caused by:
toString(result.errors[0])); toString(result.errors[0]));
} }
TEST_CASE_FIXTURE(ClassFixture, "class_type_mismatch_with_name_conflict")
{
ScopedFastFlag luauClassDefinitionModuleInError{"LuauClassDefinitionModuleInError", true};
CheckResult result = check(R"(
local i = ChildClass.New()
type ChildClass = { x: number }
local a: ChildClass = i
)");
LUAU_REQUIRE_ERROR_COUNT(1, result);
CHECK_EQ("Type 'ChildClass' from 'Test' could not be converted into 'ChildClass' from 'MainModule'", toString(result.errors[0]));
}
TEST_SUITE_END(); TEST_SUITE_END();

View file

@ -295,8 +295,6 @@ TEST_CASE_FIXTURE(Fixture, "documentation_symbols_dont_attach_to_persistent_type
TEST_CASE_FIXTURE(Fixture, "single_class_type_identity_in_global_types") TEST_CASE_FIXTURE(Fixture, "single_class_type_identity_in_global_types")
{ {
ScopedFastFlag luauCloneDeclaredGlobals{"LuauCloneDeclaredGlobals", true};
loadDefinition(R"( loadDefinition(R"(
declare class Cls declare class Cls
end end

View file

@ -656,6 +656,11 @@ TEST_CASE_FIXTURE(Fixture, "toposort_doesnt_break_mutual_recursion")
TEST_CASE_FIXTURE(Fixture, "check_function_before_lambda_that_uses_it") TEST_CASE_FIXTURE(Fixture, "check_function_before_lambda_that_uses_it")
{ {
ScopedFastFlag sff[]{
{"LuauReturnTypeInferenceInNonstrict", true},
{"LuauLowerBoundsCalculation", true},
};
CheckResult result = check(R"( CheckResult result = check(R"(
--!nonstrict --!nonstrict
@ -664,7 +669,7 @@ TEST_CASE_FIXTURE(Fixture, "check_function_before_lambda_that_uses_it")
end end
return function() return function()
return f():andThen() return f()
end end
)"); )");
@ -791,14 +796,18 @@ TEST_CASE_FIXTURE(Fixture, "calling_function_with_incorrect_argument_type_yields
TEST_CASE_FIXTURE(Fixture, "calling_function_with_anytypepack_doesnt_leak_free_types") TEST_CASE_FIXTURE(Fixture, "calling_function_with_anytypepack_doesnt_leak_free_types")
{ {
ScopedFastFlag sff[]{
{"LuauReturnTypeInferenceInNonstrict", true},
{"LuauLowerBoundsCalculation", true},
};
CheckResult result = check(R"( CheckResult result = check(R"(
--!nonstrict --!nonstrict
function Test(a) function Test(a): ...any
return 1, "" return 1, ""
end end
local tab = {} local tab = {}
table.insert(tab, Test(1)); table.insert(tab, Test(1));
)"); )");
@ -1616,4 +1625,19 @@ TEST_CASE_FIXTURE(Fixture, "occurs_check_failure_in_function_return_type")
CHECK(nullptr != get<OccursCheckFailed>(result.errors[0])); CHECK(nullptr != get<OccursCheckFailed>(result.errors[0]));
} }
TEST_CASE_FIXTURE(Fixture, "weird_fail_to_unify_type_pack")
{
ScopedFastFlag sff[]{
{"LuauReturnTypeInferenceInNonstrict", true},
{"LuauLowerBoundsCalculation", true},
};
CheckResult result = check(R"(
local function f() return end
local g = function() return f() end
)");
LUAU_REQUIRE_NO_ERRORS(result);
}
TEST_SUITE_END(); TEST_SUITE_END();

View file

@ -307,8 +307,6 @@ type Rename = typeof(x.x)
TEST_CASE_FIXTURE(Fixture, "module_type_conflict") TEST_CASE_FIXTURE(Fixture, "module_type_conflict")
{ {
ScopedFastFlag luauTypeMismatchModuleName{"LuauTypeMismatchModuleName", true};
fileResolver.source["game/A"] = R"( fileResolver.source["game/A"] = R"(
export type T = { x: number } export type T = { x: number }
return {} return {}
@ -343,8 +341,6 @@ caused by:
TEST_CASE_FIXTURE(Fixture, "module_type_conflict_instantiated") TEST_CASE_FIXTURE(Fixture, "module_type_conflict_instantiated")
{ {
ScopedFastFlag luauTypeMismatchModuleName{"LuauTypeMismatchModuleName", true};
fileResolver.source["game/A"] = R"( fileResolver.source["game/A"] = R"(
export type Wrap<T> = { x: T } export type Wrap<T> = { x: T }
return {} return {}

View file

@ -584,20 +584,6 @@ TEST_CASE_FIXTURE(Fixture, "specialization_binds_with_prototypes_too_early")
LUAU_REQUIRE_ERRORS(result); // Should not have any errors. LUAU_REQUIRE_ERRORS(result); // Should not have any errors.
} }
TEST_CASE_FIXTURE(Fixture, "weird_fail_to_unify_type_pack")
{
ScopedFastFlag sff[] = {
{"LuauLowerBoundsCalculation", false},
};
CheckResult result = check(R"(
local function f() return end
local g = function() return f() end
)");
LUAU_REQUIRE_ERRORS(result); // Should not have any errors.
}
TEST_CASE_FIXTURE(Fixture, "weird_fail_to_unify_variadic_pack") TEST_CASE_FIXTURE(Fixture, "weird_fail_to_unify_variadic_pack")
{ {
ScopedFastFlag sff[] = { ScopedFastFlag sff[] = {

View file

@ -44,7 +44,7 @@ struct RefinementClassFixture : Fixture
TypeArena& arena = typeChecker.globalTypes; TypeArena& arena = typeChecker.globalTypes;
unfreeze(arena); unfreeze(arena);
TypeId vec3 = arena.addType(ClassTypeVar{"Vector3", {}, std::nullopt, std::nullopt, {}, nullptr}); TypeId vec3 = arena.addType(ClassTypeVar{"Vector3", {}, std::nullopt, std::nullopt, {}, nullptr, "Test"});
getMutable<ClassTypeVar>(vec3)->props = { getMutable<ClassTypeVar>(vec3)->props = {
{"X", Property{typeChecker.numberType}}, {"X", Property{typeChecker.numberType}},
{"Y", Property{typeChecker.numberType}}, {"Y", Property{typeChecker.numberType}},
@ -52,7 +52,7 @@ struct RefinementClassFixture : Fixture
}; };
normalize(vec3, arena, *typeChecker.iceHandler); normalize(vec3, arena, *typeChecker.iceHandler);
TypeId inst = arena.addType(ClassTypeVar{"Instance", {}, std::nullopt, std::nullopt, {}, nullptr}); TypeId inst = arena.addType(ClassTypeVar{"Instance", {}, std::nullopt, std::nullopt, {}, nullptr, "Test"});
TypePackId isAParams = arena.addTypePack({inst, typeChecker.stringType}); TypePackId isAParams = arena.addTypePack({inst, typeChecker.stringType});
TypePackId isARets = arena.addTypePack({typeChecker.booleanType}); TypePackId isARets = arena.addTypePack({typeChecker.booleanType});
@ -66,9 +66,9 @@ struct RefinementClassFixture : Fixture
}; };
normalize(inst, arena, *typeChecker.iceHandler); normalize(inst, arena, *typeChecker.iceHandler);
TypeId folder = typeChecker.globalTypes.addType(ClassTypeVar{"Folder", {}, inst, std::nullopt, {}, nullptr}); TypeId folder = typeChecker.globalTypes.addType(ClassTypeVar{"Folder", {}, inst, std::nullopt, {}, nullptr, "Test"});
normalize(folder, arena, *typeChecker.iceHandler); normalize(folder, arena, *typeChecker.iceHandler);
TypeId part = typeChecker.globalTypes.addType(ClassTypeVar{"Part", {}, inst, std::nullopt, {}, nullptr}); TypeId part = typeChecker.globalTypes.addType(ClassTypeVar{"Part", {}, inst, std::nullopt, {}, nullptr, "Test"});
getMutable<ClassTypeVar>(part)->props = { getMutable<ClassTypeVar>(part)->props = {
{"Position", Property{vec3}}, {"Position", Property{vec3}},
}; };

View file

@ -2086,7 +2086,6 @@ caused by:
TEST_CASE_FIXTURE(Fixture, "error_detailed_indexer_key") TEST_CASE_FIXTURE(Fixture, "error_detailed_indexer_key")
{ {
ScopedFastFlag luauTableSubtypingVariance2{"LuauTableSubtypingVariance2", true}; // Only for new path ScopedFastFlag luauTableSubtypingVariance2{"LuauTableSubtypingVariance2", true}; // Only for new path
ScopedFastFlag luauExtendedIndexerError{"LuauExtendedIndexerError", true};
CheckResult result = check(R"( CheckResult result = check(R"(
type A = { [number]: string } type A = { [number]: string }
@ -2105,7 +2104,6 @@ caused by:
TEST_CASE_FIXTURE(Fixture, "error_detailed_indexer_value") TEST_CASE_FIXTURE(Fixture, "error_detailed_indexer_value")
{ {
ScopedFastFlag luauTableSubtypingVariance2{"LuauTableSubtypingVariance2", true}; // Only for new path ScopedFastFlag luauTableSubtypingVariance2{"LuauTableSubtypingVariance2", true}; // Only for new path
ScopedFastFlag luauExtendedIndexerError{"LuauExtendedIndexerError", true};
CheckResult result = check(R"( CheckResult result = check(R"(
type A = { [number]: number } type A = { [number]: number }

View file

@ -86,16 +86,21 @@ TEST_CASE_FIXTURE(Fixture, "infer_locals_via_assignment_from_its_call_site")
TEST_CASE_FIXTURE(Fixture, "infer_in_nocheck_mode") TEST_CASE_FIXTURE(Fixture, "infer_in_nocheck_mode")
{ {
ScopedFastFlag sff[]{
{"LuauReturnTypeInferenceInNonstrict", true},
{"LuauLowerBoundsCalculation", true},
};
CheckResult result = check(R"( CheckResult result = check(R"(
--!nocheck --!nocheck
function f(x) function f(x)
return x return 5
end end
-- we get type information even if there's type errors -- we get type information even if there's type errors
f(1, 2) f(1, 2)
)"); )");
CHECK_EQ("(any) -> (...any)", toString(requireType("f"))); CHECK_EQ("(any) -> number", toString(requireType("f")));
LUAU_REQUIRE_NO_ERRORS(result); LUAU_REQUIRE_NO_ERRORS(result);
} }
@ -363,6 +368,11 @@ TEST_CASE_FIXTURE(Fixture, "globals")
TEST_CASE_FIXTURE(Fixture, "globals2") TEST_CASE_FIXTURE(Fixture, "globals2")
{ {
ScopedFastFlag sff[]{
{"LuauReturnTypeInferenceInNonstrict", true},
{"LuauLowerBoundsCalculation", true},
};
CheckResult result = check(R"( CheckResult result = check(R"(
--!nonstrict --!nonstrict
foo = function() return 1 end foo = function() return 1 end
@ -373,9 +383,9 @@ TEST_CASE_FIXTURE(Fixture, "globals2")
TypeMismatch* tm = get<TypeMismatch>(result.errors[0]); TypeMismatch* tm = get<TypeMismatch>(result.errors[0]);
REQUIRE(tm); REQUIRE(tm);
CHECK_EQ("() -> (...any)", toString(tm->wantedType)); CHECK_EQ("() -> number", toString(tm->wantedType));
CHECK_EQ("string", toString(tm->givenType)); CHECK_EQ("string", toString(tm->givenType));
CHECK_EQ("() -> (...any)", toString(requireType("foo"))); CHECK_EQ("() -> number", toString(requireType("foo")));
} }
TEST_CASE_FIXTURE(Fixture, "globals_are_banned_in_strict_mode") TEST_CASE_FIXTURE(Fixture, "globals_are_banned_in_strict_mode")

View file

@ -275,7 +275,7 @@ TEST_CASE("tagging_tables")
TEST_CASE("tagging_classes") TEST_CASE("tagging_classes")
{ {
TypeVar base{ClassTypeVar{"Base", {}, std::nullopt, std::nullopt, {}, nullptr}}; TypeVar base{ClassTypeVar{"Base", {}, std::nullopt, std::nullopt, {}, nullptr, "Test"}};
CHECK(!Luau::hasTag(&base, "foo")); CHECK(!Luau::hasTag(&base, "foo"));
Luau::attachTag(&base, "foo"); Luau::attachTag(&base, "foo");
CHECK(Luau::hasTag(&base, "foo")); CHECK(Luau::hasTag(&base, "foo"));
@ -283,8 +283,8 @@ TEST_CASE("tagging_classes")
TEST_CASE("tagging_subclasses") TEST_CASE("tagging_subclasses")
{ {
TypeVar base{ClassTypeVar{"Base", {}, std::nullopt, std::nullopt, {}, nullptr}}; TypeVar base{ClassTypeVar{"Base", {}, std::nullopt, std::nullopt, {}, nullptr, "Test"}};
TypeVar derived{ClassTypeVar{"Derived", {}, &base, std::nullopt, {}, nullptr}}; TypeVar derived{ClassTypeVar{"Derived", {}, &base, std::nullopt, {}, nullptr, "Test"}};
CHECK(!Luau::hasTag(&base, "foo")); CHECK(!Luau::hasTag(&base, "foo"));
CHECK(!Luau::hasTag(&derived, "foo")); CHECK(!Luau::hasTag(&derived, "foo"));