Sync to upstream/release/558 (#796)

* Fixed garbage data in module scopes when type graph is not retained
* LOP_MOVE with the same source and target registers is no longer
generated (Fixes https://github.com/Roblox/luau/issues/793)
This commit is contained in:
vegorov-rbx 2023-01-06 13:14:35 -08:00 committed by GitHub
parent 685ca02a30
commit be52bd91e4
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
45 changed files with 2683 additions and 382 deletions

View file

@ -71,9 +71,9 @@ struct BinaryConstraint
// When we dispatch this constraint, we update the key at this map to record
// the overload that we selected.
AstExpr* expr;
DenseHashMap<const AstExpr*, TypeId>* astOriginalCallTypes;
DenseHashMap<const AstExpr*, TypeId>* astOverloadResolvedTypes;
const void* astFragment;
DenseHashMap<const void*, TypeId>* astOriginalCallTypes;
DenseHashMap<const void*, TypeId>* astOverloadResolvedTypes;
};
// iteratee is iterable

View file

@ -82,11 +82,11 @@ struct ConstraintGraphBuilder
// If the node was applied as a function, this is the unspecialized type of
// that expression.
DenseHashMap<const AstExpr*, TypeId> astOriginalCallTypes{nullptr};
DenseHashMap<const void*, TypeId> astOriginalCallTypes{nullptr};
// If overload resolution was performed on this element, this is the
// overload that was selected.
DenseHashMap<const AstExpr*, TypeId> astOverloadResolvedTypes{nullptr};
DenseHashMap<const void*, TypeId> astOverloadResolvedTypes{nullptr};
// Types resolved from type annotations. Analogous to astTypes.
DenseHashMap<const AstType*, TypeId> astResolvedTypes{nullptr};

View file

@ -73,19 +73,30 @@ struct Module
DenseHashMap<const AstExpr*, TypeId> astTypes{nullptr};
DenseHashMap<const AstExpr*, TypePackId> astTypePacks{nullptr};
DenseHashMap<const AstExpr*, TypeId> astExpectedTypes{nullptr};
DenseHashMap<const AstExpr*, TypeId> astOriginalCallTypes{nullptr};
DenseHashMap<const AstExpr*, TypeId> astOverloadResolvedTypes{nullptr};
// Pointers are either AstExpr or AstStat.
DenseHashMap<const void*, TypeId> astOriginalCallTypes{nullptr};
// Pointers are either AstExpr or AstStat.
DenseHashMap<const void*, TypeId> astOverloadResolvedTypes{nullptr};
DenseHashMap<const AstType*, TypeId> astResolvedTypes{nullptr};
DenseHashMap<const AstTypePack*, TypePackId> astResolvedTypePacks{nullptr};
// Map AST nodes to the scope they create. Cannot be NotNull<Scope> because we need a sentinel value for the map.
DenseHashMap<const AstNode*, Scope*> astScopes{nullptr};
std::unique_ptr<struct TypeReduction> reduction;
std::unordered_map<Name, TypeId> declaredGlobals;
ErrorVec errors;
Mode mode;
SourceCode::Type type;
bool timeout = false;
TypePackId returnType = nullptr;
std::unordered_map<Name, TypeFun> exportedTypeBindings;
bool hasModuleScope() const;
ScopePtr getModuleScope() const;
// Once a module has been typechecked, we clone its public interface into a separate arena.

View file

@ -32,7 +32,7 @@ struct RecursionCounter
--(*count);
}
private:
protected:
int* count;
};

View file

@ -494,13 +494,13 @@ struct AnyType
{
};
// T | U
// `T | U`
struct UnionType
{
std::vector<TypeId> options;
};
// T & U
// `T & U`
struct IntersectionType
{
std::vector<TypeId> parts;
@ -519,9 +519,7 @@ struct NeverType
{
};
// ~T
// TODO: Some simplification step that overwrites the type graph to make sure negation
// types disappear from the user's view, and (?) a debug flag to disable that
// `~T`
struct NegationType
{
TypeId ty;
@ -676,6 +674,8 @@ TypeLevel* getMutableLevel(TypeId ty);
std::optional<TypeLevel> getLevel(TypePackId tp);
const Property* lookupClassProp(const ClassType* cls, const Name& name);
// Whether `cls` is a subclass of `parent`
bool isSubclass(const ClassType* cls, const ClassType* parent);
Type* asMutable(TypeId ty);
@ -767,7 +767,7 @@ struct TypeIterator
return !(*this == rhs);
}
const TypeId& operator*()
TypeId operator*()
{
descend();
@ -779,8 +779,8 @@ struct TypeIterator
const std::vector<TypeId>& types = getTypes(t);
LUAU_ASSERT(currentIndex < types.size());
const TypeId& ty = types[currentIndex];
LUAU_ASSERT(!get<T>(follow(ty)));
TypeId ty = follow(types[currentIndex]);
LUAU_ASSERT(!get<T>(ty));
return ty;
}

View file

@ -0,0 +1,40 @@
// This file is part of the Luau programming language and is licensed under MIT License; see LICENSE.txt for details
#pragma once
#include "Luau/Type.h"
#include "Luau/TypeArena.h"
#include "Luau/TypePack.h"
#include "Luau/Variant.h"
namespace Luau
{
/// If it's desirable to allocate into a different arena than the TypeReduction instance you have, you will need
/// to create a temporary TypeReduction in that case. This is because TypeReduction caches the reduced type.
struct TypeReduction
{
explicit TypeReduction(NotNull<TypeArena> arena, NotNull<BuiltinTypes> builtinTypes, NotNull<InternalErrorReporter> handle);
std::optional<TypeId> reduce(TypeId ty);
std::optional<TypePackId> reduce(TypePackId tp);
std::optional<TypeFun> reduce(const TypeFun& fun);
private:
NotNull<TypeArena> arena;
NotNull<BuiltinTypes> builtinTypes;
NotNull<struct InternalErrorReporter> handle;
DenseHashMap<TypeId, TypeId> cachedTypes{nullptr};
DenseHashMap<TypePackId, TypePackId> cachedTypePacks{nullptr};
std::optional<TypeId> reduceImpl(TypeId ty);
std::optional<TypePackId> reduceImpl(TypePackId tp);
// Computes an *estimated length* of the cartesian product of the given type.
size_t cartesianProductSize(TypeId ty) const;
bool hasExceededCartesianProductLimit(TypeId ty) const;
bool hasExceededCartesianProductLimit(TypePackId tp) const;
};
} // namespace Luau

View file

@ -256,7 +256,8 @@ AstExpr* findExprAtPosition(const SourceModule& source, Position pos)
ScopePtr findScopeAtPosition(const Module& module, Position pos)
{
LUAU_ASSERT(!module.scopes.empty());
if (module.scopes.empty())
return nullptr;
Location scopeLocation = module.scopes.front().first;
ScopePtr scope = module.scopes.front().second;
@ -320,7 +321,6 @@ std::optional<Binding> findBindingAtPosition(const Module& module, const SourceM
return std::nullopt;
ScopePtr currentScope = findScopeAtPosition(module, pos);
LUAU_ASSERT(currentScope);
while (currentScope)
{

View file

@ -150,6 +150,8 @@ static TypeCorrectKind checkTypeCorrectKind(
{
ty = follow(ty);
LUAU_ASSERT(module.hasModuleScope());
NotNull<Scope> moduleScope{module.getModuleScope().get()};
auto typeAtPosition = findExpectedTypeAt(module, node, position);
@ -182,8 +184,7 @@ static TypeCorrectKind checkTypeCorrectKind(
}
}
return checkTypeMatch(ty, expectedType, NotNull{module.getModuleScope().get()}, typeArena, builtinTypes) ? TypeCorrectKind::Correct
: TypeCorrectKind::None;
return checkTypeMatch(ty, expectedType, moduleScope, typeArena, builtinTypes) ? TypeCorrectKind::Correct : TypeCorrectKind::None;
}
enum class PropIndexType
@ -1328,13 +1329,11 @@ static std::optional<AutocompleteEntryMap> autocompleteStringParams(const Source
}
static AutocompleteResult autocomplete(const SourceModule& sourceModule, const ModulePtr& module, NotNull<BuiltinTypes> builtinTypes,
Scope* globalScope, Position position, StringCompletionCallback callback)
TypeArena* typeArena, Scope* globalScope, Position position, StringCompletionCallback callback)
{
if (isWithinComment(sourceModule, position))
return {};
TypeArena typeArena;
std::vector<AstNode*> ancestry = findAncestryAtPositionForAutocomplete(sourceModule, position);
LUAU_ASSERT(!ancestry.empty());
AstNode* node = ancestry.back();
@ -1360,7 +1359,7 @@ static AutocompleteResult autocomplete(const SourceModule& sourceModule, const M
TypeId ty = follow(*it);
PropIndexType indexType = indexName->op == ':' ? PropIndexType::Colon : PropIndexType::Point;
return {autocompleteProps(*module, &typeArena, builtinTypes, ty, indexType, ancestry), ancestry, AutocompleteContext::Property};
return {autocompleteProps(*module, typeArena, builtinTypes, ty, indexType, ancestry), ancestry, AutocompleteContext::Property};
}
else if (auto typeReference = node->as<AstTypeReference>())
{
@ -1378,7 +1377,7 @@ static AutocompleteResult autocomplete(const SourceModule& sourceModule, const M
if (statLocal->vars.size == 1 && (!statLocal->equalsSignLocation || position < statLocal->equalsSignLocation->begin))
return {{{"function", AutocompleteEntry{AutocompleteEntryKind::Keyword}}}, ancestry, AutocompleteContext::Unknown};
else if (statLocal->equalsSignLocation && position >= statLocal->equalsSignLocation->end)
return autocompleteExpression(sourceModule, *module, builtinTypes, &typeArena, ancestry, position);
return autocompleteExpression(sourceModule, *module, builtinTypes, typeArena, ancestry, position);
else
return {};
}
@ -1392,7 +1391,7 @@ static AutocompleteResult autocomplete(const SourceModule& sourceModule, const M
if (statFor->from->location.containsClosed(position) || statFor->to->location.containsClosed(position) ||
(statFor->step && statFor->step->location.containsClosed(position)))
return autocompleteExpression(sourceModule, *module, builtinTypes, &typeArena, ancestry, position);
return autocompleteExpression(sourceModule, *module, builtinTypes, typeArena, ancestry, position);
return {};
}
@ -1422,7 +1421,7 @@ static AutocompleteResult autocomplete(const SourceModule& sourceModule, const M
AstExpr* lastExpr = statForIn->values.data[statForIn->values.size - 1];
if (lastExpr->location.containsClosed(position))
return autocompleteExpression(sourceModule, *module, builtinTypes, &typeArena, ancestry, position);
return autocompleteExpression(sourceModule, *module, builtinTypes, typeArena, ancestry, position);
if (position > lastExpr->location.end)
return {{{"do", AutocompleteEntry{AutocompleteEntryKind::Keyword}}}, ancestry, AutocompleteContext::Keyword};
@ -1446,7 +1445,7 @@ static AutocompleteResult autocomplete(const SourceModule& sourceModule, const M
return {{{"do", AutocompleteEntry{AutocompleteEntryKind::Keyword}}}, ancestry, AutocompleteContext::Keyword};
if (!statWhile->hasDo || position < statWhile->doLocation.begin)
return autocompleteExpression(sourceModule, *module, builtinTypes, &typeArena, ancestry, position);
return autocompleteExpression(sourceModule, *module, builtinTypes, typeArena, ancestry, position);
if (statWhile->hasDo && position > statWhile->doLocation.end)
return {autocompleteStatement(sourceModule, *module, ancestry, position), ancestry, AutocompleteContext::Statement};
@ -1463,7 +1462,7 @@ static AutocompleteResult autocomplete(const SourceModule& sourceModule, const M
else if (AstStatIf* statIf = parent->as<AstStatIf>(); statIf && node->is<AstStatBlock>())
{
if (statIf->condition->is<AstExprError>())
return autocompleteExpression(sourceModule, *module, builtinTypes, &typeArena, ancestry, position);
return autocompleteExpression(sourceModule, *module, builtinTypes, typeArena, ancestry, position);
else if (!statIf->thenLocation || statIf->thenLocation->containsClosed(position))
return {{{"then", AutocompleteEntry{AutocompleteEntryKind::Keyword}}}, ancestry, AutocompleteContext::Keyword};
}
@ -1471,7 +1470,7 @@ static AutocompleteResult autocomplete(const SourceModule& sourceModule, const M
statIf && (!statIf->thenLocation || statIf->thenLocation->containsClosed(position)))
return {{{"then", AutocompleteEntry{AutocompleteEntryKind::Keyword}}}, ancestry, AutocompleteContext::Keyword};
else if (AstStatRepeat* statRepeat = node->as<AstStatRepeat>(); statRepeat && statRepeat->condition->is<AstExprError>())
return autocompleteExpression(sourceModule, *module, builtinTypes, &typeArena, ancestry, position);
return autocompleteExpression(sourceModule, *module, builtinTypes, typeArena, ancestry, position);
else if (AstStatRepeat* statRepeat = extractStat<AstStatRepeat>(ancestry); statRepeat)
return {autocompleteStatement(sourceModule, *module, ancestry, position), ancestry, AutocompleteContext::Statement};
else if (AstExprTable* exprTable = parent->as<AstExprTable>();
@ -1484,7 +1483,7 @@ static AutocompleteResult autocomplete(const SourceModule& sourceModule, const M
{
if (auto it = module->astExpectedTypes.find(exprTable))
{
auto result = autocompleteProps(*module, &typeArena, builtinTypes, *it, PropIndexType::Key, ancestry);
auto result = autocompleteProps(*module, typeArena, builtinTypes, *it, PropIndexType::Key, ancestry);
if (FFlag::LuauCompleteTableKeysBetter)
{
@ -1518,7 +1517,7 @@ static AutocompleteResult autocomplete(const SourceModule& sourceModule, const M
// If we know for sure that a key is being written, do not offer general expression suggestions
if (!key)
autocompleteExpression(sourceModule, *module, builtinTypes, &typeArena, ancestry, position, result);
autocompleteExpression(sourceModule, *module, builtinTypes, typeArena, ancestry, position, result);
return {result, ancestry, AutocompleteContext::Property};
}
@ -1546,7 +1545,7 @@ static AutocompleteResult autocomplete(const SourceModule& sourceModule, const M
if (auto idxExpr = ancestry.at(ancestry.size() - 2)->as<AstExprIndexExpr>())
{
if (auto it = module->astTypes.find(idxExpr->expr))
autocompleteProps(*module, &typeArena, builtinTypes, follow(*it), PropIndexType::Point, ancestry, result);
autocompleteProps(*module, typeArena, builtinTypes, follow(*it), PropIndexType::Point, ancestry, result);
}
else if (auto binExpr = ancestry.at(ancestry.size() - 2)->as<AstExprBinary>())
{
@ -1572,7 +1571,7 @@ static AutocompleteResult autocomplete(const SourceModule& sourceModule, const M
return {};
if (node->asExpr())
return autocompleteExpression(sourceModule, *module, builtinTypes, &typeArena, ancestry, position);
return autocompleteExpression(sourceModule, *module, builtinTypes, typeArena, ancestry, position);
else if (node->asStat())
return {autocompleteStatement(sourceModule, *module, ancestry, position), ancestry, AutocompleteContext::Statement};
@ -1599,9 +1598,8 @@ AutocompleteResult autocomplete(Frontend& frontend, const ModuleName& moduleName
NotNull<BuiltinTypes> builtinTypes = frontend.builtinTypes;
Scope* globalScope = frontend.typeCheckerForAutocomplete.globalScope.get();
AutocompleteResult autocompleteResult = autocomplete(*sourceModule, module, builtinTypes, globalScope, position, callback);
return autocompleteResult;
TypeArena typeArena;
return autocomplete(*sourceModule, module, builtinTypes, &typeArena, globalScope, position, callback);
}
} // namespace Luau

View file

@ -18,9 +18,7 @@
LUAU_FASTFLAGVARIABLE(LuauSetMetaTableArgsCheck, false)
LUAU_FASTFLAG(LuauUnknownAndNeverType)
LUAU_FASTFLAGVARIABLE(LuauBuiltInMetatableNoBadSynthetic, false)
LUAU_FASTFLAG(LuauOptionalNextKey)
LUAU_FASTFLAG(LuauReportShadowedTypeAlias)
LUAU_FASTFLAG(LuauNewLibraryTypeNames)
/** FIXME: Many of these type definitions are not quite completely accurate.
*
@ -289,38 +287,18 @@ void registerBuiltinGlobals(TypeChecker& typeChecker)
addGlobalBinding(typeChecker, "string", it->second.type, "@luau");
if (FFlag::LuauOptionalNextKey)
{
// next<K, V>(t: Table<K, V>, i: K?) -> (K?, V)
TypePackId nextArgsTypePack = arena.addTypePack(TypePack{{mapOfKtoV, makeOption(typeChecker, arena, genericK)}});
TypePackId nextRetsTypePack = arena.addTypePack(TypePack{{makeOption(typeChecker, arena, genericK), genericV}});
addGlobalBinding(typeChecker, "next", arena.addType(FunctionType{{genericK, genericV}, {}, nextArgsTypePack, nextRetsTypePack}), "@luau");
// next<K, V>(t: Table<K, V>, i: K?) -> (K?, V)
TypePackId nextArgsTypePack = arena.addTypePack(TypePack{{mapOfKtoV, makeOption(typeChecker, arena, genericK)}});
TypePackId nextRetsTypePack = arena.addTypePack(TypePack{{makeOption(typeChecker, arena, genericK), genericV}});
addGlobalBinding(typeChecker, "next", arena.addType(FunctionType{{genericK, genericV}, {}, nextArgsTypePack, nextRetsTypePack}), "@luau");
TypePackId pairsArgsTypePack = arena.addTypePack({mapOfKtoV});
TypePackId pairsArgsTypePack = arena.addTypePack({mapOfKtoV});
TypeId pairsNext = arena.addType(FunctionType{nextArgsTypePack, nextRetsTypePack});
TypePackId pairsReturnTypePack = arena.addTypePack(TypePack{{pairsNext, mapOfKtoV, nilType}});
TypeId pairsNext = arena.addType(FunctionType{nextArgsTypePack, nextRetsTypePack});
TypePackId pairsReturnTypePack = arena.addTypePack(TypePack{{pairsNext, mapOfKtoV, nilType}});
// pairs<K, V>(t: Table<K, V>) -> ((Table<K, V>, K?) -> (K, V), Table<K, V>, nil)
addGlobalBinding(
typeChecker, "pairs", arena.addType(FunctionType{{genericK, genericV}, {}, pairsArgsTypePack, pairsReturnTypePack}), "@luau");
}
else
{
// next<K, V>(t: Table<K, V>, i: K?) -> (K, V)
TypePackId nextArgsTypePack = arena.addTypePack(TypePack{{mapOfKtoV, makeOption(typeChecker, arena, genericK)}});
addGlobalBinding(typeChecker, "next",
arena.addType(FunctionType{{genericK, genericV}, {}, nextArgsTypePack, arena.addTypePack(TypePack{{genericK, genericV}})}), "@luau");
TypePackId pairsArgsTypePack = arena.addTypePack({mapOfKtoV});
TypeId pairsNext = arena.addType(FunctionType{nextArgsTypePack, arena.addTypePack(TypePack{{genericK, genericV}})});
TypePackId pairsReturnTypePack = arena.addTypePack(TypePack{{pairsNext, mapOfKtoV, nilType}});
// pairs<K, V>(t: Table<K, V>) -> ((Table<K, V>, K?) -> (K, V), Table<K, V>, nil)
addGlobalBinding(
typeChecker, "pairs", arena.addType(FunctionType{{genericK, genericV}, {}, pairsArgsTypePack, pairsReturnTypePack}), "@luau");
}
// pairs<K, V>(t: Table<K, V>) -> ((Table<K, V>, K?) -> (K, V), Table<K, V>, nil)
addGlobalBinding(typeChecker, "pairs", arena.addType(FunctionType{{genericK, genericV}, {}, pairsArgsTypePack, pairsReturnTypePack}), "@luau");
TypeId genericMT = arena.addType(GenericType{"MT"});
@ -352,12 +330,7 @@ void registerBuiltinGlobals(TypeChecker& typeChecker)
if (TableType* ttv = getMutable<TableType>(pair.second.typeId))
{
if (!ttv->name)
{
if (FFlag::LuauNewLibraryTypeNames)
ttv->name = "typeof(" + toString(pair.first) + ")";
else
ttv->name = toString(pair.first);
}
ttv->name = "typeof(" + toString(pair.first) + ")";
}
}
@ -408,36 +381,18 @@ void registerBuiltinGlobals(Frontend& frontend)
addGlobalBinding(frontend, "string", it->second.type, "@luau");
if (FFlag::LuauOptionalNextKey)
{
// next<K, V>(t: Table<K, V>, i: K?) -> (K?, V)
TypePackId nextArgsTypePack = arena.addTypePack(TypePack{{mapOfKtoV, makeOption(frontend, arena, genericK)}});
TypePackId nextRetsTypePack = arena.addTypePack(TypePack{{makeOption(frontend, arena, genericK), genericV}});
addGlobalBinding(frontend, "next", arena.addType(FunctionType{{genericK, genericV}, {}, nextArgsTypePack, nextRetsTypePack}), "@luau");
// next<K, V>(t: Table<K, V>, i: K?) -> (K?, V)
TypePackId nextArgsTypePack = arena.addTypePack(TypePack{{mapOfKtoV, makeOption(frontend, arena, genericK)}});
TypePackId nextRetsTypePack = arena.addTypePack(TypePack{{makeOption(frontend, arena, genericK), genericV}});
addGlobalBinding(frontend, "next", arena.addType(FunctionType{{genericK, genericV}, {}, nextArgsTypePack, nextRetsTypePack}), "@luau");
TypePackId pairsArgsTypePack = arena.addTypePack({mapOfKtoV});
TypePackId pairsArgsTypePack = arena.addTypePack({mapOfKtoV});
TypeId pairsNext = arena.addType(FunctionType{nextArgsTypePack, nextRetsTypePack});
TypePackId pairsReturnTypePack = arena.addTypePack(TypePack{{pairsNext, mapOfKtoV, frontend.builtinTypes->nilType}});
TypeId pairsNext = arena.addType(FunctionType{nextArgsTypePack, nextRetsTypePack});
TypePackId pairsReturnTypePack = arena.addTypePack(TypePack{{pairsNext, mapOfKtoV, frontend.builtinTypes->nilType}});
// pairs<K, V>(t: Table<K, V>) -> ((Table<K, V>, K?) -> (K?, V), Table<K, V>, nil)
addGlobalBinding(frontend, "pairs", arena.addType(FunctionType{{genericK, genericV}, {}, pairsArgsTypePack, pairsReturnTypePack}), "@luau");
}
else
{
// next<K, V>(t: Table<K, V>, i: K?) -> (K, V)
TypePackId nextArgsTypePack = arena.addTypePack(TypePack{{mapOfKtoV, makeOption(frontend, arena, genericK)}});
addGlobalBinding(frontend, "next",
arena.addType(FunctionType{{genericK, genericV}, {}, nextArgsTypePack, arena.addTypePack(TypePack{{genericK, genericV}})}), "@luau");
TypePackId pairsArgsTypePack = arena.addTypePack({mapOfKtoV});
TypeId pairsNext = arena.addType(FunctionType{nextArgsTypePack, arena.addTypePack(TypePack{{genericK, genericV}})});
TypePackId pairsReturnTypePack = arena.addTypePack(TypePack{{pairsNext, mapOfKtoV, frontend.builtinTypes->nilType}});
// pairs<K, V>(t: Table<K, V>) -> ((Table<K, V>, K?) -> (K, V), Table<K, V>, nil)
addGlobalBinding(frontend, "pairs", arena.addType(FunctionType{{genericK, genericV}, {}, pairsArgsTypePack, pairsReturnTypePack}), "@luau");
}
// pairs<K, V>(t: Table<K, V>) -> ((Table<K, V>, K?) -> (K?, V), Table<K, V>, nil)
addGlobalBinding(frontend, "pairs", arena.addType(FunctionType{{genericK, genericV}, {}, pairsArgsTypePack, pairsReturnTypePack}), "@luau");
TypeId genericMT = arena.addType(GenericType{"MT"});
@ -469,12 +424,7 @@ void registerBuiltinGlobals(Frontend& frontend)
if (TableType* ttv = getMutable<TableType>(pair.second.typeId))
{
if (!ttv->name)
{
if (FFlag::LuauNewLibraryTypeNames)
ttv->name = "typeof(" + toString(pair.first) + ")";
else
ttv->name = toString(pair.first);
}
ttv->name = "typeof(" + toString(pair.first) + ")";
}
}

View file

@ -15,6 +15,7 @@ LUAU_FASTINT(LuauCheckRecursionLimit);
LUAU_FASTFLAG(DebugLuauLogSolverToJson);
LUAU_FASTFLAG(DebugLuauMagicTypes);
LUAU_FASTFLAG(LuauNegatedClassTypes);
LUAU_FASTFLAG(LuauScopelessModule);
namespace Luau
{
@ -520,7 +521,8 @@ void ConstraintGraphBuilder::visit(const ScopePtr& scope, AstStatLocal* local)
const Name name{local->vars.data[i]->name.value};
if (ModulePtr module = moduleResolver->getModule(moduleInfo->name))
scope->importedTypeBindings[name] = module->getModuleScope()->exportedTypeBindings;
scope->importedTypeBindings[name] =
FFlag::LuauScopelessModule ? module->exportedTypeBindings : module->getModuleScope()->exportedTypeBindings;
}
}
}
@ -733,16 +735,15 @@ void ConstraintGraphBuilder::visit(const ScopePtr& scope, AstStatAssign* assign)
void ConstraintGraphBuilder::visit(const ScopePtr& scope, AstStatCompoundAssign* assign)
{
// Synthesize A = A op B from A op= B and then build constraints for that instead.
// We need to tweak the BinaryConstraint that we emit, so we cannot use the
// strategy of falsifying an AST fragment.
TypeId varId = checkLValue(scope, assign->var);
Inference valueInf = check(scope, assign->value);
AstExprBinary exprBinary{assign->location, assign->op, assign->var, assign->value};
AstExpr* exprBinaryPtr = &exprBinary;
AstArray<AstExpr*> vars{&assign->var, 1};
AstArray<AstExpr*> values{&exprBinaryPtr, 1};
AstStatAssign syntheticAssign{assign->location, vars, values};
visit(scope, &syntheticAssign);
TypeId resultType = arena->addType(BlockedType{});
addConstraint(scope, assign->location,
BinaryConstraint{assign->op, varId, valueInf.ty, resultType, assign, &astOriginalCallTypes, &astOverloadResolvedTypes});
addConstraint(scope, assign->location, SubtypeConstraint{resultType, varId});
}
void ConstraintGraphBuilder::visit(const ScopePtr& scope, AstStatIf* ifStatement)

View file

@ -18,6 +18,7 @@
LUAU_FASTFLAGVARIABLE(DebugLuauLogSolver, false);
LUAU_FASTFLAGVARIABLE(DebugLuauLogSolverToJson, false);
LUAU_FASTFLAG(LuauScopelessModule);
namespace Luau
{
@ -681,8 +682,8 @@ bool ConstraintSolver::tryDispatch(const BinaryConstraint& c, NotNull<const Cons
asMutable(resultType)->ty.emplace<BoundType>(mmResult);
unblock(resultType);
(*c.astOriginalCallTypes)[c.expr] = *mm;
(*c.astOverloadResolvedTypes)[c.expr] = *instantiatedMm;
(*c.astOriginalCallTypes)[c.astFragment] = *mm;
(*c.astOverloadResolvedTypes)[c.astFragment] = *instantiatedMm;
return true;
}
}
@ -1895,7 +1896,7 @@ TypeId ConstraintSolver::resolveModule(const ModuleInfo& info, const Location& l
return errorRecoveryType();
}
TypePackId modulePack = module->getModuleScope()->returnType;
TypePackId modulePack = FFlag::LuauScopelessModule ? module->returnType : module->getModuleScope()->returnType;
if (get<Unifiable::Error>(modulePack))
return errorRecoveryType();

View file

@ -2,7 +2,6 @@
#include "Luau/BuiltinDefinitions.h"
LUAU_FASTFLAG(LuauUnknownAndNeverType)
LUAU_FASTFLAG(LuauOptionalNextKey)
namespace Luau
{
@ -127,7 +126,7 @@ declare function rawlen<K, V>(obj: {[K]: V} | string): number
declare function setfenv<T..., R...>(target: number | (T...) -> R..., env: {[string]: any}): ((T...) -> R...)?
-- TODO: place ipairs definition here with removal of FFlagLuauOptionalNextKey
declare function ipairs<V>(tab: {V}): (({V}, number) -> (number?, V), {V}, number)
declare function pcall<A..., R...>(f: (A...) -> R..., ...: A...): (boolean, R...)
@ -208,11 +207,6 @@ std::string getBuiltinDefinitionSource()
else
result += "declare function error<T>(message: T, level: number?)\n";
if (FFlag::LuauOptionalNextKey)
result += "declare function ipairs<V>(tab: {V}): (({V}, number) -> (number?, V), {V}, number)\n";
else
result += "declare function ipairs<V>(tab: {V}): (({V}, number) -> (number, V), {V}, number)\n";
return result;
}

View file

@ -16,6 +16,7 @@
#include "Luau/TimeTrace.h"
#include "Luau/TypeChecker2.h"
#include "Luau/TypeInfer.h"
#include "Luau/TypeReduction.h"
#include "Luau/Variant.h"
#include <algorithm>
@ -30,6 +31,7 @@ LUAU_FASTFLAGVARIABLE(LuauKnowsTheDataModel3, false)
LUAU_FASTINTVARIABLE(LuauAutocompleteCheckTimeoutMs, 100)
LUAU_FASTFLAGVARIABLE(DebugLuauDeferredConstraintResolution, false)
LUAU_FASTFLAG(DebugLuauLogSolverToJson);
LUAU_FASTFLAG(LuauScopelessModule);
namespace Luau
{
@ -111,7 +113,9 @@ LoadDefinitionFileResult Frontend::loadDefinitionFile(std::string_view source, c
CloneState cloneState;
std::vector<TypeId> typesToPersist;
typesToPersist.reserve(checkedModule->declaredGlobals.size() + checkedModule->getModuleScope()->exportedTypeBindings.size());
typesToPersist.reserve(
checkedModule->declaredGlobals.size() +
(FFlag::LuauScopelessModule ? checkedModule->exportedTypeBindings.size() : checkedModule->getModuleScope()->exportedTypeBindings.size()));
for (const auto& [name, ty] : checkedModule->declaredGlobals)
{
@ -123,7 +127,8 @@ LoadDefinitionFileResult Frontend::loadDefinitionFile(std::string_view source, c
typesToPersist.push_back(globalTy);
}
for (const auto& [name, ty] : checkedModule->getModuleScope()->exportedTypeBindings)
for (const auto& [name, ty] :
FFlag::LuauScopelessModule ? checkedModule->exportedTypeBindings : checkedModule->getModuleScope()->exportedTypeBindings)
{
TypeFun globalTy = clone(ty, globalTypes, cloneState);
std::string documentationSymbol = packageName + "/globaltype/" + name;
@ -168,7 +173,9 @@ LoadDefinitionFileResult loadDefinitionFile(TypeChecker& typeChecker, ScopePtr t
CloneState cloneState;
std::vector<TypeId> typesToPersist;
typesToPersist.reserve(checkedModule->declaredGlobals.size() + checkedModule->getModuleScope()->exportedTypeBindings.size());
typesToPersist.reserve(
checkedModule->declaredGlobals.size() +
(FFlag::LuauScopelessModule ? checkedModule->exportedTypeBindings.size() : checkedModule->getModuleScope()->exportedTypeBindings.size()));
for (const auto& [name, ty] : checkedModule->declaredGlobals)
{
@ -180,7 +187,8 @@ LoadDefinitionFileResult loadDefinitionFile(TypeChecker& typeChecker, ScopePtr t
typesToPersist.push_back(globalTy);
}
for (const auto& [name, ty] : checkedModule->getModuleScope()->exportedTypeBindings)
for (const auto& [name, ty] :
FFlag::LuauScopelessModule ? checkedModule->exportedTypeBindings : checkedModule->getModuleScope()->exportedTypeBindings)
{
TypeFun globalTy = clone(ty, typeChecker.globalTypes, cloneState);
std::string documentationSymbol = packageName + "/globaltype/" + name;
@ -562,12 +570,29 @@ CheckResult Frontend::check(const ModuleName& name, std::optional<FrontendOption
freeze(module->interfaceTypes);
module->internalTypes.clear();
module->astTypes.clear();
module->astExpectedTypes.clear();
module->astOriginalCallTypes.clear();
module->astResolvedTypes.clear();
module->astResolvedTypePacks.clear();
module->scopes.resize(1);
if (FFlag::LuauScopelessModule)
{
module->astTypes.clear();
module->astTypePacks.clear();
module->astExpectedTypes.clear();
module->astOriginalCallTypes.clear();
module->astOverloadResolvedTypes.clear();
module->astResolvedTypes.clear();
module->astResolvedTypePacks.clear();
module->astScopes.clear();
module->scopes.clear();
}
else
{
module->astTypes.clear();
module->astExpectedTypes.clear();
module->astOriginalCallTypes.clear();
module->astResolvedTypes.clear();
module->astResolvedTypePacks.clear();
module->scopes.resize(1);
}
}
if (mode != Mode::NoCheck)
@ -852,6 +877,7 @@ ModulePtr Frontend::check(
const SourceModule& sourceModule, Mode mode, const ScopePtr& environmentScope, std::vector<RequireCycle> requireCycles, bool forAutocomplete)
{
ModulePtr result = std::make_shared<Module>();
result->reduction = std::make_unique<TypeReduction>(NotNull{&result->internalTypes}, builtinTypes, NotNull{&iceHandler});
std::unique_ptr<DcrLogger> logger;
if (FFlag::DebugLuauLogSolverToJson)

View file

@ -7,9 +7,10 @@
#include "Luau/Normalize.h"
#include "Luau/RecursionCounter.h"
#include "Luau/Scope.h"
#include "Luau/Type.h"
#include "Luau/TypeInfer.h"
#include "Luau/TypePack.h"
#include "Luau/Type.h"
#include "Luau/TypeReduction.h"
#include "Luau/VisitType.h"
#include <algorithm>
@ -17,6 +18,7 @@
LUAU_FASTFLAG(DebugLuauDeferredConstraintResolution);
LUAU_FASTFLAGVARIABLE(LuauClonePublicInterfaceLess, false);
LUAU_FASTFLAG(LuauSubstitutionReentrant);
LUAU_FASTFLAG(LuauScopelessModule);
LUAU_FASTFLAG(LuauClassTypeVarsInSubstitution);
LUAU_FASTFLAG(LuauSubstitutionFixMissingFields);
@ -189,7 +191,6 @@ void Module::clonePublicInterface(NotNull<BuiltinTypes> builtinTypes, InternalEr
TypePackId returnType = moduleScope->returnType;
std::optional<TypePackId> varargPack = FFlag::DebugLuauDeferredConstraintResolution ? std::nullopt : moduleScope->varargPack;
std::unordered_map<Name, TypeFun>* exportedTypeBindings = &moduleScope->exportedTypeBindings;
TxnLog log;
ClonePublicInterface clonePublicInterface{&log, builtinTypes, this};
@ -209,15 +210,12 @@ void Module::clonePublicInterface(NotNull<BuiltinTypes> builtinTypes, InternalEr
moduleScope->varargPack = varargPack;
}
if (exportedTypeBindings)
for (auto& [name, tf] : moduleScope->exportedTypeBindings)
{
for (auto& [name, tf] : *exportedTypeBindings)
{
if (FFlag::LuauClonePublicInterfaceLess)
tf = clonePublicInterface.cloneTypeFun(tf);
else
tf = clone(tf, interfaceTypes, cloneState);
}
if (FFlag::LuauClonePublicInterfaceLess)
tf = clonePublicInterface.cloneTypeFun(tf);
else
tf = clone(tf, interfaceTypes, cloneState);
}
for (auto& [name, ty] : declaredGlobals)
@ -228,13 +226,25 @@ void Module::clonePublicInterface(NotNull<BuiltinTypes> builtinTypes, InternalEr
ty = clone(ty, interfaceTypes, cloneState);
}
// Copy external stuff over to Module itself
if (FFlag::LuauScopelessModule)
{
this->returnType = moduleScope->returnType;
this->exportedTypeBindings = std::move(moduleScope->exportedTypeBindings);
}
freeze(internalTypes);
freeze(interfaceTypes);
}
bool Module::hasModuleScope() const
{
return !scopes.empty();
}
ScopePtr Module::getModuleScope() const
{
LUAU_ASSERT(!scopes.empty());
LUAU_ASSERT(hasModuleScope());
return scopes.front().second;
}

View file

@ -26,6 +26,7 @@ LUAU_FASTFLAGVARIABLE(LuauSerializeNilUnionAsNil, false)
* Fair warning: Setting this will break a lot of Luau unit tests.
*/
LUAU_FASTFLAGVARIABLE(DebugLuauVerboseTypeNames, false)
LUAU_FASTFLAGVARIABLE(DebugLuauToStringNoLexicalSort, false)
namespace Luau
{
@ -756,7 +757,8 @@ struct TypeStringifier
state.unsee(&uv);
std::sort(results.begin(), results.end());
if (!FFlag::DebugLuauToStringNoLexicalSort)
std::sort(results.begin(), results.end());
if (optional && results.size() > 1)
state.emit("(");
@ -821,7 +823,8 @@ struct TypeStringifier
state.unsee(&uv);
std::sort(results.begin(), results.end());
if (!FFlag::DebugLuauToStringNoLexicalSort)
std::sort(results.begin(), results.end());
bool first = true;
for (std::string& ss : results)

View file

@ -26,7 +26,6 @@ LUAU_FASTINTVARIABLE(LuauTableTypeMaximumStringifierLength, 0)
LUAU_FASTINT(LuauTypeInferRecursionLimit)
LUAU_FASTFLAG(LuauUnknownAndNeverType)
LUAU_FASTFLAGVARIABLE(LuauMaybeGenericIntersectionTypes, false)
LUAU_FASTFLAGVARIABLE(LuauNewLibraryTypeNames, false)
LUAU_FASTFLAG(LuauInstantiateInSubtyping)
namespace Luau
@ -865,7 +864,7 @@ TypeId BuiltinTypes::makeStringMetatable()
TypeId tableType = arena->addType(TableType{std::move(stringLib), std::nullopt, TypeLevel{}, TableState::Sealed});
if (TableType* ttv = getMutable<TableType>(tableType))
ttv->name = FFlag::LuauNewLibraryTypeNames ? "typeof(string)" : "string";
ttv->name = "typeof(string)";
return arena->addType(TableType{{{{"__index", {tableType}}}}, std::nullopt, TypeLevel{}, TableState::Sealed});
}

View file

@ -487,8 +487,11 @@ public:
AstType* annotation = local->annotation;
if (!annotation)
{
if (auto result = getScope(local->location)->lookup(local))
local->annotation = typeAst(*result);
if (auto scope = getScope(local->location))
{
if (auto result = scope->lookup(local))
local->annotation = typeAst(*result);
}
}
return true;
}

View file

@ -626,8 +626,11 @@ struct TypeChecker2
void visit(AstStatCompoundAssign* stat)
{
visit(stat->var);
visit(stat->value);
AstExprBinary fake{stat->location, stat->op, stat->var, stat->value};
TypeId resultTy = visit(&fake, stat);
TypeId varTy = lookupType(stat->var);
reportErrors(tryUnify(stack.back(), stat->location, resultTy, varTy));
}
void visit(AstStatFunction* stat)
@ -737,7 +740,10 @@ struct TypeChecker2
else if (auto e = expr->as<AstExprUnary>())
return visit(e);
else if (auto e = expr->as<AstExprBinary>())
return visit(e);
{
visit(e);
return;
}
else if (auto e = expr->as<AstExprTypeAssertion>())
return visit(e);
else if (auto e = expr->as<AstExprIfElse>())
@ -1045,7 +1051,7 @@ struct TypeChecker2
}
}
void visit(AstExprBinary* expr)
TypeId visit(AstExprBinary* expr, void* overrideKey = nullptr)
{
visit(expr->left);
visit(expr->right);
@ -1066,8 +1072,10 @@ struct TypeChecker2
bool isStringOperation = isString(leftType) && isString(rightType);
if (get<AnyType>(leftType) || get<ErrorType>(leftType) || get<AnyType>(rightType) || get<ErrorType>(rightType))
return;
if (get<AnyType>(leftType) || get<ErrorType>(leftType))
return leftType;
else if (get<AnyType>(rightType) || get<ErrorType>(rightType))
return rightType;
if ((get<BlockedType>(leftType) || get<FreeType>(leftType)) && !isEquality && !isLogical)
{
@ -1075,14 +1083,13 @@ struct TypeChecker2
reportError(CannotInferBinaryOperation{expr->op, name,
isComparison ? CannotInferBinaryOperation::OpKind::Comparison : CannotInferBinaryOperation::OpKind::Operation},
expr->location);
return;
return leftType;
}
if (auto it = kBinaryOpMetamethods.find(expr->op); it != kBinaryOpMetamethods.end())
{
std::optional<TypeId> leftMt = getMetatable(leftType, builtinTypes);
std::optional<TypeId> rightMt = getMetatable(rightType, builtinTypes);
bool matches = leftMt == rightMt;
if (isEquality && !matches)
{
@ -1114,7 +1121,7 @@ struct TypeChecker2
toString(leftType).c_str(), toString(rightType).c_str(), toString(expr->op).c_str())},
expr->location);
return;
return builtinTypes->errorRecoveryType();
}
std::optional<TypeId> mm;
@ -1128,7 +1135,11 @@ struct TypeChecker2
if (mm)
{
TypeId instantiatedMm = module->astOverloadResolvedTypes[expr];
void* key = expr;
if (overrideKey != nullptr)
key = overrideKey;
TypeId instantiatedMm = module->astOverloadResolvedTypes[key];
if (!instantiatedMm)
reportError(CodeTooComplex{}, expr->location);
@ -1146,20 +1157,50 @@ struct TypeChecker2
expectedArgs = testArena.addTypePack({leftType, rightType});
}
reportErrors(tryUnify(scope, expr->location, ftv->argTypes, expectedArgs));
TypePackId expectedRets;
if (expr->op == AstExprBinary::CompareEq || expr->op == AstExprBinary::CompareNe || expr->op == AstExprBinary::CompareGe ||
expr->op == AstExprBinary::CompareGt || expr->op == AstExprBinary::Op::CompareLe || expr->op == AstExprBinary::Op::CompareLt)
{
TypePackId expectedRets = testArena.addTypePack({builtinTypes->booleanType});
if (!isSubtype(ftv->retTypes, expectedRets, scope))
expectedRets = testArena.addTypePack({builtinTypes->booleanType});
}
else
{
expectedRets = testArena.addTypePack({testArena.freshType(scope, TypeLevel{})});
}
TypeId expectedTy = testArena.addType(FunctionType(expectedArgs, expectedRets));
reportErrors(tryUnify(scope, expr->location, follow(*mm), expectedTy));
std::optional<TypeId> ret = first(ftv->retTypes);
if (ret)
{
if (isComparison)
{
reportError(GenericError{format("Metamethod '%s' must return type 'boolean'", it->second)}, expr->location);
if (!isBoolean(follow(*ret)))
{
reportError(GenericError{format("Metamethod '%s' must return a boolean", it->second)}, expr->location);
}
return builtinTypes->booleanType;
}
else
{
return follow(*ret);
}
}
else if (!first(ftv->retTypes))
else
{
reportError(GenericError{format("Metamethod '%s' must return a value", it->second)}, expr->location);
if (isComparison)
{
reportError(GenericError{format("Metamethod '%s' must return a boolean", it->second)}, expr->location);
}
else
{
reportError(GenericError{format("Metamethod '%s' must return a value", it->second)}, expr->location);
}
return builtinTypes->errorRecoveryType();
}
}
else
@ -1167,13 +1208,13 @@ struct TypeChecker2
reportError(CannotCallNonFunction{*mm}, expr->location);
}
return;
return builtinTypes->errorRecoveryType();
}
// If this is a string comparison, or a concatenation of strings, we
// want to fall through to primitive behavior.
else if (!isEquality && !(isStringOperation && (expr->op == AstExprBinary::Op::Concat || isComparison)))
{
if (leftMt || rightMt)
if ((leftMt && !isString(leftType)) || (rightMt && !isString(rightType)))
{
if (isComparison)
{
@ -1190,7 +1231,7 @@ struct TypeChecker2
expr->location);
}
return;
return builtinTypes->errorRecoveryType();
}
else if (!leftMt && !rightMt && (get<TableType>(leftType) || get<TableType>(rightType)))
{
@ -1207,7 +1248,7 @@ struct TypeChecker2
expr->location);
}
return;
return builtinTypes->errorRecoveryType();
}
}
}
@ -1223,34 +1264,44 @@ struct TypeChecker2
reportErrors(tryUnify(scope, expr->left->location, leftType, builtinTypes->numberType));
reportErrors(tryUnify(scope, expr->right->location, rightType, builtinTypes->numberType));
break;
return builtinTypes->numberType;
case AstExprBinary::Op::Concat:
reportErrors(tryUnify(scope, expr->left->location, leftType, builtinTypes->stringType));
reportErrors(tryUnify(scope, expr->right->location, rightType, builtinTypes->stringType));
break;
return builtinTypes->stringType;
case AstExprBinary::Op::CompareGe:
case AstExprBinary::Op::CompareGt:
case AstExprBinary::Op::CompareLe:
case AstExprBinary::Op::CompareLt:
if (isNumber(leftType))
{
reportErrors(tryUnify(scope, expr->right->location, rightType, builtinTypes->numberType));
return builtinTypes->numberType;
}
else if (isString(leftType))
{
reportErrors(tryUnify(scope, expr->right->location, rightType, builtinTypes->stringType));
return builtinTypes->stringType;
}
else
{
reportError(GenericError{format("Types '%s' and '%s' cannot be compared with relational operator %s", toString(leftType).c_str(),
toString(rightType).c_str(), toString(expr->op).c_str())},
expr->location);
break;
return builtinTypes->errorRecoveryType();
}
case AstExprBinary::Op::And:
case AstExprBinary::Op::Or:
case AstExprBinary::Op::CompareEq:
case AstExprBinary::Op::CompareNe:
break;
// Ugly case: we don't care about this possibility, because a
// compound assignment will never exist with one of these operators.
return builtinTypes->anyType;
default:
// Unhandled AstExprBinary::Op possibility.
LUAU_ASSERT(false);
return builtinTypes->errorRecoveryType();
}
}

View file

@ -16,9 +16,10 @@
#include "Luau/TopoSortStatements.h"
#include "Luau/ToString.h"
#include "Luau/ToString.h"
#include "Luau/TypePack.h"
#include "Luau/TypeUtils.h"
#include "Luau/Type.h"
#include "Luau/TypePack.h"
#include "Luau/TypeReduction.h"
#include "Luau/TypeUtils.h"
#include "Luau/VisitType.h"
#include <algorithm>
@ -35,17 +36,16 @@ LUAU_FASTFLAG(LuauTypeNormalization2)
LUAU_FASTFLAGVARIABLE(DebugLuauFreezeDuringUnification, false)
LUAU_FASTFLAGVARIABLE(LuauReturnAnyInsteadOfICE, false) // Eventually removed as false.
LUAU_FASTFLAGVARIABLE(DebugLuauSharedSelf, false)
LUAU_FASTFLAGVARIABLE(LuauNilIterator, false)
LUAU_FASTFLAGVARIABLE(LuauUnknownAndNeverType, false)
LUAU_FASTFLAGVARIABLE(LuauTypeInferMissingFollows, false)
LUAU_FASTFLAGVARIABLE(LuauBinaryNeedsExpectedTypesToo, false)
LUAU_FASTFLAGVARIABLE(LuauNeverTypesAndOperatorsInference, false)
LUAU_FASTFLAGVARIABLE(LuauScopelessModule, false)
LUAU_FASTFLAGVARIABLE(LuauFollowInLvalueIndexCheck, false)
LUAU_FASTFLAGVARIABLE(LuauReturnsFromCallsitesAreNotWidened, false)
LUAU_FASTFLAGVARIABLE(LuauTryhardAnd, false)
LUAU_FASTFLAG(LuauInstantiateInSubtyping)
LUAU_FASTFLAGVARIABLE(LuauCompleteVisitor, false)
LUAU_FASTFLAGVARIABLE(LuauOptionalNextKey, false)
LUAU_FASTFLAGVARIABLE(LuauReportShadowedTypeAlias, false)
LUAU_FASTFLAGVARIABLE(LuauBetterMessagingOnCountMismatch, false)
LUAU_FASTFLAGVARIABLE(LuauIntersectionTestForEquality, false)
@ -276,6 +276,7 @@ ModulePtr TypeChecker::checkWithoutRecursionCheck(const SourceModule& module, Mo
LUAU_TIMETRACE_ARGUMENT("module", module.name.c_str());
currentModule.reset(new Module);
currentModule->reduction = std::make_unique<TypeReduction>(NotNull{&currentModule->internalTypes}, builtinTypes, NotNull{iceHandler});
currentModule->type = module.type;
currentModule->allocator = module.allocator;
currentModule->names = module.names;
@ -1136,7 +1137,8 @@ void TypeChecker::check(const ScopePtr& scope, const AstStatLocal& local)
const Name name{local.vars.data[i]->name.value};
if (ModulePtr module = resolver->getModule(moduleInfo->name))
scope->importedTypeBindings[name] = module->getModuleScope()->exportedTypeBindings;
scope->importedTypeBindings[name] =
FFlag::LuauScopelessModule ? module->exportedTypeBindings : module->getModuleScope()->exportedTypeBindings;
// In non-strict mode we force the module type on the variable, in strict mode it is already unified
if (isNonstrictMode())
@ -1248,8 +1250,7 @@ void TypeChecker::check(const ScopePtr& scope, const AstStatForIn& forin)
iterTy = instantiate(scope, checkExpr(scope, *firstValue).type, firstValue->location);
}
if (FFlag::LuauNilIterator)
iterTy = stripFromNilAndReport(iterTy, firstValue->location);
iterTy = stripFromNilAndReport(iterTy, firstValue->location);
if (std::optional<TypeId> iterMM = findMetatableEntry(iterTy, "__iter", firstValue->location, /* addErrors= */ true))
{
@ -1334,62 +1335,41 @@ void TypeChecker::check(const ScopePtr& scope, const AstStatForIn& forin)
reportErrors(state.errors);
}
if (FFlag::LuauOptionalNextKey)
TypePackId retPack = iterFunc->retTypes;
if (forin.values.size >= 2)
{
TypePackId retPack = iterFunc->retTypes;
AstArray<AstExpr*> arguments{forin.values.data + 1, forin.values.size - 1};
if (forin.values.size >= 2)
{
AstArray<AstExpr*> arguments{forin.values.data + 1, forin.values.size - 1};
Position start = firstValue->location.begin;
Position end = values[forin.values.size - 1]->location.end;
AstExprCall exprCall{Location(start, end), firstValue, arguments, /* self= */ false, Location()};
Position start = firstValue->location.begin;
Position end = values[forin.values.size - 1]->location.end;
AstExprCall exprCall{Location(start, end), firstValue, arguments, /* self= */ false, Location()};
retPack = checkExprPack(scope, exprCall).type;
}
// We need to remove 'nil' from the set of options of the first return value
// Because for loop stops when it gets 'nil', this result is never actually assigned to the first variable
if (std::optional<TypeId> fty = first(retPack); fty && !varTypes.empty())
{
TypeId keyTy = follow(*fty);
if (get<UnionType>(keyTy))
{
if (std::optional<TypeId> ty = tryStripUnionFromNil(keyTy))
keyTy = *ty;
}
unify(keyTy, varTypes.front(), scope, forin.location);
// We have already handled the first variable type, make it match in the pack check
varTypes.front() = *fty;
}
TypePackId varPack = addTypePack(TypePackVar{TypePack{varTypes, freshTypePack(scope)}});
unify(retPack, varPack, scope, forin.location);
retPack = checkExprPack(scope, exprCall).type;
}
else
// We need to remove 'nil' from the set of options of the first return value
// Because for loop stops when it gets 'nil', this result is never actually assigned to the first variable
if (std::optional<TypeId> fty = first(retPack); fty && !varTypes.empty())
{
TypePackId varPack = addTypePack(TypePackVar{TypePack{varTypes, freshTypePack(scope)}});
TypeId keyTy = follow(*fty);
if (forin.values.size >= 2)
if (get<UnionType>(keyTy))
{
AstArray<AstExpr*> arguments{forin.values.data + 1, forin.values.size - 1};
Position start = firstValue->location.begin;
Position end = values[forin.values.size - 1]->location.end;
AstExprCall exprCall{Location(start, end), firstValue, arguments, /* self= */ false, Location()};
TypePackId retPack = checkExprPack(scope, exprCall).type;
unify(retPack, varPack, scope, forin.location);
if (std::optional<TypeId> ty = tryStripUnionFromNil(keyTy))
keyTy = *ty;
}
else
unify(iterFunc->retTypes, varPack, scope, forin.location);
unify(keyTy, varTypes.front(), scope, forin.location);
// We have already handled the first variable type, make it match in the pack check
varTypes.front() = *fty;
}
TypePackId varPack = addTypePack(TypePackVar{TypePack{varTypes, freshTypePack(scope)}});
unify(retPack, varPack, scope, forin.location);
check(loopScope, *forin.body);
}
@ -4685,7 +4665,7 @@ TypeId TypeChecker::checkRequire(const ScopePtr& scope, const ModuleInfo& module
return errorRecoveryType(scope);
}
TypePackId modulePack = module->getModuleScope()->returnType;
TypePackId modulePack = FFlag::LuauScopelessModule ? module->returnType : module->getModuleScope()->returnType;
if (get<Unifiable::Error>(modulePack))
return errorRecoveryType(scope);

View file

@ -0,0 +1,838 @@
// This file is part of the Luau programming language and is licensed under MIT License; see LICENSE.txt for details
#include "Luau/TypeReduction.h"
#include "Luau/Common.h"
#include "Luau/Error.h"
#include "Luau/RecursionCounter.h"
#include <numeric>
#include <deque>
LUAU_FASTINTVARIABLE(LuauTypeReductionCartesianProductLimit, 100'000)
LUAU_FASTINTVARIABLE(LuauTypeReductionRecursionLimit, 700)
LUAU_FASTFLAGVARIABLE(DebugLuauDontReduceTypes, false)
namespace Luau
{
namespace
{
struct RecursionGuard : RecursionLimiter
{
std::deque<const void*>* seen;
RecursionGuard(int* count, int limit, std::deque<const void*>* seen)
: RecursionLimiter(count, limit)
, seen(seen)
{
// count has been incremented, which should imply that seen has already had an element pushed in.
LUAU_ASSERT(size_t(*count) == seen->size());
}
~RecursionGuard()
{
LUAU_ASSERT(!seen->empty()); // It is UB to pop_back() on an empty deque.
seen->pop_back();
}
};
template<typename A, typename B, typename Thing>
std::pair<const A*, const B*> get2(const Thing& one, const Thing& two)
{
const A* a = get<A>(one);
const B* b = get<B>(two);
return a && b ? std::make_pair(a, b) : std::make_pair(nullptr, nullptr);
}
struct TypeReducer
{
NotNull<TypeArena> arena;
NotNull<BuiltinTypes> builtinTypes;
NotNull<InternalErrorReporter> handle;
TypeId reduce(TypeId ty);
TypePackId reduce(TypePackId tp);
std::optional<TypeId> intersectionType(TypeId left, TypeId right);
std::optional<TypeId> unionType(TypeId left, TypeId right);
TypeId tableType(TypeId ty);
TypeId functionType(TypeId ty);
TypeId negationType(TypeId ty);
std::deque<const void*> seen;
int depth = 0;
RecursionGuard guard(TypeId ty);
RecursionGuard guard(TypePackId tp);
std::unordered_map<TypeId, TypeId> copies;
template<typename T>
LUAU_NOINLINE std::pair<TypeId, T*> copy(TypeId ty, const T* t)
{
if (auto it = copies.find(ty); it != copies.end())
return {it->second, getMutable<T>(it->second)};
TypeId copiedTy = arena->addType(*t);
copies[ty] = copiedTy;
return {copiedTy, getMutable<T>(copiedTy)};
}
using Folder = std::optional<TypeId> (TypeReducer::*)(TypeId, TypeId);
template<typename T, typename Iter>
void foldl_impl(Iter it, Iter endIt, Folder f, NotNull<std::vector<TypeId>> result)
{
while (it != endIt)
{
bool replaced = false;
TypeId currentTy = reduce(*it);
RecursionGuard rg = guard(*it);
// We're hitting a case where the `currentTy` returned a type that's the same as `T`.
// e.g. `(string?) & ~(false | nil)` became `(string?) & (~false & ~nil)` but the current iterator we're consuming doesn't know this.
// We will need to recurse and traverse that first.
if (auto t = get<T>(currentTy))
{
foldl_impl<T>(begin(t), end(t), f, result);
++it;
continue;
}
auto resultIt = result->begin();
while (resultIt != result->end())
{
TypeId& ty = *resultIt;
std::optional<TypeId> reduced = (this->*f)(ty, currentTy);
if (reduced && replaced)
{
// We want to erase any other elements that occurs after the first replacement too.
// e.g. `"a" | "b" | string` where `"a"` and `"b"` is in the `result` vector, then `string` replaces both `"a"` and `"b"`.
// If we don't erase redundant elements, `"b"` may be kept or be replaced by `string`, leaving us with `string | string`.
resultIt = result->erase(resultIt);
}
else if (reduced && !replaced)
{
++resultIt;
replaced = true;
ty = *reduced;
}
else
{
++resultIt;
continue;
}
}
if (!replaced)
result->push_back(currentTy);
++it;
}
}
template<typename T, typename Iter>
TypeId foldl(Iter it, Iter endIt, Folder f)
{
std::vector<TypeId> result;
foldl_impl<T>(it, endIt, f, NotNull{&result});
if (result.size() == 1)
return result[0];
else
return arena->addType(T{std::move(result)});
}
};
TypeId TypeReducer::reduce(TypeId ty)
{
ty = follow(ty);
if (std::find(seen.begin(), seen.end(), ty) != seen.end())
return ty;
RecursionGuard rg = guard(ty);
if (auto i = get<IntersectionType>(ty))
return foldl<IntersectionType>(begin(i), end(i), &TypeReducer::intersectionType);
else if (auto u = get<UnionType>(ty))
return foldl<UnionType>(begin(u), end(u), &TypeReducer::unionType);
else if (get<TableType>(ty) || get<MetatableType>(ty))
return tableType(ty);
else if (get<FunctionType>(ty))
return functionType(ty);
else if (auto n = get<NegationType>(ty))
return negationType(follow(n->ty));
else
return ty;
}
TypePackId TypeReducer::reduce(TypePackId tp)
{
tp = follow(tp);
if (std::find(seen.begin(), seen.end(), tp) != seen.end())
return tp;
RecursionGuard rg = guard(tp);
TypePackIterator it = begin(tp);
std::vector<TypeId> head;
while (it != end(tp))
{
head.push_back(reduce(*it));
++it;
}
std::optional<TypePackId> tail = it.tail();
if (tail)
{
if (auto vtp = get<VariadicTypePack>(follow(*it.tail())))
tail = arena->addTypePack(VariadicTypePack{reduce(vtp->ty), vtp->hidden});
}
return arena->addTypePack(TypePack{std::move(head), tail});
}
std::optional<TypeId> TypeReducer::intersectionType(TypeId left, TypeId right)
{
LUAU_ASSERT(!get<IntersectionType>(left));
LUAU_ASSERT(!get<IntersectionType>(right));
if (get<NeverType>(left))
return left; // never & T ~ never
else if (get<NeverType>(right))
return right; // T & never ~ never
else if (get<UnknownType>(left))
return right; // unknown & T ~ T
else if (get<UnknownType>(right))
return left; // T & unknown ~ T
else if (get<AnyType>(left))
return right; // any & T ~ T
else if (get<AnyType>(right))
return left; // T & any ~ T
else if (get<ErrorType>(left))
return std::nullopt; // error & T ~ error & T
else if (get<ErrorType>(right))
return std::nullopt; // T & error ~ T & error
else if (auto ut = get<UnionType>(left))
{
std::vector<TypeId> options;
for (TypeId option : ut)
{
if (auto result = intersectionType(option, right))
options.push_back(*result);
else
options.push_back(arena->addType(IntersectionType{{option, right}}));
}
return foldl<UnionType>(begin(options), end(options), &TypeReducer::unionType); // (A | B) & T ~ (A & T) | (B & T)
}
else if (get<UnionType>(right))
return intersectionType(right, left); // T & (A | B) ~ (A | B) & T
else if (auto [p1, p2] = get2<PrimitiveType, PrimitiveType>(left, right); p1 && p2)
{
if (p1->type == p2->type)
return left; // P1 & P2 ~ P1 iff P1 == P2
else
return builtinTypes->neverType; // P1 & P2 ~ never iff P1 != P2
}
else if (auto [p, s] = get2<PrimitiveType, SingletonType>(left, right); p && s)
{
if (p->type == PrimitiveType::String && get<StringSingleton>(s))
return right; // string & "A" ~ "A"
else if (p->type == PrimitiveType::Boolean && get<BooleanSingleton>(s))
return right; // boolean & true ~ true
else
return builtinTypes->neverType; // string & true ~ never
}
else if (auto [s, p] = get2<SingletonType, PrimitiveType>(left, right); s && p)
return intersectionType(right, left); // S & P ~ P & S
else if (auto [p, f] = get2<PrimitiveType, FunctionType>(left, right); p && f)
{
if (p->type == PrimitiveType::Function)
return right; // function & () -> () ~ () -> ()
else
return builtinTypes->neverType; // string & () -> () ~ never
}
else if (auto [f, p] = get2<FunctionType, PrimitiveType>(left, right); f && p)
return intersectionType(right, left); // () -> () & P ~ P & () -> ()
else if (auto [s1, s2] = get2<SingletonType, SingletonType>(left, right); s1 && s2)
{
if (*s1 == *s2)
return left; // "a" & "a" ~ "a"
else
return builtinTypes->neverType; // "a" & "b" ~ never
}
else if (auto [c1, c2] = get2<ClassType, ClassType>(left, right); c1 && c2)
{
if (isSubclass(c1, c2))
return left; // Derived & Base ~ Derived
else if (isSubclass(c2, c1))
return right; // Base & Derived ~ Derived
else
return builtinTypes->neverType; // Base & Unrelated ~ never
}
else if (auto [f1, f2] = get2<FunctionType, FunctionType>(left, right); f1 && f2)
{
if (std::find(seen.begin(), seen.end(), left) != seen.end())
return std::nullopt;
else if (std::find(seen.begin(), seen.end(), right) != seen.end())
return std::nullopt;
return std::nullopt; // TODO
}
else if (auto [t1, t2] = get2<TableType, TableType>(left, right); t1 && t2)
{
if (t1->state == TableState::Free || t2->state == TableState::Free)
return std::nullopt; // '{ x: T } & { x: U } ~ '{ x: T } & { x: U }
else if (t1->state == TableState::Generic || t2->state == TableState::Generic)
return std::nullopt; // '{ x: T } & { x: U } ~ '{ x: T } & { x: U }
if (std::find(seen.begin(), seen.end(), left) != seen.end())
return std::nullopt;
else if (std::find(seen.begin(), seen.end(), right) != seen.end())
return std::nullopt;
TypeId resultTy = arena->addType(TableType{});
TableType* table = getMutable<TableType>(resultTy);
table->state = t1->state == TableState::Sealed || t2->state == TableState::Sealed ? TableState::Sealed : TableState::Unsealed;
for (const auto& [name, prop] : t1->props)
{
// TODO: when t1 has properties, we should also intersect that with the indexer in t2 if it exists,
// even if we have the corresponding property in the other one.
if (auto other = t2->props.find(name); other != t2->props.end())
{
std::vector<TypeId> parts{prop.type, other->second.type};
TypeId propTy = foldl<IntersectionType>(begin(parts), end(parts), &TypeReducer::intersectionType);
if (get<NeverType>(propTy))
return builtinTypes->neverType; // { p : string } & { p : number } ~ { p : string & number } ~ { p : never } ~ never
else
table->props[name] = {propTy}; // { p : string } & { p : ~"a" } ~ { p : string & ~"a" }
}
else
table->props[name] = prop; // { p : string } & {} ~ { p : string }
}
for (const auto& [name, prop] : t2->props)
{
// TODO: And vice versa, t2 properties against t1 indexer if it exists,
// even if we have the corresponding property in the other one.
if (!t1->props.count(name))
table->props[name] = prop; // {} & { p : string } ~ { p : string }
}
if (t1->indexer && t2->indexer)
{
std::vector<TypeId> keyParts{t1->indexer->indexType, t2->indexer->indexType};
TypeId keyTy = foldl<IntersectionType>(begin(keyParts), end(keyParts), &TypeReducer::intersectionType);
if (get<NeverType>(keyTy))
return builtinTypes->neverType; // { [string]: _ } & { [number]: _ } ~ { [string & number]: _ } ~ { [never]: _ } ~ never
std::vector<TypeId> valueParts{t1->indexer->indexResultType, t2->indexer->indexResultType};
TypeId valueTy = foldl<IntersectionType>(begin(valueParts), end(valueParts), &TypeReducer::intersectionType);
if (get<NeverType>(valueTy))
return builtinTypes->neverType; // { [_]: string } & { [_]: number } ~ { [_]: string & number } ~ { [_]: never } ~ never
table->indexer = TableIndexer{keyTy, valueTy};
}
else if (t1->indexer)
table->indexer = t1->indexer; // { [number]: boolean } & { p : string } ~ { p : string, [number]: boolean }
else if (t2->indexer)
table->indexer = t2->indexer; // { p : string } & { [number]: boolean } ~ { p : string, [number]: boolean }
return resultTy;
}
else if (auto [mt, tt] = get2<MetatableType, TableType>(left, right); mt && tt)
return std::nullopt; // TODO
else if (auto [tt, mt] = get2<TableType, MetatableType>(left, right); tt && mt)
return intersectionType(right, left); // T & M ~ M & T
else if (auto [m1, m2] = get2<MetatableType, MetatableType>(left, right); m1 && m2)
return std::nullopt; // TODO
else if (auto nl = get<NegationType>(left))
{
// These should've been reduced already.
TypeId nlTy = follow(nl->ty);
LUAU_ASSERT(!get<UnknownType>(nlTy));
LUAU_ASSERT(!get<NeverType>(nlTy));
LUAU_ASSERT(!get<AnyType>(nlTy));
LUAU_ASSERT(!get<IntersectionType>(nlTy));
LUAU_ASSERT(!get<UnionType>(nlTy));
if (auto [np, p] = get2<PrimitiveType, PrimitiveType>(nlTy, right); np && p)
{
if (np->type == p->type)
return builtinTypes->neverType; // ~P1 & P2 ~ never iff P1 == P2
else
return right; // ~P1 & P2 ~ P2 iff P1 != P2
}
else if (auto [ns, s] = get2<SingletonType, SingletonType>(nlTy, right); ns && s)
{
if (*ns == *s)
return builtinTypes->neverType; // ~"A" & "A" ~ never
else
return right; // ~"A" & "B" ~ "B"
}
else if (auto [ns, p] = get2<SingletonType, PrimitiveType>(nlTy, right); ns && p)
{
if (get<StringSingleton>(ns) && p->type == PrimitiveType::String)
return std::nullopt; // ~"A" & string ~ ~"A" & string
else if (get<BooleanSingleton>(ns) && p->type == PrimitiveType::Boolean)
{
// Because booleans contain a fixed amount of values (2), we can do something cooler with this one.
const BooleanSingleton* b = get<BooleanSingleton>(ns);
return arena->addType(SingletonType{BooleanSingleton{!b->value}}); // ~false & boolean ~ true
}
else
return right; // ~"A" & number ~ number
}
else if (auto [np, s] = get2<PrimitiveType, SingletonType>(nlTy, right); np && s)
{
if (np->type == PrimitiveType::String && get<StringSingleton>(s))
return builtinTypes->neverType; // ~string & "A" ~ never
else if (np->type == PrimitiveType::Boolean && get<BooleanSingleton>(s))
return builtinTypes->neverType; // ~boolean & true ~ never
else
return right; // ~P & "A" ~ "A"
}
else if (auto [np, f] = get2<PrimitiveType, FunctionType>(nlTy, right); np && f)
{
if (np->type == PrimitiveType::Function)
return builtinTypes->neverType; // ~function & () -> () ~ never
else
return right; // ~string & () -> () ~ () -> ()
}
else if (auto [nc, c] = get2<ClassType, ClassType>(nlTy, right); nc && c)
{
if (isSubclass(nc, c))
return std::nullopt; // ~Derived & Base ~ ~Derived & Base
else if (isSubclass(c, nc))
return builtinTypes->neverType; // ~Base & Derived ~ never
else
return right; // ~Base & Unrelated ~ Unrelated
}
else
return std::nullopt; // TODO
}
else if (get<NegationType>(right))
return intersectionType(right, left); // T & ~U ~ ~U & T
else
return builtinTypes->neverType; // for all T and U except the ones handled above, T & U ~ never
}
std::optional<TypeId> TypeReducer::unionType(TypeId left, TypeId right)
{
LUAU_ASSERT(!get<UnionType>(left));
LUAU_ASSERT(!get<UnionType>(right));
if (get<NeverType>(left))
return right; // never | T ~ T
else if (get<NeverType>(right))
return left; // T | never ~ T
else if (get<UnknownType>(left))
return left; // unknown | T ~ unknown
else if (get<UnknownType>(right))
return right; // T | unknown ~ unknown
else if (get<AnyType>(left))
return left; // any | T ~ any
else if (get<AnyType>(right))
return right; // T | any ~ any
else if (get<ErrorType>(left))
return std::nullopt; // error | T ~ error | T
else if (get<ErrorType>(right))
return std::nullopt; // T | error ~ T | error
else if (auto [p1, p2] = get2<PrimitiveType, PrimitiveType>(left, right); p1 && p2)
{
if (p1->type == p2->type)
return left; // P1 | P2 ~ P1 iff P1 == P2
else
return std::nullopt; // P1 | P2 ~ P1 | P2 iff P1 != P2
}
else if (auto [p, s] = get2<PrimitiveType, SingletonType>(left, right); p && s)
{
if (p->type == PrimitiveType::String && get<StringSingleton>(s))
return left; // string | "A" ~ string
else if (p->type == PrimitiveType::Boolean && get<BooleanSingleton>(s))
return left; // boolean | true ~ boolean
else
return std::nullopt; // string | true ~ string | true
}
else if (auto [s, p] = get2<SingletonType, PrimitiveType>(left, right); s && p)
return unionType(right, left); // S | P ~ P | S
else if (auto [p, f] = get2<PrimitiveType, FunctionType>(left, right); p && f)
{
if (p->type == PrimitiveType::Function)
return left; // function | () -> () ~ function
else
return std::nullopt; // P | () -> () ~ P | () -> ()
}
else if (auto [f, p] = get2<FunctionType, PrimitiveType>(left, right); f && p)
return unionType(right, left); // () -> () | P ~ P | () -> ()
else if (auto [s1, s2] = get2<SingletonType, SingletonType>(left, right); s1 && s2)
{
if (*s1 == *s2)
return left; // "a" | "a" ~ "a"
else
return std::nullopt; // "a" | "b" ~ "a" | "b"
}
else if (auto [c1, c2] = get2<ClassType, ClassType>(left, right); c1 && c2)
{
if (isSubclass(c1, c2))
return right; // Derived | Base ~ Base
else if (isSubclass(c2, c1))
return left; // Base | Derived ~ Base
else
return std::nullopt; // Base | Unrelated ~ Base | Unrelated
}
else if (auto [nt, it] = get2<NegationType, IntersectionType>(left, right); nt && it)
{
std::vector<TypeId> parts;
for (TypeId option : it)
{
if (auto result = unionType(left, option))
parts.push_back(*result);
else
{
// TODO: does there exist a reduced form such that `~T | A` hasn't already reduced it, if `A & B` is irreducible?
// I want to say yes, but I can't generate a case that hits this code path.
parts.push_back(arena->addType(UnionType{{left, option}}));
}
}
return foldl<IntersectionType>(begin(parts), end(parts), &TypeReducer::intersectionType); // ~T | (A & B) ~ (~T | A) & (~T | B)
}
else if (auto [it, nt] = get2<IntersectionType, NegationType>(left, right); it && nt)
return unionType(right, left); // (A & B) | ~T ~ ~T | (A & B)
else if (auto [nl, nr] = get2<NegationType, NegationType>(left, right); nl && nr)
{
// These should've been reduced already.
TypeId nlTy = follow(nl->ty);
TypeId nrTy = follow(nr->ty);
LUAU_ASSERT(!get<UnknownType>(nlTy) && !get<UnknownType>(nrTy));
LUAU_ASSERT(!get<NeverType>(nlTy) && !get<NeverType>(nrTy));
LUAU_ASSERT(!get<AnyType>(nlTy) && !get<AnyType>(nrTy));
LUAU_ASSERT(!get<IntersectionType>(nlTy) && !get<IntersectionType>(nrTy));
LUAU_ASSERT(!get<UnionType>(nlTy) && !get<UnionType>(nrTy));
if (auto [npl, npr] = get2<PrimitiveType, PrimitiveType>(nlTy, nrTy); npl && npr)
{
if (npl->type == npr->type)
return left; // ~P1 | ~P2 ~ ~P1 iff P1 == P2
else
return builtinTypes->unknownType; // ~P1 | ~P2 ~ ~P1 iff P1 != P2
}
else if (auto [nsl, nsr] = get2<SingletonType, SingletonType>(nlTy, nrTy); nsl && nsr)
{
if (*nsl == *nsr)
return left; // ~"A" | ~"A" ~ ~"A"
else
return builtinTypes->unknownType; // ~"A" | ~"B" ~ unknown
}
else if (auto [ns, np] = get2<SingletonType, PrimitiveType>(nlTy, nrTy); ns && np)
{
if (get<StringSingleton>(ns) && np->type == PrimitiveType::String)
return left; // ~"A" | ~string ~ ~"A"
else if (get<BooleanSingleton>(ns) && np->type == PrimitiveType::Boolean)
return left; // ~false | ~boolean ~ ~false
else
return builtinTypes->unknownType; // ~"A" | ~P ~ unknown
}
else if (auto [np, ns] = get2<PrimitiveType, SingletonType>(nlTy, nrTy); np && ns)
return unionType(right, left); // ~P | ~S ~ ~S | ~P
else
return std::nullopt; // TODO!
}
else if (auto nl = get<NegationType>(left))
{
// These should've been reduced already.
TypeId nlTy = follow(nl->ty);
LUAU_ASSERT(!get<UnknownType>(nlTy));
LUAU_ASSERT(!get<NeverType>(nlTy));
LUAU_ASSERT(!get<AnyType>(nlTy));
LUAU_ASSERT(!get<IntersectionType>(nlTy));
LUAU_ASSERT(!get<UnionType>(nlTy));
if (auto [np, p] = get2<PrimitiveType, PrimitiveType>(nlTy, right); np && p)
{
if (np->type == p->type)
return builtinTypes->unknownType; // ~P1 | P2 ~ unknown iff P1 == P2
else
return left; // ~P1 | P2 ~ ~P1 iff P1 != P2
}
else if (auto [ns, s] = get2<SingletonType, SingletonType>(nlTy, right); ns && s)
{
if (*ns == *s)
return builtinTypes->unknownType; // ~"A" | "A" ~ unknown
else
return left; // ~"A" | "B" ~ ~"A"
}
else if (auto [ns, p] = get2<SingletonType, PrimitiveType>(nlTy, right); ns && p)
{
if (get<StringSingleton>(ns) && p->type == PrimitiveType::String)
return builtinTypes->unknownType; // ~"A" | string ~ unknown
else if (get<BooleanSingleton>(ns) && p->type == PrimitiveType::Boolean)
return builtinTypes->unknownType; // ~false | boolean ~ unknown
else
return left; // ~"A" | T ~ ~"A"
}
else if (auto [np, s] = get2<PrimitiveType, SingletonType>(nlTy, right); np && s)
{
if (np->type == PrimitiveType::String && get<StringSingleton>(s))
return std::nullopt; // ~string | "A" ~ ~string | "A"
else if (np->type == PrimitiveType::Boolean && get<BooleanSingleton>(s))
{
const BooleanSingleton* b = get<BooleanSingleton>(s);
return negationType(arena->addType(SingletonType{BooleanSingleton{!b->value}})); // ~boolean | false ~ ~true
}
else
return left; // ~P | "A" ~ ~P
}
else if (auto [nc, c] = get2<ClassType, ClassType>(nlTy, right); nc && c)
{
if (isSubclass(nc, c))
return builtinTypes->unknownType; // ~Derived | Base ~ unknown
else if (isSubclass(c, nc))
return std::nullopt; // ~Base | Derived ~ ~Base | Derived
else
return left; // ~Base | Unrelated ~ ~Base
}
else
return std::nullopt; // TODO
}
else if (get<NegationType>(right))
return unionType(right, left); // T | ~U ~ ~U | T
else
return std::nullopt; // for all T and U except the ones handled above, T | U ~ T | U
}
TypeId TypeReducer::tableType(TypeId ty)
{
RecursionGuard rg = guard(ty);
if (auto mt = get<MetatableType>(ty))
{
auto [copiedTy, copied] = copy(ty, mt);
copied->table = reduce(mt->table);
copied->metatable = reduce(mt->metatable);
return copiedTy;
}
else if (auto tt = get<TableType>(ty))
{
auto [copiedTy, copied] = copy(ty, tt);
for (auto& [name, prop] : copied->props)
prop.type = reduce(prop.type);
if (auto& indexer = copied->indexer)
{
indexer->indexType = reduce(indexer->indexType);
indexer->indexResultType = reduce(indexer->indexResultType);
}
for (TypeId& ty : copied->instantiatedTypeParams)
ty = reduce(ty);
for (TypePackId& tp : copied->instantiatedTypePackParams)
tp = reduce(tp);
return copiedTy;
}
else
handle->ice("Unexpected type in TypeReducer::tableType");
}
TypeId TypeReducer::functionType(TypeId ty)
{
RecursionGuard rg = guard(ty);
const FunctionType* f = get<FunctionType>(ty);
if (!f)
handle->ice("TypeReducer::reduce expects a FunctionType");
// TODO: once we have bounded quantification, we need to be able to reduce the generic bounds.
auto [copiedTy, copied] = copy(ty, f);
copied->argTypes = reduce(f->argTypes);
copied->retTypes = reduce(f->retTypes);
return copiedTy;
}
TypeId TypeReducer::negationType(TypeId ty)
{
RecursionGuard rg = guard(ty);
if (auto nn = get<NegationType>(ty))
return nn->ty; // ~~T ~ T
else if (get<NeverType>(ty))
return builtinTypes->unknownType; // ~never ~ unknown
else if (get<UnknownType>(ty))
return builtinTypes->neverType; // ~unknown ~ never
else if (get<AnyType>(ty))
return builtinTypes->anyType; // ~any ~ any
else if (auto ni = get<IntersectionType>(ty))
{
std::vector<TypeId> options;
for (TypeId part : ni)
options.push_back(negationType(part));
return foldl<UnionType>(begin(options), end(options), &TypeReducer::unionType); // ~(T & U) ~ (~T | ~U)
}
else if (auto nu = get<UnionType>(ty))
{
std::vector<TypeId> parts;
for (TypeId option : nu)
parts.push_back(negationType(option));
return foldl<IntersectionType>(begin(parts), end(parts), &TypeReducer::intersectionType); // ~(T | U) ~ (~T & ~U)
}
else
return arena->addType(NegationType{ty}); // for all T except the ones handled above, ~T ~ ~T
}
RecursionGuard TypeReducer::guard(TypeId ty)
{
seen.push_back(ty);
return RecursionGuard{&depth, FInt::LuauTypeReductionRecursionLimit, &seen};
}
RecursionGuard TypeReducer::guard(TypePackId tp)
{
seen.push_back(tp);
return RecursionGuard{&depth, FInt::LuauTypeReductionRecursionLimit, &seen};
}
} // namespace
TypeReduction::TypeReduction(NotNull<TypeArena> arena, NotNull<BuiltinTypes> builtinTypes, NotNull<InternalErrorReporter> handle)
: arena(arena)
, builtinTypes(builtinTypes)
, handle(handle)
{
}
std::optional<TypeId> TypeReduction::reduce(TypeId ty)
{
if (auto found = cachedTypes.find(ty))
return *found;
if (auto reduced = reduceImpl(ty))
{
cachedTypes[ty] = *reduced;
return *reduced;
}
return std::nullopt;
}
std::optional<TypePackId> TypeReduction::reduce(TypePackId tp)
{
if (auto found = cachedTypePacks.find(tp))
return *found;
if (auto reduced = reduceImpl(tp))
{
cachedTypePacks[tp] = *reduced;
return *reduced;
}
return std::nullopt;
}
std::optional<TypeId> TypeReduction::reduceImpl(TypeId ty)
{
if (FFlag::DebugLuauDontReduceTypes)
return ty;
if (hasExceededCartesianProductLimit(ty))
return std::nullopt;
try
{
TypeReducer reducer{arena, builtinTypes, handle};
return reducer.reduce(ty);
}
catch (const RecursionLimitException&)
{
return std::nullopt;
}
}
std::optional<TypePackId> TypeReduction::reduceImpl(TypePackId tp)
{
if (FFlag::DebugLuauDontReduceTypes)
return tp;
if (hasExceededCartesianProductLimit(tp))
return std::nullopt;
try
{
TypeReducer reducer{arena, builtinTypes, handle};
return reducer.reduce(tp);
}
catch (const RecursionLimitException&)
{
return std::nullopt;
}
}
std::optional<TypeFun> TypeReduction::reduce(const TypeFun& fun)
{
if (FFlag::DebugLuauDontReduceTypes)
return fun;
// TODO: once we have bounded quantification, we need to be able to reduce the generic bounds.
if (auto reducedTy = reduce(fun.type))
return TypeFun{fun.typeParams, fun.typePackParams, *reducedTy};
return std::nullopt;
}
size_t TypeReduction::cartesianProductSize(TypeId ty) const
{
ty = follow(ty);
auto it = get<IntersectionType>(follow(ty));
if (!it)
return 1;
return std::accumulate(begin(it), end(it), size_t(1), [](size_t acc, TypeId ty) {
if (auto ut = get<UnionType>(ty))
return acc * std::distance(begin(ut), end(ut));
else if (get<NeverType>(ty))
return acc * 0;
else
return acc * 1;
});
}
bool TypeReduction::hasExceededCartesianProductLimit(TypeId ty) const
{
return cartesianProductSize(ty) >= size_t(FInt::LuauTypeReductionCartesianProductLimit);
}
bool TypeReduction::hasExceededCartesianProductLimit(TypePackId tp) const
{
TypePackIterator it = begin(tp);
while (it != end(tp))
{
if (hasExceededCartesianProductLimit(*it))
return true;
++it;
}
if (auto tail = it.tail())
{
if (auto vtp = get<VariadicTypePack>(follow(*tail)))
{
if (hasExceededCartesianProductLimit(vtp->ty))
return true;
}
}
return false;
}
} // namespace Luau

View file

@ -27,6 +27,7 @@ LUAU_FASTINTVARIABLE(LuauCompileInlineDepth, 5)
LUAU_FASTFLAG(LuauInterpolatedStringBaseSupport)
LUAU_FASTFLAGVARIABLE(LuauMultiAssignmentConflictFix, false)
LUAU_FASTFLAGVARIABLE(LuauSelfAssignmentSkip, false)
namespace Luau
{
@ -2027,7 +2028,9 @@ struct Compiler
// note: this can't check expr->upvalue because upvalues may be upgraded to locals during inlining
if (int reg = getExprLocalReg(expr); reg >= 0)
{
bytecode.emitABC(LOP_MOVE, target, uint8_t(reg), 0);
// Optimization: we don't need to move if target happens to be in the same register
if (!FFlag::LuauSelfAssignmentSkip || options.optimizationLevel == 0 || target != reg)
bytecode.emitABC(LOP_MOVE, target, uint8_t(reg), 0);
}
else
{

View file

@ -146,6 +146,7 @@ target_sources(Luau.Analysis PRIVATE
Analysis/include/Luau/TypedAllocator.h
Analysis/include/Luau/TypeInfer.h
Analysis/include/Luau/TypePack.h
Analysis/include/Luau/TypeReduction.h
Analysis/include/Luau/TypeUtils.h
Analysis/include/Luau/Type.h
Analysis/include/Luau/Unifiable.h
@ -195,6 +196,7 @@ target_sources(Luau.Analysis PRIVATE
Analysis/src/TypedAllocator.cpp
Analysis/src/TypeInfer.cpp
Analysis/src/TypePack.cpp
Analysis/src/TypeReduction.cpp
Analysis/src/TypeUtils.cpp
Analysis/src/Type.cpp
Analysis/src/Unifiable.cpp
@ -364,6 +366,7 @@ if(TARGET Luau.UnitTest)
tests/TypeInfer.unionTypes.test.cpp
tests/TypeInfer.unknownnever.test.cpp
tests/TypePack.test.cpp
tests/TypeReduction.test.cpp
tests/TypeVar.test.cpp
tests/Variant.test.cpp
tests/VisitType.test.cpp

View file

@ -3331,4 +3331,36 @@ TEST_CASE_FIXTURE(ACFixture, "globals_are_order_independent")
CHECK(ac.entryMap.count("abc1"));
}
TEST_CASE_FIXTURE(ACFixture, "type_reduction_is_hooked_up_to_autocomplete")
{
check(R"(
type T = { x: (number & string)? }
function f(thingamabob: T)
thingamabob.@1
end
function g(thingamabob: T)
thingama@2
end
)");
ToStringOptions opts;
opts.exhaustive = true;
auto ac1 = autocomplete('1');
REQUIRE(ac1.entryMap.count("x"));
std::optional<TypeId> ty1 = ac1.entryMap.at("x").type;
REQUIRE(ty1);
CHECK("(number & string)?" == toString(*ty1, opts));
// CHECK("nil" == toString(*ty1, opts));
auto ac2 = autocomplete('2');
REQUIRE(ac2.entryMap.count("thingamabob"));
std::optional<TypeId> ty2 = ac2.entryMap.at("thingamabob").type;
REQUIRE(ty2);
CHECK("{| x: (number & string)? |}" == toString(*ty2, opts));
// CHECK("{| x: nil |}" == toString(*ty2, opts));
}
TEST_SUITE_END();

View file

@ -1025,6 +1025,8 @@ L0: RETURN R0 0
TEST_CASE("AndOr")
{
ScopedFastFlag luauSelfAssignmentSkip{"LuauSelfAssignmentSkip", true};
// codegen for constant, local, global for and
CHECK_EQ("\n" + compileFunction0("local a = 1 a = a and 2 return a"), R"(
LOADN R0 1
@ -1079,7 +1081,6 @@ RETURN R0 1
// note: `a = a` assignment is to disable constant folding for testing purposes
CHECK_EQ("\n" + compileFunction0("local a = 1 a = a b = 2 local c = a and b return c"), R"(
LOADN R0 1
MOVE R0 R0
LOADN R1 2
SETGLOBAL R1 K0
MOVE R1 R0
@ -1090,7 +1091,6 @@ L0: RETURN R1 1
CHECK_EQ("\n" + compileFunction0("local a = 1 a = a b = 2 local c = a or b return c"), R"(
LOADN R0 1
MOVE R0 R0
LOADN R1 2
SETGLOBAL R1 K0
MOVE R1 R0
@ -2260,6 +2260,8 @@ L1: RETURN R3 -1
TEST_CASE("UpvaluesLoopsBytecode")
{
ScopedFastFlag luauSelfAssignmentSkip{"LuauSelfAssignmentSkip", true};
CHECK_EQ("\n" + compileFunction(R"(
function test()
for i=1,10 do
@ -2279,7 +2281,6 @@ LOADN R0 10
LOADN R1 1
FORNPREP R0 L2
L0: MOVE R3 R2
MOVE R3 R3
GETIMPORT R4 1
NEWCLOSURE R5 P0
CAPTURE REF R3
@ -2312,8 +2313,7 @@ GETIMPORT R0 1
GETIMPORT R1 3
CALL R0 1 3
FORGPREP_INEXT R0 L2
L0: MOVE R3 R3
GETIMPORT R5 5
L0: GETIMPORT R5 5
NEWCLOSURE R6 P0
CAPTURE REF R3
CALL R5 1 0
@ -5159,6 +5159,8 @@ RETURN R1 1
TEST_CASE("InlineMutate")
{
ScopedFastFlag luauSelfAssignmentSkip{"LuauSelfAssignmentSkip", true};
// if the argument is mutated, it gets a register even if the value is constant
CHECK_EQ("\n" + compileFunction(R"(
local function foo(a)
@ -5231,7 +5233,6 @@ return x
1, 2),
R"(
DUPCLOSURE R0 K0
MOVE R0 R0
MOVE R1 R0
LOADN R2 42
CALL R1 1 1
@ -6790,4 +6791,31 @@ L0: RETURN R1 -1
)");
}
TEST_CASE("SkipSelfAssignment")
{
ScopedFastFlag luauSelfAssignmentSkip{"LuauSelfAssignmentSkip", true};
CHECK_EQ("\n" + compileFunction0("local a a = a"), R"(
LOADNIL R0
RETURN R0 0
)");
CHECK_EQ("\n" + compileFunction0("local a a = a :: number"), R"(
LOADNIL R0
RETURN R0 0
)");
CHECK_EQ("\n" + compileFunction0("local a a = (((a)))"), R"(
LOADNIL R0
RETURN R0 0
)");
// Keep it on optimization level 0
CHECK_EQ("\n" + compileFunction("local a a = a", 0, 0), R"(
LOADNIL R0
MOVE R0 R0
RETURN R0 0
)");
}
TEST_SUITE_END();

View file

@ -1,6 +1,8 @@
// This file is part of the Luau programming language and is licensed under MIT License; see LICENSE.txt for details
#include "ConstraintGraphBuilderFixture.h"
#include "Luau/TypeReduction.h"
namespace Luau
{
@ -9,6 +11,8 @@ ConstraintGraphBuilderFixture::ConstraintGraphBuilderFixture()
, mainModule(new Module)
, forceTheFlag{"DebugLuauDeferredConstraintResolution", true}
{
mainModule->reduction = std::make_unique<TypeReduction>(NotNull{&mainModule->internalTypes}, builtinTypes, NotNull{&ice});
BlockedType::nextIndex = 0;
BlockedTypePack::nextIndex = 0;
}

View file

@ -312,6 +312,9 @@ std::optional<TypeId> Fixture::getType(const std::string& name)
ModulePtr module = getMainModule();
REQUIRE(module);
if (!module->hasModuleScope())
return std::nullopt;
if (FFlag::DebugLuauDeferredConstraintResolution)
return linearSearchForBinding(module->getModuleScope().get(), name.c_str());
else
@ -329,11 +332,14 @@ TypeId Fixture::requireType(const ModuleName& moduleName, const std::string& nam
{
ModulePtr module = frontend.moduleResolver.getModule(moduleName);
REQUIRE(module);
return requireType(module->getModuleScope(), name);
return requireType(module, name);
}
TypeId Fixture::requireType(const ModulePtr& module, const std::string& name)
{
if (!module->hasModuleScope())
FAIL("requireType: module scope data is not available");
return requireType(module->getModuleScope(), name);
}
@ -367,7 +373,12 @@ TypeId Fixture::requireTypeAtPosition(Position position)
std::optional<TypeId> Fixture::lookupType(const std::string& name)
{
if (auto typeFun = getMainModule()->getModuleScope()->lookupType(name))
ModulePtr module = getMainModule();
if (!module->hasModuleScope())
return std::nullopt;
if (auto typeFun = module->getModuleScope()->lookupType(name))
return typeFun->type;
return std::nullopt;
@ -375,12 +386,24 @@ std::optional<TypeId> Fixture::lookupType(const std::string& name)
std::optional<TypeId> Fixture::lookupImportedType(const std::string& moduleAlias, const std::string& name)
{
if (auto typeFun = getMainModule()->getModuleScope()->lookupImportedType(moduleAlias, name))
ModulePtr module = getMainModule();
if (!module->hasModuleScope())
FAIL("lookupImportedType: module scope data is not available");
if (auto typeFun = module->getModuleScope()->lookupImportedType(moduleAlias, name))
return typeFun->type;
return std::nullopt;
}
TypeId Fixture::requireTypeAlias(const std::string& name)
{
std::optional<TypeId> ty = lookupType(name);
REQUIRE(ty);
return *ty;
}
std::string Fixture::decorateWithTypes(const std::string& code)
{
fileResolver.source[mainModuleName] = code;
@ -552,15 +575,52 @@ std::optional<TypeId> linearSearchForBinding(Scope* scope, const char* name)
return std::nullopt;
}
void registerHiddenTypes(Fixture& fixture, TypeArena& arena)
void registerHiddenTypes(Frontend* frontend)
{
TypeId t = arena.addType(GenericType{"T"});
TypeId t = frontend->globalTypes.addType(GenericType{"T"});
GenericTypeDefinition genericT{t};
ScopePtr moduleScope = fixture.frontend.getGlobalScope();
moduleScope->exportedTypeBindings["Not"] = TypeFun{{genericT}, arena.addType(NegationType{t})};
moduleScope->exportedTypeBindings["fun"] = TypeFun{{}, fixture.builtinTypes->functionType};
moduleScope->exportedTypeBindings["cls"] = TypeFun{{}, fixture.builtinTypes->classType};
ScopePtr globalScope = frontend->getGlobalScope();
globalScope->exportedTypeBindings["Not"] = TypeFun{{genericT}, frontend->globalTypes.addType(NegationType{t})};
globalScope->exportedTypeBindings["fun"] = TypeFun{{}, frontend->builtinTypes->functionType};
globalScope->exportedTypeBindings["cls"] = TypeFun{{}, frontend->builtinTypes->classType};
globalScope->exportedTypeBindings["err"] = TypeFun{{}, frontend->builtinTypes->errorType};
}
void createSomeClasses(Frontend* frontend)
{
TypeArena& arena = frontend->globalTypes;
unfreeze(arena);
ScopePtr moduleScope = frontend->getGlobalScope();
TypeId parentType = arena.addType(ClassType{"Parent", {}, frontend->builtinTypes->classType, std::nullopt, {}, nullptr, "Test"});
ClassType* parentClass = getMutable<ClassType>(parentType);
parentClass->props["method"] = {makeFunction(arena, parentType, {}, {})};
parentClass->props["virtual_method"] = {makeFunction(arena, parentType, {}, {})};
addGlobalBinding(*frontend, "Parent", {parentType});
moduleScope->exportedTypeBindings["Parent"] = TypeFun{{}, parentType};
TypeId childType = arena.addType(ClassType{"Child", {}, parentType, std::nullopt, {}, nullptr, "Test"});
ClassType* childClass = getMutable<ClassType>(childType);
childClass->props["virtual_method"] = {makeFunction(arena, childType, {}, {})};
addGlobalBinding(*frontend, "Child", {childType});
moduleScope->exportedTypeBindings["Child"] = TypeFun{{}, childType};
TypeId unrelatedType = arena.addType(ClassType{"Unrelated", {}, frontend->builtinTypes->classType, std::nullopt, {}, nullptr, "Test"});
addGlobalBinding(*frontend, "Unrelated", {unrelatedType});
moduleScope->exportedTypeBindings["Unrelated"] = TypeFun{{}, unrelatedType};
for (const auto& [name, ty] : moduleScope->exportedTypeBindings)
persist(ty.type);
freeze(arena);
}
void dump(const std::vector<Constraint>& constraints)

View file

@ -91,6 +91,7 @@ struct Fixture
std::optional<TypeId> lookupType(const std::string& name);
std::optional<TypeId> lookupImportedType(const std::string& moduleAlias, const std::string& name);
TypeId requireTypeAlias(const std::string& name);
ScopedFastFlag sff_DebugLuauFreezeArena;
ScopedFastFlag sff_UnknownNever{"LuauUnknownAndNeverType", true};
@ -151,7 +152,8 @@ std::optional<TypeId> lookupName(ScopePtr scope, const std::string& name); // Wa
std::optional<TypeId> linearSearchForBinding(Scope* scope, const char* name);
void registerHiddenTypes(Fixture& fixture, TypeArena& arena);
void registerHiddenTypes(Frontend* frontend);
void createSomeClasses(Frontend* frontend);
} // namespace Luau

View file

@ -10,6 +10,8 @@
#include <algorithm>
LUAU_FASTFLAG(LuauScopelessModule)
using namespace Luau;
namespace
@ -143,6 +145,8 @@ TEST_CASE_FIXTURE(FrontendFixture, "real_source")
TEST_CASE_FIXTURE(FrontendFixture, "automatically_check_dependent_scripts")
{
ScopedFastFlag luauScopelessModule{"LuauScopelessModule", true};
fileResolver.source["game/Gui/Modules/A"] = "return {hello=5, world=true}";
fileResolver.source["game/Gui/Modules/B"] = R"(
local Modules = game:GetService('Gui').Modules
@ -157,7 +161,7 @@ TEST_CASE_FIXTURE(FrontendFixture, "automatically_check_dependent_scripts")
CHECK(bModule->errors.empty());
Luau::dumpErrors(bModule);
auto bExports = first(bModule->getModuleScope()->returnType);
auto bExports = first(bModule->returnType);
REQUIRE(!!bExports);
CHECK_EQ("{| b_value: number |}", toString(*bExports));
@ -220,6 +224,8 @@ TEST_CASE_FIXTURE(FrontendFixture, "any_annotation_breaks_cycle")
TEST_CASE_FIXTURE(FrontendFixture, "nocheck_modules_are_typed")
{
ScopedFastFlag luauScopelessModule{"LuauScopelessModule", true};
fileResolver.source["game/Gui/Modules/A"] = R"(
--!nocheck
export type Foo = number
@ -243,13 +249,13 @@ TEST_CASE_FIXTURE(FrontendFixture, "nocheck_modules_are_typed")
ModulePtr aModule = frontend.moduleResolver.modules["game/Gui/Modules/A"];
REQUIRE(bool(aModule));
std::optional<TypeId> aExports = first(aModule->getModuleScope()->returnType);
std::optional<TypeId> aExports = first(aModule->returnType);
REQUIRE(bool(aExports));
ModulePtr bModule = frontend.moduleResolver.modules["game/Gui/Modules/B"];
REQUIRE(bool(bModule));
std::optional<TypeId> bExports = first(bModule->getModuleScope()->returnType);
std::optional<TypeId> bExports = first(bModule->returnType);
REQUIRE(bool(bExports));
CHECK_EQ(toString(*aExports), toString(*bExports));
@ -275,6 +281,8 @@ TEST_CASE_FIXTURE(FrontendFixture, "cycle_detection_between_check_and_nocheck")
TEST_CASE_FIXTURE(FrontendFixture, "nocheck_cycle_used_by_checked")
{
ScopedFastFlag luauScopelessModule{"LuauScopelessModule", true};
fileResolver.source["game/Gui/Modules/A"] = R"(
--!nocheck
local Modules = game:GetService('Gui').Modules
@ -300,7 +308,7 @@ TEST_CASE_FIXTURE(FrontendFixture, "nocheck_cycle_used_by_checked")
ModulePtr cModule = frontend.moduleResolver.modules["game/Gui/Modules/C"];
REQUIRE(bool(cModule));
std::optional<TypeId> cExports = first(cModule->getModuleScope()->returnType);
std::optional<TypeId> cExports = first(cModule->returnType);
REQUIRE(bool(cExports));
CHECK_EQ("{| a: any, b: any |}", toString(*cExports));
}
@ -493,6 +501,8 @@ TEST_CASE_FIXTURE(FrontendFixture, "dont_recheck_script_that_hasnt_been_marked_d
TEST_CASE_FIXTURE(FrontendFixture, "recheck_if_dependent_script_is_dirty")
{
ScopedFastFlag luauScopelessModule{"LuauScopelessModule", true};
fileResolver.source["game/Gui/Modules/A"] = "return {hello=5, world=true}";
fileResolver.source["game/Gui/Modules/B"] = R"(
local Modules = game:GetService('Gui').Modules
@ -511,7 +521,7 @@ TEST_CASE_FIXTURE(FrontendFixture, "recheck_if_dependent_script_is_dirty")
CHECK(bModule->errors.empty());
Luau::dumpErrors(bModule);
auto bExports = first(bModule->getModuleScope()->returnType);
auto bExports = first(bModule->returnType);
REQUIRE(!!bExports);
CHECK_EQ("{| b_value: string |}", toString(*bExports));

View file

@ -112,6 +112,8 @@ TEST_CASE_FIXTURE(Fixture, "deepClone_cyclic_table")
TEST_CASE_FIXTURE(BuiltinsFixture, "builtin_types_point_into_globalTypes_arena")
{
ScopedFastFlag luauScopelessModule{"LuauScopelessModule", true};
CheckResult result = check(R"(
return {sign=math.sign}
)");
@ -119,7 +121,7 @@ TEST_CASE_FIXTURE(BuiltinsFixture, "builtin_types_point_into_globalTypes_arena")
LUAU_REQUIRE_NO_ERRORS(result);
ModulePtr module = frontend.moduleResolver.getModule("MainModule");
std::optional<TypeId> exports = first(module->getModuleScope()->returnType);
std::optional<TypeId> exports = first(module->returnType);
REQUIRE(bool(exports));
REQUIRE(isInArena(*exports, module->interfaceTypes));
@ -283,6 +285,8 @@ TEST_CASE_FIXTURE(Fixture, "clone_recursion_limit")
TEST_CASE_FIXTURE(Fixture, "any_persistance_does_not_leak")
{
ScopedFastFlag luauScopelessModule{"LuauScopelessModule", true};
fileResolver.source["Module/A"] = R"(
export type A = B
type B = A
@ -294,8 +298,8 @@ type B = A
LUAU_REQUIRE_ERRORS(result);
auto mod = frontend.moduleResolver.getModule("Module/A");
auto it = mod->getModuleScope()->exportedTypeBindings.find("A");
REQUIRE(it != mod->getModuleScope()->exportedTypeBindings.end());
auto it = mod->exportedTypeBindings.find("A");
REQUIRE(it != mod->exportedTypeBindings.end());
CHECK(toString(it->second.type) == "any");
}
@ -306,6 +310,7 @@ TEST_CASE_FIXTURE(BuiltinsFixture, "do_not_clone_reexports")
{"LuauSubstitutionReentrant", true},
{"LuauClassTypeVarsInSubstitution", true},
{"LuauSubstitutionFixMissingFields", true},
{"LuauScopelessModule", true},
};
fileResolver.source["Module/A"] = R"(
@ -326,10 +331,10 @@ return {}
ModulePtr modB = frontend.moduleResolver.getModule("Module/B");
REQUIRE(modA);
REQUIRE(modB);
auto modAiter = modA->getModuleScope()->exportedTypeBindings.find("A");
auto modBiter = modB->getModuleScope()->exportedTypeBindings.find("B");
REQUIRE(modAiter != modA->getModuleScope()->exportedTypeBindings.end());
REQUIRE(modBiter != modB->getModuleScope()->exportedTypeBindings.end());
auto modAiter = modA->exportedTypeBindings.find("A");
auto modBiter = modB->exportedTypeBindings.find("B");
REQUIRE(modAiter != modA->exportedTypeBindings.end());
REQUIRE(modBiter != modB->exportedTypeBindings.end());
TypeId typeA = modAiter->second.type;
TypeId typeB = modBiter->second.type;
TableType* tableB = getMutable<TableType>(typeB);
@ -344,6 +349,7 @@ TEST_CASE_FIXTURE(BuiltinsFixture, "do_not_clone_types_of_reexported_values")
{"LuauSubstitutionReentrant", true},
{"LuauClassTypeVarsInSubstitution", true},
{"LuauSubstitutionFixMissingFields", true},
{"LuauScopelessModule", true},
};
fileResolver.source["Module/A"] = R"(
@ -364,8 +370,8 @@ return exports
ModulePtr modB = frontend.moduleResolver.getModule("Module/B");
REQUIRE(modA);
REQUIRE(modB);
std::optional<TypeId> typeA = first(modA->getModuleScope()->returnType);
std::optional<TypeId> typeB = first(modB->getModuleScope()->returnType);
std::optional<TypeId> typeA = first(modA->returnType);
std::optional<TypeId> typeB = first(modB->returnType);
REQUIRE(typeA);
REQUIRE(typeB);
TableType* tableA = getMutable<TableType>(*typeA);

View file

@ -253,6 +253,8 @@ TEST_CASE_FIXTURE(Fixture, "delay_function_does_not_require_its_argument_to_retu
TEST_CASE_FIXTURE(Fixture, "inconsistent_module_return_types_are_ok")
{
ScopedFastFlag luauScopelessModule{"LuauScopelessModule", true};
CheckResult result = check(R"(
--!nonstrict
@ -269,7 +271,7 @@ TEST_CASE_FIXTURE(Fixture, "inconsistent_module_return_types_are_ok")
LUAU_REQUIRE_NO_ERRORS(result);
REQUIRE_EQ("any", toString(getMainModule()->getModuleScope()->returnType));
REQUIRE_EQ("any", toString(getMainModule()->returnType));
}
TEST_CASE_FIXTURE(Fixture, "returning_insufficient_return_values")

View file

@ -17,46 +17,17 @@ struct IsSubtypeFixture : Fixture
{
bool isSubtype(TypeId a, TypeId b)
{
return ::Luau::isSubtype(a, b, NotNull{getMainModule()->getModuleScope().get()}, builtinTypes, ice);
ModulePtr module = getMainModule();
REQUIRE(module);
if (!module->hasModuleScope())
FAIL("isSubtype: module scope data is not available");
return ::Luau::isSubtype(a, b, NotNull{module->getModuleScope().get()}, builtinTypes, ice);
}
};
} // namespace
void createSomeClasses(Frontend& frontend)
{
auto& arena = frontend.globalTypes;
unfreeze(arena);
TypeId parentType = arena.addType(ClassType{"Parent", {}, frontend.builtinTypes->classType, std::nullopt, {}, nullptr, "Test"});
ClassType* parentClass = getMutable<ClassType>(parentType);
parentClass->props["method"] = {makeFunction(arena, parentType, {}, {})};
parentClass->props["virtual_method"] = {makeFunction(arena, parentType, {}, {})};
addGlobalBinding(frontend, "Parent", {parentType});
frontend.getGlobalScope()->exportedTypeBindings["Parent"] = TypeFun{{}, parentType};
TypeId childType = arena.addType(ClassType{"Child", {}, parentType, std::nullopt, {}, nullptr, "Test"});
ClassType* childClass = getMutable<ClassType>(childType);
childClass->props["virtual_method"] = {makeFunction(arena, childType, {}, {})};
addGlobalBinding(frontend, "Child", {childType});
frontend.getGlobalScope()->exportedTypeBindings["Child"] = TypeFun{{}, childType};
TypeId unrelatedType = arena.addType(ClassType{"Unrelated", {}, frontend.builtinTypes->classType, std::nullopt, {}, nullptr, "Test"});
addGlobalBinding(frontend, "Unrelated", {unrelatedType});
frontend.getGlobalScope()->exportedTypeBindings["Unrelated"] = TypeFun{{}, unrelatedType};
for (const auto& [name, ty] : frontend.getGlobalScope()->exportedTypeBindings)
persist(ty.type);
freeze(arena);
}
TEST_SUITE_BEGIN("isSubtype");
TEST_CASE_FIXTURE(IsSubtypeFixture, "primitives")
@ -352,7 +323,7 @@ TEST_CASE_FIXTURE(IsSubtypeFixture, "cyclic_table")
TEST_CASE_FIXTURE(IsSubtypeFixture, "classes")
{
createSomeClasses(frontend);
createSomeClasses(&frontend);
check(""); // Ensure that we have a main Module.
@ -403,7 +374,7 @@ struct NormalizeFixture : Fixture
NormalizeFixture()
{
registerHiddenTypes(*this, arena);
registerHiddenTypes(&frontend);
}
const NormalizedType* toNormalizedType(const std::string& annotation)
@ -589,7 +560,7 @@ TEST_CASE_FIXTURE(Fixture, "cyclic_table_normalizes_sensibly")
TEST_CASE_FIXTURE(BuiltinsFixture, "skip_force_normal_on_external_types")
{
createSomeClasses(frontend);
createSomeClasses(&frontend);
CheckResult result = check(R"(
export type t0 = { a: Child }
@ -612,7 +583,7 @@ TEST_CASE_FIXTURE(NormalizeFixture, "unions_of_classes")
{
ScopedFastFlag sff{"LuauNegatedClassTypes", true};
createSomeClasses(frontend);
createSomeClasses(&frontend);
CHECK("Parent | Unrelated" == toString(normal("Parent | Unrelated")));
CHECK("Parent" == toString(normal("Parent | Child")));
CHECK("Parent | Unrelated" == toString(normal("Parent | Child | Unrelated")));
@ -622,7 +593,7 @@ TEST_CASE_FIXTURE(NormalizeFixture, "intersections_of_classes")
{
ScopedFastFlag sff{"LuauNegatedClassTypes", true};
createSomeClasses(frontend);
createSomeClasses(&frontend);
CHECK("Child" == toString(normal("Parent & Child")));
CHECK("never" == toString(normal("Child & Unrelated")));
}
@ -631,7 +602,7 @@ TEST_CASE_FIXTURE(NormalizeFixture, "narrow_union_of_classes_with_intersection")
{
ScopedFastFlag sff{"LuauNegatedClassTypes", true};
createSomeClasses(frontend);
createSomeClasses(&frontend);
CHECK("Child" == toString(normal("(Child | Unrelated) & Child")));
}
@ -639,7 +610,7 @@ TEST_CASE_FIXTURE(NormalizeFixture, "negations_of_classes")
{
ScopedFastFlag sff{"LuauNegatedClassTypes", true};
createSomeClasses(frontend);
createSomeClasses(&frontend);
CHECK("(Parent & ~Child) | Unrelated" == toString(normal("(Parent & Not<Child>) | Unrelated")));
CHECK("((class & ~Child) | boolean | function | number | string | thread)?" == toString(normal("Not<Child>")));
CHECK("Child" == toString(normal("Not<Parent> & Child")));
@ -653,7 +624,7 @@ TEST_CASE_FIXTURE(NormalizeFixture, "classes_and_unknown")
{
ScopedFastFlag sff{"LuauNegatedClassTypes", true};
createSomeClasses(frontend);
createSomeClasses(&frontend);
CHECK("Parent" == toString(normal("Parent & unknown")));
}
@ -661,7 +632,7 @@ TEST_CASE_FIXTURE(NormalizeFixture, "classes_and_never")
{
ScopedFastFlag sff{"LuauNegatedClassTypes", true};
createSomeClasses(frontend);
createSomeClasses(&frontend);
CHECK("never" == toString(normal("Parent & never")));
}

View file

@ -9,7 +9,6 @@ using namespace Luau;
LUAU_FASTFLAG(DebugLuauDeferredConstraintResolution)
LUAU_FASTFLAG(LuauTypeMismatchInvarianceInError)
LUAU_FASTFLAG(LuauNewLibraryTypeNames)
TEST_SUITE_BEGIN("TypeAliases");
@ -506,19 +505,14 @@ TEST_CASE_FIXTURE(BuiltinsFixture, "general_require_multi_assign")
CheckResult result = frontend.check("workspace/C");
LUAU_REQUIRE_NO_ERRORS(result);
ModulePtr m = frontend.moduleResolver.modules["workspace/C"];
REQUIRE(m != nullptr);
std::optional<TypeId> aTypeId = lookupName(m->getModuleScope(), "a");
REQUIRE(aTypeId);
const Luau::TableType* aType = get<TableType>(follow(*aTypeId));
TypeId aTypeId = requireType("workspace/C", "a");
const Luau::TableType* aType = get<TableType>(follow(aTypeId));
REQUIRE(aType);
REQUIRE(aType->props.size() == 2);
std::optional<TypeId> bTypeId = lookupName(m->getModuleScope(), "b");
REQUIRE(bTypeId);
const Luau::TableType* bType = get<TableType>(follow(*bTypeId));
TypeId bTypeId = requireType("workspace/C", "b");
const Luau::TableType* bType = get<TableType>(follow(bTypeId));
REQUIRE(bType);
REQUIRE(bType->props.size() == 3);
}
@ -530,10 +524,7 @@ TEST_CASE_FIXTURE(BuiltinsFixture, "type_alias_import_mutation")
TypeId ty = getGlobalBinding(frontend, "table");
if (FFlag::LuauNewLibraryTypeNames)
CHECK(toString(ty) == "typeof(table)");
else
CHECK(toString(ty) == "table");
CHECK(toString(ty) == "typeof(table)");
const TableType* ttv = get<TableType>(ty);
REQUIRE(ttv);

View file

@ -319,10 +319,10 @@ TEST_CASE_FIXTURE(Fixture, "self_referential_type_alias")
LUAU_REQUIRE_NO_ERRORS(result);
std::optional<TypeFun> res = getMainModule()->getModuleScope()->lookupType("O");
std::optional<TypeId> res = lookupType("O");
REQUIRE(res);
TypeId oType = follow(res->type);
TypeId oType = follow(*res);
const TableType* oTable = get<TableType>(oType);
REQUIRE(oTable);
@ -347,6 +347,8 @@ TEST_CASE_FIXTURE(Fixture, "define_generic_type_alias")
LUAU_REQUIRE_NO_ERRORS(result);
ModulePtr mainModule = getMainModule();
REQUIRE(mainModule);
REQUIRE(mainModule->hasModuleScope());
auto it = mainModule->getModuleScope()->privateTypeBindings.find("Array");
REQUIRE(it != mainModule->getModuleScope()->privateTypeBindings.end());
@ -463,6 +465,8 @@ TEST_CASE_FIXTURE(Fixture, "type_alias_always_resolve_to_a_real_type")
TEST_CASE_FIXTURE(Fixture, "interface_types_belong_to_interface_arena")
{
ScopedFastFlag luauScopelessModule{"LuauScopelessModule", true};
CheckResult result = check(R"(
export type A = {field: number}
@ -475,12 +479,12 @@ TEST_CASE_FIXTURE(Fixture, "interface_types_belong_to_interface_arena")
Module& mod = *getMainModule();
const TypeFun& a = mod.getModuleScope()->exportedTypeBindings["A"];
const TypeFun& a = mod.exportedTypeBindings["A"];
CHECK(isInArena(a.type, mod.interfaceTypes));
CHECK(!isInArena(a.type, typeChecker.globalTypes));
std::optional<TypeId> exportsType = first(mod.getModuleScope()->returnType);
std::optional<TypeId> exportsType = first(mod.returnType);
REQUIRE(exportsType);
TableType* exportsTable = getMutable<TableType>(*exportsType);
@ -494,6 +498,8 @@ TEST_CASE_FIXTURE(Fixture, "interface_types_belong_to_interface_arena")
TEST_CASE_FIXTURE(Fixture, "generic_aliases_are_cloned_properly")
{
ScopedFastFlag luauScopelessModule{"LuauScopelessModule", true};
CheckResult result = check(R"(
export type Array<T> = { [number]: T }
)");
@ -501,7 +507,7 @@ TEST_CASE_FIXTURE(Fixture, "generic_aliases_are_cloned_properly")
dumpErrors(result);
Module& mod = *getMainModule();
const auto& typeBindings = mod.getModuleScope()->exportedTypeBindings;
const auto& typeBindings = mod.exportedTypeBindings;
auto it = typeBindings.find("Array");
REQUIRE(typeBindings.end() != it);
@ -521,6 +527,8 @@ TEST_CASE_FIXTURE(Fixture, "generic_aliases_are_cloned_properly")
TEST_CASE_FIXTURE(Fixture, "cloned_interface_maintains_pointers_between_definitions")
{
ScopedFastFlag luauScopelessModule{"LuauScopelessModule", true};
CheckResult result = check(R"(
export type Record = { name: string, location: string }
local a: Record = { name="Waldo", location="?????" }
@ -533,9 +541,9 @@ TEST_CASE_FIXTURE(Fixture, "cloned_interface_maintains_pointers_between_definiti
Module& mod = *getMainModule();
TypeId recordType = mod.getModuleScope()->exportedTypeBindings["Record"].type;
TypeId recordType = mod.exportedTypeBindings["Record"].type;
std::optional<TypeId> exportsType = first(mod.getModuleScope()->returnType);
std::optional<TypeId> exportsType = first(mod.returnType);
REQUIRE(exportsType);
TableType* exportsTable = getMutable<TableType>(*exportsType);

View file

@ -109,6 +109,8 @@ TEST_CASE_FIXTURE(Fixture, "vararg_functions_should_allow_calls_of_any_types_and
TEST_CASE_FIXTURE(BuiltinsFixture, "vararg_function_is_quantified")
{
ScopedFastFlag luauScopelessModule{"LuauScopelessModule", true};
CheckResult result = check(R"(
local T = {}
function T.f(...)
@ -129,7 +131,7 @@ TEST_CASE_FIXTURE(BuiltinsFixture, "vararg_function_is_quantified")
LUAU_REQUIRE_NO_ERRORS(result);
auto r = first(getMainModule()->getModuleScope()->returnType);
auto r = first(getMainModule()->returnType);
REQUIRE(r);
TableType* ttv = getMutable<TableType>(*r);
@ -1772,7 +1774,7 @@ z = y -- Not OK, so the line is colorable
TEST_CASE_FIXTURE(Fixture, "function_is_supertype_of_concrete_functions")
{
ScopedFastFlag sff{"LuauNegatedFunctionTypes", true};
registerHiddenTypes(*this, frontend.globalTypes);
registerHiddenTypes(&frontend);
CheckResult result = check(R"(
function foo(f: fun) end
@ -1791,7 +1793,7 @@ TEST_CASE_FIXTURE(Fixture, "function_is_supertype_of_concrete_functions")
TEST_CASE_FIXTURE(Fixture, "concrete_functions_are_not_supertypes_of_function")
{
ScopedFastFlag sff{"LuauNegatedFunctionTypes", true};
registerHiddenTypes(*this, frontend.globalTypes);
registerHiddenTypes(&frontend);
CheckResult result = check(R"(
local a: fun = function() end
@ -1812,7 +1814,7 @@ TEST_CASE_FIXTURE(Fixture, "concrete_functions_are_not_supertypes_of_function")
TEST_CASE_FIXTURE(Fixture, "other_things_are_not_related_to_function")
{
ScopedFastFlag sff{"LuauNegatedFunctionTypes", true};
registerHiddenTypes(*this, frontend.globalTypes);
registerHiddenTypes(&frontend);
CheckResult result = check(R"(
local a: fun = function() end

View file

@ -1021,9 +1021,9 @@ TEST_CASE_FIXTURE(Fixture, "no_stack_overflow_from_quantifying")
LUAU_REQUIRE_ERRORS(result);
std::optional<TypeFun> t0 = getMainModule()->getModuleScope()->lookupType("t0");
std::optional<TypeId> t0 = lookupType("t0");
REQUIRE(t0);
CHECK_EQ("*error-type*", toString(t0->type));
CHECK_EQ("*error-type*", toString(*t0));
auto it = std::find_if(result.errors.begin(), result.errors.end(), [](TypeError& err) {
return get<OccursCheckFailed>(err);

View file

@ -20,7 +20,7 @@ struct NegationFixture : Fixture
NegationFixture()
{
registerHiddenTypes(*this, arena);
registerHiddenTypes(&frontend);
}
};

View file

@ -405,17 +405,41 @@ TEST_CASE_FIXTURE(BuiltinsFixture, "compound_assign_metatable")
type V2B = { x: number, y: number }
local v2b: V2B = { x = 0, y = 0 }
local VMT = {}
type V2 = typeof(setmetatable(v2b, VMT))
function VMT.__add(a: V2, b: V2): V2
VMT.__add = function(a: V2, b: V2): V2
return setmetatable({ x = a.x + b.x, y = a.y + b.y }, VMT)
end
type V2 = typeof(setmetatable(v2b, VMT))
local v1: V2 = setmetatable({ x = 1, y = 2 }, VMT)
local v2: V2 = setmetatable({ x = 3, y = 4 }, VMT)
v1 += v2
)");
CHECK_EQ(0, result.errors.size());
LUAU_REQUIRE_NO_ERRORS(result);
}
TEST_CASE_FIXTURE(BuiltinsFixture, "compound_assign_result_must_be_compatible_with_var")
{
CheckResult result = check(R"(
function __add(left, right)
return 123
end
local mt = {
__add = __add,
}
local x = setmetatable({}, mt)
local v: number
v += x -- okay: number + x -> number
x += v -- not okay: x </: number
)");
LUAU_REQUIRE_ERROR_COUNT(1, result);
CHECK(result.errors[0] == TypeError{Location{{13, 8}, {13, 14}}, TypeMismatch{requireType("x"), builtinTypes->numberType}});
}
TEST_CASE_FIXTURE(BuiltinsFixture, "compound_assign_mismatch_metatable")
@ -1015,11 +1039,11 @@ TEST_CASE_FIXTURE(BuiltinsFixture, "mm_ops_must_return_a_value")
local y = x + 123
)");
LUAU_REQUIRE_ERROR_COUNT(1, result);
LUAU_REQUIRE_ERROR_COUNT(2, result);
CHECK(requireType("y") == builtinTypes->errorRecoveryType());
const GenericError* ge = get<GenericError>(result.errors[0]);
const GenericError* ge = get<GenericError>(result.errors[1]);
REQUIRE(ge);
CHECK(ge->message == "Metamethod '__add' must return a value");
}
@ -1049,13 +1073,13 @@ TEST_CASE_FIXTURE(BuiltinsFixture, "mm_comparisons_must_return_a_boolean")
local v2 = o2 < o2
)");
LUAU_REQUIRE_ERROR_COUNT(2, result);
LUAU_REQUIRE_ERROR_COUNT(4, result);
CHECK(requireType("v1") == builtinTypes->booleanType);
CHECK(requireType("v2") == builtinTypes->booleanType);
CHECK(toString(result.errors[0]) == "Metamethod '__lt' must return type 'boolean'");
CHECK(toString(result.errors[1]) == "Metamethod '__lt' must return type 'boolean'");
CHECK(toString(result.errors[1]) == "Metamethod '__lt' must return a boolean");
CHECK(toString(result.errors[3]) == "Metamethod '__lt' must return a boolean");
}
TEST_CASE_FIXTURE(BuiltinsFixture, "reworked_and")

View file

@ -516,6 +516,8 @@ TEST_CASE_FIXTURE(BuiltinsFixture, "for_in_loop_with_zero_iterators")
// Ideally, we would not try to export a function type with generic types from incorrect scope
TEST_CASE_FIXTURE(BuiltinsFixture, "generic_type_leak_to_module_interface")
{
ScopedFastFlag luauScopelessModule{"LuauScopelessModule", true};
fileResolver.source["game/A"] = R"(
local wrapStrictTable
@ -548,13 +550,15 @@ return wrapStrictTable(Constants, "Constants")
ModulePtr m = frontend.moduleResolver.modules["game/B"];
REQUIRE(m);
std::optional<TypeId> result = first(m->getModuleScope()->returnType);
std::optional<TypeId> result = first(m->returnType);
REQUIRE(result);
CHECK(get<AnyType>(*result));
}
TEST_CASE_FIXTURE(BuiltinsFixture, "generic_type_leak_to_module_interface_variadic")
{
ScopedFastFlag luauScopelessModule{"LuauScopelessModule", true};
fileResolver.source["game/A"] = R"(
local wrapStrictTable
@ -587,7 +591,7 @@ return wrapStrictTable(Constants, "Constants")
ModulePtr m = frontend.moduleResolver.modules["game/B"];
REQUIRE(m);
std::optional<TypeId> result = first(m->getModuleScope()->returnType);
std::optional<TypeId> result = first(m->returnType);
REQUIRE(result);
CHECK(get<AnyType>(*result));
}
@ -620,7 +624,13 @@ struct IsSubtypeFixture : Fixture
{
bool isSubtype(TypeId a, TypeId b)
{
return ::Luau::isSubtype(a, b, NotNull{getMainModule()->getModuleScope().get()}, builtinTypes, ice);
ModulePtr module = getMainModule();
REQUIRE(module);
if (!module->hasModuleScope())
FAIL("isSubtype: module scope data is not available");
return ::Luau::isSubtype(a, b, NotNull{module->getModuleScope().get()}, builtinTypes, ice);
}
};
} // namespace

View file

@ -18,7 +18,6 @@ LUAU_FASTFLAG(LuauLowerBoundsCalculation);
LUAU_FASTFLAG(DebugLuauDeferredConstraintResolution);
LUAU_FASTFLAG(LuauInstantiateInSubtyping)
LUAU_FASTFLAG(LuauTypeMismatchInvarianceInError)
LUAU_FASTFLAG(LuauNewLibraryTypeNames)
TEST_SUITE_BEGIN("TableTests");
@ -1730,16 +1729,8 @@ TEST_CASE_FIXTURE(BuiltinsFixture, "builtin_table_names")
LUAU_REQUIRE_ERROR_COUNT(2, result);
if (FFlag::LuauNewLibraryTypeNames)
{
CHECK_EQ("Cannot add property 'h' to table 'typeof(os)'", toString(result.errors[0]));
CHECK_EQ("Cannot add property 'k' to table 'typeof(string)'", toString(result.errors[1]));
}
else
{
CHECK_EQ("Cannot add property 'h' to table 'os'", toString(result.errors[0]));
CHECK_EQ("Cannot add property 'k' to table 'string'", toString(result.errors[1]));
}
CHECK_EQ("Cannot add property 'h' to table 'typeof(os)'", toString(result.errors[0]));
CHECK_EQ("Cannot add property 'k' to table 'typeof(string)'", toString(result.errors[1]));
}
TEST_CASE_FIXTURE(BuiltinsFixture, "persistent_sealed_table_is_immutable")
@ -1750,10 +1741,7 @@ TEST_CASE_FIXTURE(BuiltinsFixture, "persistent_sealed_table_is_immutable")
)");
LUAU_REQUIRE_ERROR_COUNT(1, result);
if (FFlag::LuauNewLibraryTypeNames)
CHECK_EQ("Cannot add property 'bad' to table 'typeof(os)'", toString(result.errors[0]));
else
CHECK_EQ("Cannot add property 'bad' to table 'os'", toString(result.errors[0]));
CHECK_EQ("Cannot add property 'bad' to table 'typeof(os)'", toString(result.errors[0]));
const TableType* osType = get<TableType>(requireType("os"));
REQUIRE(osType != nullptr);
@ -2967,6 +2955,8 @@ TEST_CASE_FIXTURE(Fixture, "inferred_properties_of_a_table_should_start_with_the
// The real bug here was that we weren't always uncondionally typechecking a trailing return statement last.
TEST_CASE_FIXTURE(BuiltinsFixture, "dont_leak_free_table_props")
{
ScopedFastFlag luauScopelessModule{"LuauScopelessModule", true};
CheckResult result = check(R"(
local function a(state)
print(state.blah)
@ -2988,7 +2978,7 @@ TEST_CASE_FIXTURE(BuiltinsFixture, "dont_leak_free_table_props")
CHECK_EQ("<a>({+ blah: a +}) -> ()", toString(requireType("a")));
CHECK_EQ("<a>({+ gwar: a +}) -> ()", toString(requireType("b")));
CHECK_EQ("() -> <a, b>({+ blah: a, gwar: b +}) -> ()", toString(getMainModule()->getModuleScope()->returnType));
CHECK_EQ("() -> <a, b>({+ blah: a, gwar: b +}) -> ()", toString(getMainModule()->returnType));
}
TEST_CASE_FIXTURE(Fixture, "inferred_return_type_of_free_table")
@ -3230,8 +3220,6 @@ TEST_CASE_FIXTURE(Fixture, "scalar_is_a_subtype_of_a_compatible_polymorphic_shap
TEST_CASE_FIXTURE(Fixture, "scalar_is_not_a_subtype_of_a_compatible_polymorphic_shape_type")
{
ScopedFastFlag sff{"LuauScalarShapeSubtyping", true};
if (!FFlag::LuauNewLibraryTypeNames)
return;
CheckResult result = check(R"(
local function f(s)
@ -3280,8 +3268,6 @@ TEST_CASE_FIXTURE(Fixture, "a_free_shape_can_turn_into_a_scalar_if_it_is_compati
TEST_CASE_FIXTURE(Fixture, "a_free_shape_cannot_turn_into_a_scalar_if_it_is_not_compatible")
{
ScopedFastFlag sff{"LuauScalarShapeSubtyping", true};
if (!FFlag::LuauNewLibraryTypeNames)
return;
CheckResult result = check(R"(
local function f(s): string

View file

@ -648,10 +648,10 @@ TEST_CASE_FIXTURE(Fixture, "no_stack_overflow_from_isoptional")
LUAU_REQUIRE_ERRORS(result);
std::optional<TypeFun> t0 = getMainModule()->getModuleScope()->lookupType("t0");
std::optional<TypeId> t0 = lookupType("t0");
REQUIRE(t0);
CHECK_EQ("*error-type*", toString(t0->type));
CHECK_EQ("*error-type*", toString(*t0));
auto it = std::find_if(result.errors.begin(), result.errors.end(), [](TypeError& err) {
return get<OccursCheckFailed>(err);

View file

@ -428,8 +428,12 @@ type E = X<(number, ...string)>
LUAU_REQUIRE_NO_ERRORS(result);
CHECK_EQ(toString(*lookupType("D")), "(...number) -> (string, ...number)");
CHECK_EQ(toString(*lookupType("E")), "(number, ...string) -> (string, number, ...string)");
auto d = lookupType("D");
REQUIRE(d);
auto e = lookupType("E");
REQUIRE(e);
CHECK_EQ(toString(*d), "(...number) -> (string, ...number)");
CHECK_EQ(toString(*e), "(number, ...string) -> (string, number, ...string)");
}
TEST_CASE_FIXTURE(Fixture, "type_alias_type_pack_multi")
@ -887,9 +891,13 @@ TEST_CASE_FIXTURE(Fixture, "unifying_vararg_pack_with_fixed_length_pack_produces
LUAU_REQUIRE_NO_ERRORS(result);
REQUIRE(bool(getMainModule()->getModuleScope()->varargPack));
ModulePtr mainModule = getMainModule();
REQUIRE(mainModule);
REQUIRE(mainModule->hasModuleScope());
TypePackId varargPack = *getMainModule()->getModuleScope()->varargPack;
REQUIRE(bool(mainModule->getModuleScope()->varargPack));
TypePackId varargPack = *mainModule->getModuleScope()->varargPack;
auto iter = begin(varargPack);
auto endIter = end(varargPack);

View file

@ -397,8 +397,6 @@ local e = a.z
TEST_CASE_FIXTURE(Fixture, "optional_iteration")
{
ScopedFastFlag luauNilIterator{"LuauNilIterator", true};
CheckResult result = check(R"(
function foo(values: {number}?)
local s = 0

1249
tests/TypeReduction.test.cpp Normal file

File diff suppressed because it is too large Load diff

View file

@ -1,7 +1,8 @@
// This file is part of the Luau programming language and is licensed under MIT License; see LICENSE.txt for details
#include "Luau/Scope.h"
#include "Luau/TypeInfer.h"
#include "Luau/Type.h"
#include "Luau/TypeInfer.h"
#include "Luau/TypeReduction.h"
#include "Luau/VisitType.h"
#include "Fixture.h"

View file

@ -321,6 +321,7 @@ TypeInfer.globals2
TypeInfer.infer_assignment_value_types_mutable_lval
TypeInfer.it_is_ok_to_have_inconsistent_number_of_return_values_in_nonstrict
TypeInfer.no_stack_overflow_from_isoptional
TypeInfer.no_stack_overflow_from_isoptional2
TypeInfer.tc_after_error_recovery_no_replacement_name_in_error
TypeInfer.tc_if_else_expressions_expected_type_3
TypeInfer.tc_interpolated_string_basic
@ -408,10 +409,7 @@ TypeInferOperators.cannot_compare_tables_that_do_not_have_the_same_metatable
TypeInferOperators.cannot_indirectly_compare_types_that_do_not_have_a_metatable
TypeInferOperators.cannot_indirectly_compare_types_that_do_not_offer_overloaded_ordering_operators
TypeInferOperators.cli_38355_recursive_union
TypeInferOperators.compound_assign_metatable
TypeInferOperators.compound_assign_mismatch_metatable
TypeInferOperators.compound_assign_mismatch_op
TypeInferOperators.compound_assign_mismatch_result
TypeInferOperators.disallow_string_and_types_without_metatables_from_arithmetic_binary_ops
TypeInferOperators.in_nonstrict_mode_strip_nil_from_intersections_when_considering_relational_operators
TypeInferOperators.infer_any_in_all_modes_when_lhs_is_unknown