diff --git a/Analysis/include/Luau/Constraint.h b/Analysis/include/Luau/Constraint.h index b4132954..ec94eee9 100644 --- a/Analysis/include/Luau/Constraint.h +++ b/Analysis/include/Luau/Constraint.h @@ -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* astOriginalCallTypes; - DenseHashMap* astOverloadResolvedTypes; + const void* astFragment; + DenseHashMap* astOriginalCallTypes; + DenseHashMap* astOverloadResolvedTypes; }; // iteratee is iterable diff --git a/Analysis/include/Luau/ConstraintGraphBuilder.h b/Analysis/include/Luau/ConstraintGraphBuilder.h index 65ea5e09..3a67610a 100644 --- a/Analysis/include/Luau/ConstraintGraphBuilder.h +++ b/Analysis/include/Luau/ConstraintGraphBuilder.h @@ -82,11 +82,11 @@ struct ConstraintGraphBuilder // If the node was applied as a function, this is the unspecialized type of // that expression. - DenseHashMap astOriginalCallTypes{nullptr}; + DenseHashMap astOriginalCallTypes{nullptr}; // If overload resolution was performed on this element, this is the // overload that was selected. - DenseHashMap astOverloadResolvedTypes{nullptr}; + DenseHashMap astOverloadResolvedTypes{nullptr}; // Types resolved from type annotations. Analogous to astTypes. DenseHashMap astResolvedTypes{nullptr}; diff --git a/Analysis/include/Luau/Module.h b/Analysis/include/Luau/Module.h index d6d9f841..2cd6802e 100644 --- a/Analysis/include/Luau/Module.h +++ b/Analysis/include/Luau/Module.h @@ -73,19 +73,30 @@ struct Module DenseHashMap astTypes{nullptr}; DenseHashMap astTypePacks{nullptr}; DenseHashMap astExpectedTypes{nullptr}; - DenseHashMap astOriginalCallTypes{nullptr}; - DenseHashMap astOverloadResolvedTypes{nullptr}; + + // Pointers are either AstExpr or AstStat. + DenseHashMap astOriginalCallTypes{nullptr}; + + // Pointers are either AstExpr or AstStat. + DenseHashMap astOverloadResolvedTypes{nullptr}; + DenseHashMap astResolvedTypes{nullptr}; DenseHashMap astResolvedTypePacks{nullptr}; // Map AST nodes to the scope they create. Cannot be NotNull because we need a sentinel value for the map. DenseHashMap astScopes{nullptr}; + std::unique_ptr reduction; + std::unordered_map declaredGlobals; ErrorVec errors; Mode mode; SourceCode::Type type; bool timeout = false; + TypePackId returnType = nullptr; + std::unordered_map exportedTypeBindings; + + bool hasModuleScope() const; ScopePtr getModuleScope() const; // Once a module has been typechecked, we clone its public interface into a separate arena. diff --git a/Analysis/include/Luau/RecursionCounter.h b/Analysis/include/Luau/RecursionCounter.h index 77af10a0..0dc55700 100644 --- a/Analysis/include/Luau/RecursionCounter.h +++ b/Analysis/include/Luau/RecursionCounter.h @@ -32,7 +32,7 @@ struct RecursionCounter --(*count); } -private: +protected: int* count; }; diff --git a/Analysis/include/Luau/Type.h b/Analysis/include/Luau/Type.h index fcc073d8..734d40ea 100644 --- a/Analysis/include/Luau/Type.h +++ b/Analysis/include/Luau/Type.h @@ -494,13 +494,13 @@ struct AnyType { }; -// T | U +// `T | U` struct UnionType { std::vector options; }; -// T & U +// `T & U` struct IntersectionType { std::vector 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 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& types = getTypes(t); LUAU_ASSERT(currentIndex < types.size()); - const TypeId& ty = types[currentIndex]; - LUAU_ASSERT(!get(follow(ty))); + TypeId ty = follow(types[currentIndex]); + LUAU_ASSERT(!get(ty)); return ty; } diff --git a/Analysis/include/Luau/TypeReduction.h b/Analysis/include/Luau/TypeReduction.h new file mode 100644 index 00000000..7df7edfa --- /dev/null +++ b/Analysis/include/Luau/TypeReduction.h @@ -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 arena, NotNull builtinTypes, NotNull handle); + + std::optional reduce(TypeId ty); + std::optional reduce(TypePackId tp); + std::optional reduce(const TypeFun& fun); + +private: + NotNull arena; + NotNull builtinTypes; + NotNull handle; + + DenseHashMap cachedTypes{nullptr}; + DenseHashMap cachedTypePacks{nullptr}; + + std::optional reduceImpl(TypeId ty); + std::optional 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 diff --git a/Analysis/src/AstQuery.cpp b/Analysis/src/AstQuery.cpp index 39f613e5..ffab734a 100644 --- a/Analysis/src/AstQuery.cpp +++ b/Analysis/src/AstQuery.cpp @@ -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 findBindingAtPosition(const Module& module, const SourceM return std::nullopt; ScopePtr currentScope = findScopeAtPosition(module, pos); - LUAU_ASSERT(currentScope); while (currentScope) { diff --git a/Analysis/src/Autocomplete.cpp b/Analysis/src/Autocomplete.cpp index 7a649546..49c430e6 100644 --- a/Analysis/src/Autocomplete.cpp +++ b/Analysis/src/Autocomplete.cpp @@ -150,6 +150,8 @@ static TypeCorrectKind checkTypeCorrectKind( { ty = follow(ty); + LUAU_ASSERT(module.hasModuleScope()); + NotNull 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 autocompleteStringParams(const Source } static AutocompleteResult autocomplete(const SourceModule& sourceModule, const ModulePtr& module, NotNull builtinTypes, - Scope* globalScope, Position position, StringCompletionCallback callback) + TypeArena* typeArena, Scope* globalScope, Position position, StringCompletionCallback callback) { if (isWithinComment(sourceModule, position)) return {}; - TypeArena typeArena; - std::vector 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()) { @@ -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(); statIf && node->is()) { if (statIf->condition->is()) - 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(); statRepeat && statRepeat->condition->is()) - return autocompleteExpression(sourceModule, *module, builtinTypes, &typeArena, ancestry, position); + return autocompleteExpression(sourceModule, *module, builtinTypes, typeArena, ancestry, position); else if (AstStatRepeat* statRepeat = extractStat(ancestry); statRepeat) return {autocompleteStatement(sourceModule, *module, ancestry, position), ancestry, AutocompleteContext::Statement}; else if (AstExprTable* exprTable = parent->as(); @@ -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()) { 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()) { @@ -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 = 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 diff --git a/Analysis/src/BuiltinDefinitions.cpp b/Analysis/src/BuiltinDefinitions.cpp index 81702ff6..26aaf54f 100644 --- a/Analysis/src/BuiltinDefinitions.cpp +++ b/Analysis/src/BuiltinDefinitions.cpp @@ -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(t: Table, 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(t: Table, 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(t: Table) -> ((Table, K?) -> (K, V), Table, nil) - addGlobalBinding( - typeChecker, "pairs", arena.addType(FunctionType{{genericK, genericV}, {}, pairsArgsTypePack, pairsReturnTypePack}), "@luau"); - } - else - { - // next(t: Table, 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(t: Table) -> ((Table, K?) -> (K, V), Table, nil) - addGlobalBinding( - typeChecker, "pairs", arena.addType(FunctionType{{genericK, genericV}, {}, pairsArgsTypePack, pairsReturnTypePack}), "@luau"); - } + // pairs(t: Table) -> ((Table, K?) -> (K, V), Table, 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(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(t: Table, 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(t: Table, 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(t: Table) -> ((Table, K?) -> (K?, V), Table, nil) - addGlobalBinding(frontend, "pairs", arena.addType(FunctionType{{genericK, genericV}, {}, pairsArgsTypePack, pairsReturnTypePack}), "@luau"); - } - else - { - // next(t: Table, 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(t: Table) -> ((Table, K?) -> (K, V), Table, nil) - addGlobalBinding(frontend, "pairs", arena.addType(FunctionType{{genericK, genericV}, {}, pairsArgsTypePack, pairsReturnTypePack}), "@luau"); - } + // pairs(t: Table) -> ((Table, K?) -> (K?, V), Table, 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(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) + ")"; } } diff --git a/Analysis/src/ConstraintGraphBuilder.cpp b/Analysis/src/ConstraintGraphBuilder.cpp index 256eba54..6a80fed2 100644 --- a/Analysis/src/ConstraintGraphBuilder.cpp +++ b/Analysis/src/ConstraintGraphBuilder.cpp @@ -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 vars{&assign->var, 1}; - AstArray 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) diff --git a/Analysis/src/ConstraintSolver.cpp b/Analysis/src/ConstraintSolver.cpp index 67c1732c..8092144c 100644 --- a/Analysis/src/ConstraintSolver.cpp +++ b/Analysis/src/ConstraintSolver.cpp @@ -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, NotNullty.emplace(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(modulePack)) return errorRecoveryType(); diff --git a/Analysis/src/EmbeddedBuiltinDefinitions.cpp b/Analysis/src/EmbeddedBuiltinDefinitions.cpp index b0f21737..1fe09773 100644 --- a/Analysis/src/EmbeddedBuiltinDefinitions.cpp +++ b/Analysis/src/EmbeddedBuiltinDefinitions.cpp @@ -2,7 +2,6 @@ #include "Luau/BuiltinDefinitions.h" LUAU_FASTFLAG(LuauUnknownAndNeverType) -LUAU_FASTFLAG(LuauOptionalNextKey) namespace Luau { @@ -127,7 +126,7 @@ declare function rawlen(obj: {[K]: V} | string): number declare function setfenv(target: number | (T...) -> R..., env: {[string]: any}): ((T...) -> R...)? --- TODO: place ipairs definition here with removal of FFlagLuauOptionalNextKey +declare function ipairs(tab: {V}): (({V}, number) -> (number?, V), {V}, number) declare function pcall(f: (A...) -> R..., ...: A...): (boolean, R...) @@ -208,11 +207,6 @@ std::string getBuiltinDefinitionSource() else result += "declare function error(message: T, level: number?)\n"; - if (FFlag::LuauOptionalNextKey) - result += "declare function ipairs(tab: {V}): (({V}, number) -> (number?, V), {V}, number)\n"; - else - result += "declare function ipairs(tab: {V}): (({V}, number) -> (number, V), {V}, number)\n"; - return result; } diff --git a/Analysis/src/Frontend.cpp b/Analysis/src/Frontend.cpp index 5d2c1587..f4e529db 100644 --- a/Analysis/src/Frontend.cpp +++ b/Analysis/src/Frontend.cpp @@ -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 @@ -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 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 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::optionalinterfaceTypes); 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 requireCycles, bool forAutocomplete) { ModulePtr result = std::make_shared(); + result->reduction = std::make_unique(NotNull{&result->internalTypes}, builtinTypes, NotNull{&iceHandler}); std::unique_ptr logger; if (FFlag::DebugLuauLogSolverToJson) diff --git a/Analysis/src/Module.cpp b/Analysis/src/Module.cpp index a73b928b..e54a4493 100644 --- a/Analysis/src/Module.cpp +++ b/Analysis/src/Module.cpp @@ -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 @@ -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, InternalEr TypePackId returnType = moduleScope->returnType; std::optional varargPack = FFlag::DebugLuauDeferredConstraintResolution ? std::nullopt : moduleScope->varargPack; - std::unordered_map* exportedTypeBindings = &moduleScope->exportedTypeBindings; TxnLog log; ClonePublicInterface clonePublicInterface{&log, builtinTypes, this}; @@ -209,15 +210,12 @@ void Module::clonePublicInterface(NotNull 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, 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; } diff --git a/Analysis/src/ToString.cpp b/Analysis/src/ToString.cpp index 05e51211..af038f18 100644 --- a/Analysis/src/ToString.cpp +++ b/Analysis/src/ToString.cpp @@ -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) diff --git a/Analysis/src/Type.cpp b/Analysis/src/Type.cpp index aba6bddc..f03061a8 100644 --- a/Analysis/src/Type.cpp +++ b/Analysis/src/Type.cpp @@ -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)) - ttv->name = FFlag::LuauNewLibraryTypeNames ? "typeof(string)" : "string"; + ttv->name = "typeof(string)"; return arena->addType(TableType{{{{"__index", {tableType}}}}, std::nullopt, TypeLevel{}, TableState::Sealed}); } diff --git a/Analysis/src/TypeAttach.cpp b/Analysis/src/TypeAttach.cpp index d1d89b25..f9a16205 100644 --- a/Analysis/src/TypeAttach.cpp +++ b/Analysis/src/TypeAttach.cpp @@ -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; } diff --git a/Analysis/src/TypeChecker2.cpp b/Analysis/src/TypeChecker2.cpp index 5451a454..1d212851 100644 --- a/Analysis/src/TypeChecker2.cpp +++ b/Analysis/src/TypeChecker2.cpp @@ -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()) return visit(e); else if (auto e = expr->as()) - return visit(e); + { + visit(e); + return; + } else if (auto e = expr->as()) return visit(e); else if (auto e = expr->as()) @@ -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(leftType) || get(leftType) || get(rightType) || get(rightType)) - return; + if (get(leftType) || get(leftType)) + return leftType; + else if (get(rightType) || get(rightType)) + return rightType; if ((get(leftType) || get(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 leftMt = getMetatable(leftType, builtinTypes); std::optional 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 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 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(leftType) || get(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(); } } diff --git a/Analysis/src/TypeInfer.cpp b/Analysis/src/TypeInfer.cpp index f31ea938..5c1ee388 100644 --- a/Analysis/src/TypeInfer.cpp +++ b/Analysis/src/TypeInfer.cpp @@ -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 @@ -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(NotNull{¤tModule->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 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 arguments{forin.values.data + 1, forin.values.size - 1}; - if (forin.values.size >= 2) - { - AstArray 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 fty = first(retPack); fty && !varTypes.empty()) - { - TypeId keyTy = follow(*fty); - - if (get(keyTy)) - { - if (std::optional 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 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(keyTy)) { - AstArray 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 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(modulePack)) return errorRecoveryType(scope); diff --git a/Analysis/src/TypeReduction.cpp b/Analysis/src/TypeReduction.cpp new file mode 100644 index 00000000..db79debf --- /dev/null +++ b/Analysis/src/TypeReduction.cpp @@ -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 +#include + +LUAU_FASTINTVARIABLE(LuauTypeReductionCartesianProductLimit, 100'000) +LUAU_FASTINTVARIABLE(LuauTypeReductionRecursionLimit, 700) +LUAU_FASTFLAGVARIABLE(DebugLuauDontReduceTypes, false) + +namespace Luau +{ + +namespace +{ + +struct RecursionGuard : RecursionLimiter +{ + std::deque* seen; + + RecursionGuard(int* count, int limit, std::deque* 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 +std::pair get2(const Thing& one, const Thing& two) +{ + const A* a = get(one); + const B* b = get(two); + return a && b ? std::make_pair(a, b) : std::make_pair(nullptr, nullptr); +} + +struct TypeReducer +{ + NotNull arena; + NotNull builtinTypes; + NotNull handle; + + TypeId reduce(TypeId ty); + TypePackId reduce(TypePackId tp); + + std::optional intersectionType(TypeId left, TypeId right); + std::optional unionType(TypeId left, TypeId right); + TypeId tableType(TypeId ty); + TypeId functionType(TypeId ty); + TypeId negationType(TypeId ty); + + std::deque seen; + int depth = 0; + + RecursionGuard guard(TypeId ty); + RecursionGuard guard(TypePackId tp); + + std::unordered_map copies; + + template + LUAU_NOINLINE std::pair copy(TypeId ty, const T* t) + { + if (auto it = copies.find(ty); it != copies.end()) + return {it->second, getMutable(it->second)}; + + TypeId copiedTy = arena->addType(*t); + copies[ty] = copiedTy; + return {copiedTy, getMutable(copiedTy)}; + } + + using Folder = std::optional (TypeReducer::*)(TypeId, TypeId); + + template + void foldl_impl(Iter it, Iter endIt, Folder f, NotNull> 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(currentTy)) + { + foldl_impl(begin(t), end(t), f, result); + ++it; + continue; + } + + auto resultIt = result->begin(); + while (resultIt != result->end()) + { + TypeId& ty = *resultIt; + + std::optional 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 + TypeId foldl(Iter it, Iter endIt, Folder f) + { + std::vector result; + foldl_impl(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(ty)) + return foldl(begin(i), end(i), &TypeReducer::intersectionType); + else if (auto u = get(ty)) + return foldl(begin(u), end(u), &TypeReducer::unionType); + else if (get(ty) || get(ty)) + return tableType(ty); + else if (get(ty)) + return functionType(ty); + else if (auto n = get(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 head; + while (it != end(tp)) + { + head.push_back(reduce(*it)); + ++it; + } + + std::optional tail = it.tail(); + if (tail) + { + if (auto vtp = get(follow(*it.tail()))) + tail = arena->addTypePack(VariadicTypePack{reduce(vtp->ty), vtp->hidden}); + } + + return arena->addTypePack(TypePack{std::move(head), tail}); +} + +std::optional TypeReducer::intersectionType(TypeId left, TypeId right) +{ + LUAU_ASSERT(!get(left)); + LUAU_ASSERT(!get(right)); + + if (get(left)) + return left; // never & T ~ never + else if (get(right)) + return right; // T & never ~ never + else if (get(left)) + return right; // unknown & T ~ T + else if (get(right)) + return left; // T & unknown ~ T + else if (get(left)) + return right; // any & T ~ T + else if (get(right)) + return left; // T & any ~ T + else if (get(left)) + return std::nullopt; // error & T ~ error & T + else if (get(right)) + return std::nullopt; // T & error ~ T & error + else if (auto ut = get(left)) + { + std::vector 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(begin(options), end(options), &TypeReducer::unionType); // (A | B) & T ~ (A & T) | (B & T) + } + else if (get(right)) + return intersectionType(right, left); // T & (A | B) ~ (A | B) & T + else if (auto [p1, p2] = get2(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(left, right); p && s) + { + if (p->type == PrimitiveType::String && get(s)) + return right; // string & "A" ~ "A" + else if (p->type == PrimitiveType::Boolean && get(s)) + return right; // boolean & true ~ true + else + return builtinTypes->neverType; // string & true ~ never + } + else if (auto [s, p] = get2(left, right); s && p) + return intersectionType(right, left); // S & P ~ P & S + else if (auto [p, f] = get2(left, right); p && f) + { + if (p->type == PrimitiveType::Function) + return right; // function & () -> () ~ () -> () + else + return builtinTypes->neverType; // string & () -> () ~ never + } + else if (auto [f, p] = get2(left, right); f && p) + return intersectionType(right, left); // () -> () & P ~ P & () -> () + else if (auto [s1, s2] = get2(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(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(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(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(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 parts{prop.type, other->second.type}; + TypeId propTy = foldl(begin(parts), end(parts), &TypeReducer::intersectionType); + if (get(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 keyParts{t1->indexer->indexType, t2->indexer->indexType}; + TypeId keyTy = foldl(begin(keyParts), end(keyParts), &TypeReducer::intersectionType); + if (get(keyTy)) + return builtinTypes->neverType; // { [string]: _ } & { [number]: _ } ~ { [string & number]: _ } ~ { [never]: _ } ~ never + + std::vector valueParts{t1->indexer->indexResultType, t2->indexer->indexResultType}; + TypeId valueTy = foldl(begin(valueParts), end(valueParts), &TypeReducer::intersectionType); + if (get(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(left, right); mt && tt) + return std::nullopt; // TODO + else if (auto [tt, mt] = get2(left, right); tt && mt) + return intersectionType(right, left); // T & M ~ M & T + else if (auto [m1, m2] = get2(left, right); m1 && m2) + return std::nullopt; // TODO + else if (auto nl = get(left)) + { + // These should've been reduced already. + TypeId nlTy = follow(nl->ty); + LUAU_ASSERT(!get(nlTy)); + LUAU_ASSERT(!get(nlTy)); + LUAU_ASSERT(!get(nlTy)); + LUAU_ASSERT(!get(nlTy)); + LUAU_ASSERT(!get(nlTy)); + + if (auto [np, p] = get2(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(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(nlTy, right); ns && p) + { + if (get(ns) && p->type == PrimitiveType::String) + return std::nullopt; // ~"A" & string ~ ~"A" & string + else if (get(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(ns); + return arena->addType(SingletonType{BooleanSingleton{!b->value}}); // ~false & boolean ~ true + } + else + return right; // ~"A" & number ~ number + } + else if (auto [np, s] = get2(nlTy, right); np && s) + { + if (np->type == PrimitiveType::String && get(s)) + return builtinTypes->neverType; // ~string & "A" ~ never + else if (np->type == PrimitiveType::Boolean && get(s)) + return builtinTypes->neverType; // ~boolean & true ~ never + else + return right; // ~P & "A" ~ "A" + } + else if (auto [np, f] = get2(nlTy, right); np && f) + { + if (np->type == PrimitiveType::Function) + return builtinTypes->neverType; // ~function & () -> () ~ never + else + return right; // ~string & () -> () ~ () -> () + } + else if (auto [nc, c] = get2(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(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 TypeReducer::unionType(TypeId left, TypeId right) +{ + LUAU_ASSERT(!get(left)); + LUAU_ASSERT(!get(right)); + + if (get(left)) + return right; // never | T ~ T + else if (get(right)) + return left; // T | never ~ T + else if (get(left)) + return left; // unknown | T ~ unknown + else if (get(right)) + return right; // T | unknown ~ unknown + else if (get(left)) + return left; // any | T ~ any + else if (get(right)) + return right; // T | any ~ any + else if (get(left)) + return std::nullopt; // error | T ~ error | T + else if (get(right)) + return std::nullopt; // T | error ~ T | error + else if (auto [p1, p2] = get2(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(left, right); p && s) + { + if (p->type == PrimitiveType::String && get(s)) + return left; // string | "A" ~ string + else if (p->type == PrimitiveType::Boolean && get(s)) + return left; // boolean | true ~ boolean + else + return std::nullopt; // string | true ~ string | true + } + else if (auto [s, p] = get2(left, right); s && p) + return unionType(right, left); // S | P ~ P | S + else if (auto [p, f] = get2(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(left, right); f && p) + return unionType(right, left); // () -> () | P ~ P | () -> () + else if (auto [s1, s2] = get2(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(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(left, right); nt && it) + { + std::vector 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(begin(parts), end(parts), &TypeReducer::intersectionType); // ~T | (A & B) ~ (~T | A) & (~T | B) + } + else if (auto [it, nt] = get2(left, right); it && nt) + return unionType(right, left); // (A & B) | ~T ~ ~T | (A & B) + else if (auto [nl, nr] = get2(left, right); nl && nr) + { + // These should've been reduced already. + TypeId nlTy = follow(nl->ty); + TypeId nrTy = follow(nr->ty); + LUAU_ASSERT(!get(nlTy) && !get(nrTy)); + LUAU_ASSERT(!get(nlTy) && !get(nrTy)); + LUAU_ASSERT(!get(nlTy) && !get(nrTy)); + LUAU_ASSERT(!get(nlTy) && !get(nrTy)); + LUAU_ASSERT(!get(nlTy) && !get(nrTy)); + + if (auto [npl, npr] = get2(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(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(nlTy, nrTy); ns && np) + { + if (get(ns) && np->type == PrimitiveType::String) + return left; // ~"A" | ~string ~ ~"A" + else if (get(ns) && np->type == PrimitiveType::Boolean) + return left; // ~false | ~boolean ~ ~false + else + return builtinTypes->unknownType; // ~"A" | ~P ~ unknown + } + else if (auto [np, ns] = get2(nlTy, nrTy); np && ns) + return unionType(right, left); // ~P | ~S ~ ~S | ~P + else + return std::nullopt; // TODO! + } + else if (auto nl = get(left)) + { + // These should've been reduced already. + TypeId nlTy = follow(nl->ty); + LUAU_ASSERT(!get(nlTy)); + LUAU_ASSERT(!get(nlTy)); + LUAU_ASSERT(!get(nlTy)); + LUAU_ASSERT(!get(nlTy)); + LUAU_ASSERT(!get(nlTy)); + + if (auto [np, p] = get2(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(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(nlTy, right); ns && p) + { + if (get(ns) && p->type == PrimitiveType::String) + return builtinTypes->unknownType; // ~"A" | string ~ unknown + else if (get(ns) && p->type == PrimitiveType::Boolean) + return builtinTypes->unknownType; // ~false | boolean ~ unknown + else + return left; // ~"A" | T ~ ~"A" + } + else if (auto [np, s] = get2(nlTy, right); np && s) + { + if (np->type == PrimitiveType::String && get(s)) + return std::nullopt; // ~string | "A" ~ ~string | "A" + else if (np->type == PrimitiveType::Boolean && get(s)) + { + const BooleanSingleton* b = get(s); + return negationType(arena->addType(SingletonType{BooleanSingleton{!b->value}})); // ~boolean | false ~ ~true + } + else + return left; // ~P | "A" ~ ~P + } + else if (auto [nc, c] = get2(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(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(ty)) + { + auto [copiedTy, copied] = copy(ty, mt); + copied->table = reduce(mt->table); + copied->metatable = reduce(mt->metatable); + return copiedTy; + } + else if (auto tt = get(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(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(ty)) + return nn->ty; // ~~T ~ T + else if (get(ty)) + return builtinTypes->unknownType; // ~never ~ unknown + else if (get(ty)) + return builtinTypes->neverType; // ~unknown ~ never + else if (get(ty)) + return builtinTypes->anyType; // ~any ~ any + else if (auto ni = get(ty)) + { + std::vector options; + for (TypeId part : ni) + options.push_back(negationType(part)); + return foldl(begin(options), end(options), &TypeReducer::unionType); // ~(T & U) ~ (~T | ~U) + } + else if (auto nu = get(ty)) + { + std::vector parts; + for (TypeId option : nu) + parts.push_back(negationType(option)); + return foldl(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 arena, NotNull builtinTypes, NotNull handle) + : arena(arena) + , builtinTypes(builtinTypes) + , handle(handle) +{ +} + +std::optional 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 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 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 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 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(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(ty)) + return acc * std::distance(begin(ut), end(ut)); + else if (get(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(follow(*tail))) + { + if (hasExceededCartesianProductLimit(vtp->ty)) + return true; + } + } + + return false; +} + +} // namespace Luau diff --git a/Compiler/src/Compiler.cpp b/Compiler/src/Compiler.cpp index b4dea4f5..8a1e80fc 100644 --- a/Compiler/src/Compiler.cpp +++ b/Compiler/src/Compiler.cpp @@ -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 { diff --git a/Sources.cmake b/Sources.cmake index 437ff993..87d76bf3 100644 --- a/Sources.cmake +++ b/Sources.cmake @@ -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 diff --git a/tests/Autocomplete.test.cpp b/tests/Autocomplete.test.cpp index 10582947..f241963a 100644 --- a/tests/Autocomplete.test.cpp +++ b/tests/Autocomplete.test.cpp @@ -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 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 ty2 = ac2.entryMap.at("thingamabob").type; + REQUIRE(ty2); + CHECK("{| x: (number & string)? |}" == toString(*ty2, opts)); + // CHECK("{| x: nil |}" == toString(*ty2, opts)); +} + TEST_SUITE_END(); diff --git a/tests/Compiler.test.cpp b/tests/Compiler.test.cpp index d2cf0ae8..1a606126 100644 --- a/tests/Compiler.test.cpp +++ b/tests/Compiler.test.cpp @@ -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(); diff --git a/tests/ConstraintGraphBuilderFixture.cpp b/tests/ConstraintGraphBuilderFixture.cpp index 64e6baaf..4d7ee4fe 100644 --- a/tests/ConstraintGraphBuilderFixture.cpp +++ b/tests/ConstraintGraphBuilderFixture.cpp @@ -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(NotNull{&mainModule->internalTypes}, builtinTypes, NotNull{&ice}); + BlockedType::nextIndex = 0; BlockedTypePack::nextIndex = 0; } diff --git a/tests/Fixture.cpp b/tests/Fixture.cpp index 41629281..5ff00627 100644 --- a/tests/Fixture.cpp +++ b/tests/Fixture.cpp @@ -312,6 +312,9 @@ std::optional 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 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 Fixture::lookupType(const std::string& name) std::optional 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 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 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(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(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& constraints) diff --git a/tests/Fixture.h b/tests/Fixture.h index 3edd6b4c..6dc8abf2 100644 --- a/tests/Fixture.h +++ b/tests/Fixture.h @@ -91,6 +91,7 @@ struct Fixture std::optional lookupType(const std::string& name); std::optional 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 lookupName(ScopePtr scope, const std::string& name); // Wa std::optional linearSearchForBinding(Scope* scope, const char* name); -void registerHiddenTypes(Fixture& fixture, TypeArena& arena); +void registerHiddenTypes(Frontend* frontend); +void createSomeClasses(Frontend* frontend); } // namespace Luau diff --git a/tests/Frontend.test.cpp b/tests/Frontend.test.cpp index 93df5605..a69965e0 100644 --- a/tests/Frontend.test.cpp +++ b/tests/Frontend.test.cpp @@ -10,6 +10,8 @@ #include +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 aExports = first(aModule->getModuleScope()->returnType); + std::optional aExports = first(aModule->returnType); REQUIRE(bool(aExports)); ModulePtr bModule = frontend.moduleResolver.modules["game/Gui/Modules/B"]; REQUIRE(bool(bModule)); - std::optional bExports = first(bModule->getModuleScope()->returnType); + std::optional 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 cExports = first(cModule->getModuleScope()->returnType); + std::optional 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)); diff --git a/tests/Module.test.cpp b/tests/Module.test.cpp index 5f97fb6c..34c2e8fd 100644 --- a/tests/Module.test.cpp +++ b/tests/Module.test.cpp @@ -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 exports = first(module->getModuleScope()->returnType); + std::optional 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(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 typeA = first(modA->getModuleScope()->returnType); - std::optional typeB = first(modB->getModuleScope()->returnType); + std::optional typeA = first(modA->returnType); + std::optional typeB = first(modB->returnType); REQUIRE(typeA); REQUIRE(typeB); TableType* tableA = getMutable(*typeA); diff --git a/tests/NonstrictMode.test.cpp b/tests/NonstrictMode.test.cpp index 8a25a5e5..5deeb35d 100644 --- a/tests/NonstrictMode.test.cpp +++ b/tests/NonstrictMode.test.cpp @@ -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") diff --git a/tests/Normalize.test.cpp b/tests/Normalize.test.cpp index 2ee82623..615fc997 100644 --- a/tests/Normalize.test.cpp +++ b/tests/Normalize.test.cpp @@ -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(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(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) | Unrelated"))); CHECK("((class & ~Child) | boolean | function | number | string | thread)?" == toString(normal("Not"))); CHECK("Child" == toString(normal("Not & 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"))); } diff --git a/tests/TypeInfer.aliases.test.cpp b/tests/TypeInfer.aliases.test.cpp index 4dd82269..9b65cf24 100644 --- a/tests/TypeInfer.aliases.test.cpp +++ b/tests/TypeInfer.aliases.test.cpp @@ -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 aTypeId = lookupName(m->getModuleScope(), "a"); - REQUIRE(aTypeId); - const Luau::TableType* aType = get(follow(*aTypeId)); + TypeId aTypeId = requireType("workspace/C", "a"); + const Luau::TableType* aType = get(follow(aTypeId)); REQUIRE(aType); REQUIRE(aType->props.size() == 2); - std::optional bTypeId = lookupName(m->getModuleScope(), "b"); - REQUIRE(bTypeId); - const Luau::TableType* bType = get(follow(*bTypeId)); + TypeId bTypeId = requireType("workspace/C", "b"); + const Luau::TableType* bType = get(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(ty); REQUIRE(ttv); diff --git a/tests/TypeInfer.annotations.test.cpp b/tests/TypeInfer.annotations.test.cpp index bf66ecbc..3e98367c 100644 --- a/tests/TypeInfer.annotations.test.cpp +++ b/tests/TypeInfer.annotations.test.cpp @@ -319,10 +319,10 @@ TEST_CASE_FIXTURE(Fixture, "self_referential_type_alias") LUAU_REQUIRE_NO_ERRORS(result); - std::optional res = getMainModule()->getModuleScope()->lookupType("O"); + std::optional res = lookupType("O"); REQUIRE(res); - TypeId oType = follow(res->type); + TypeId oType = follow(*res); const TableType* oTable = get(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 exportsType = first(mod.getModuleScope()->returnType); + std::optional exportsType = first(mod.returnType); REQUIRE(exportsType); TableType* exportsTable = getMutable(*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 = { [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 exportsType = first(mod.getModuleScope()->returnType); + std::optional exportsType = first(mod.returnType); REQUIRE(exportsType); TableType* exportsTable = getMutable(*exportsType); diff --git a/tests/TypeInfer.functions.test.cpp b/tests/TypeInfer.functions.test.cpp index a97cea21..70de13d1 100644 --- a/tests/TypeInfer.functions.test.cpp +++ b/tests/TypeInfer.functions.test.cpp @@ -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(*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 diff --git a/tests/TypeInfer.generics.test.cpp b/tests/TypeInfer.generics.test.cpp index 7b417621..3861a8b6 100644 --- a/tests/TypeInfer.generics.test.cpp +++ b/tests/TypeInfer.generics.test.cpp @@ -1021,9 +1021,9 @@ TEST_CASE_FIXTURE(Fixture, "no_stack_overflow_from_quantifying") LUAU_REQUIRE_ERRORS(result); - std::optional t0 = getMainModule()->getModuleScope()->lookupType("t0"); + std::optional 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(err); diff --git a/tests/TypeInfer.negations.test.cpp b/tests/TypeInfer.negations.test.cpp index 02350a72..261314a6 100644 --- a/tests/TypeInfer.negations.test.cpp +++ b/tests/TypeInfer.negations.test.cpp @@ -20,7 +20,7 @@ struct NegationFixture : Fixture NegationFixture() { - registerHiddenTypes(*this, arena); + registerHiddenTypes(&frontend); } }; diff --git a/tests/TypeInfer.operators.test.cpp b/tests/TypeInfer.operators.test.cpp index 0196666a..5db5b880 100644 --- a/tests/TypeInfer.operators.test.cpp +++ b/tests/TypeInfer.operators.test.cpp @@ -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 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(result.errors[0]); + const GenericError* ge = get(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") diff --git a/tests/TypeInfer.provisional.test.cpp b/tests/TypeInfer.provisional.test.cpp index cf969f2d..3e278ca2 100644 --- a/tests/TypeInfer.provisional.test.cpp +++ b/tests/TypeInfer.provisional.test.cpp @@ -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 result = first(m->getModuleScope()->returnType); + std::optional result = first(m->returnType); REQUIRE(result); CHECK(get(*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 result = first(m->getModuleScope()->returnType); + std::optional result = first(m->returnType); REQUIRE(result); CHECK(get(*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 diff --git a/tests/TypeInfer.tables.test.cpp b/tests/TypeInfer.tables.test.cpp index e2aa01f9..dc3b7ceb 100644 --- a/tests/TypeInfer.tables.test.cpp +++ b/tests/TypeInfer.tables.test.cpp @@ -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(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("({+ blah: a +}) -> ()", toString(requireType("a"))); CHECK_EQ("({+ gwar: a +}) -> ()", toString(requireType("b"))); - CHECK_EQ("() -> ({+ blah: a, gwar: b +}) -> ()", toString(getMainModule()->getModuleScope()->returnType)); + CHECK_EQ("() -> ({+ 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 diff --git a/tests/TypeInfer.test.cpp b/tests/TypeInfer.test.cpp index f6279fa2..f4b84262 100644 --- a/tests/TypeInfer.test.cpp +++ b/tests/TypeInfer.test.cpp @@ -648,10 +648,10 @@ TEST_CASE_FIXTURE(Fixture, "no_stack_overflow_from_isoptional") LUAU_REQUIRE_ERRORS(result); - std::optional t0 = getMainModule()->getModuleScope()->lookupType("t0"); + std::optional 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(err); diff --git a/tests/TypeInfer.typePacks.cpp b/tests/TypeInfer.typePacks.cpp index b753d30e..94448cfa 100644 --- a/tests/TypeInfer.typePacks.cpp +++ b/tests/TypeInfer.typePacks.cpp @@ -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); diff --git a/tests/TypeInfer.unionTypes.test.cpp b/tests/TypeInfer.unionTypes.test.cpp index d3022095..8831bb2e 100644 --- a/tests/TypeInfer.unionTypes.test.cpp +++ b/tests/TypeInfer.unionTypes.test.cpp @@ -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 diff --git a/tests/TypeReduction.test.cpp b/tests/TypeReduction.test.cpp new file mode 100644 index 00000000..c629b3e3 --- /dev/null +++ b/tests/TypeReduction.test.cpp @@ -0,0 +1,1249 @@ +// 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 "Fixture.h" +#include "doctest.h" + +using namespace Luau; + +namespace +{ +struct ReductionFixture : Fixture +{ + TypeArena arena; + InternalErrorReporter iceHandler; + UnifierSharedState unifierState{&iceHandler}; + TypeReduction reduction{NotNull{&arena}, builtinTypes, NotNull{&iceHandler}}; + + ReductionFixture() + { + registerHiddenTypes(&frontend); + createSomeClasses(&frontend); + } + + TypeId reductionof(TypeId ty) + { + std::optional reducedTy = reduction.reduce(ty); + REQUIRE(reducedTy); + return *reducedTy; + } + + std::optional tryReduce(const std::string& annotation) + { + CheckResult result = check("type _Res = " + annotation); + LUAU_REQUIRE_NO_ERRORS(result); + return reduction.reduce(requireTypeAlias("_Res")); + } + + TypeId reductionof(const std::string& annotation) + { + std::optional reducedTy = tryReduce(annotation); + REQUIRE_MESSAGE(reducedTy, "Exceeded the cartesian product of the type"); + return *reducedTy; + } +}; +} // namespace + +TEST_SUITE_BEGIN("TypeReductionTests"); + +TEST_CASE_FIXTURE(ReductionFixture, "cartesian_product_exceeded") +{ + ScopedFastInt sfi{"LuauTypeReductionCartesianProductLimit", 5}; + + std::optional ty = tryReduce(R"( + string & (number | string | boolean) & (number | string | boolean) + )"); + + CHECK(!ty); +} + +TEST_CASE_FIXTURE(ReductionFixture, "cartesian_product_exceeded_with_normal_limit") +{ + std::optional ty = tryReduce(R"( + string -- 1 = 1 + & (number | string | boolean) -- 1 * 3 = 3 + & (number | string | boolean) -- 3 * 3 = 9 + & (number | string | boolean) -- 9 * 3 = 27 + & (number | string | boolean) -- 27 * 3 = 81 + & (number | string | boolean) -- 81 * 3 = 243 + & (number | string | boolean) -- 243 * 3 = 729 + & (number | string | boolean) -- 729 * 3 = 2187 + & (number | string | boolean) -- 2187 * 3 = 6561 + & (number | string | boolean) -- 6561 * 3 = 19683 + & (number | string | boolean) -- 19683 * 3 = 59049 + & (number | string) -- 59049 * 2 = 118098 + )"); + + CHECK(!ty); +} + +TEST_CASE_FIXTURE(ReductionFixture, "cartesian_product_is_zero") +{ + ScopedFastInt sfi{"LuauTypeReductionCartesianProductLimit", 5}; + + std::optional ty = tryReduce(R"( + string & (number | string | boolean) & (number | string | boolean) & never + )"); + + CHECK(ty); +} + +TEST_CASE_FIXTURE(ReductionFixture, "intersections_without_negations") +{ + SUBCASE("string_and_string") + { + TypeId ty = reductionof("string & string"); + CHECK("string" == toString(ty)); + } + + SUBCASE("never_and_string") + { + TypeId ty = reductionof("never & string"); + CHECK("never" == toString(ty)); + } + + SUBCASE("string_and_never") + { + TypeId ty = reductionof("string & never"); + CHECK("never" == toString(ty)); + } + + SUBCASE("unknown_and_string") + { + TypeId ty = reductionof("unknown & string"); + CHECK("string" == toString(ty)); + } + + SUBCASE("string_and_unknown") + { + TypeId ty = reductionof("string & unknown"); + CHECK("string" == toString(ty)); + } + + SUBCASE("any_and_string") + { + TypeId ty = reductionof("any & string"); + CHECK("string" == toString(ty)); + } + + SUBCASE("string_and_any") + { + TypeId ty = reductionof("string & any"); + CHECK("string" == toString(ty)); + } + + SUBCASE("string_or_number_and_string") + { + TypeId ty = reductionof("(string | number) & string"); + CHECK("string" == toString(ty)); + } + + SUBCASE("string_and_string_or_number") + { + TypeId ty = reductionof("string & (string | number)"); + CHECK("string" == toString(ty)); + } + + SUBCASE("string_and_a") + { + TypeId ty = reductionof(R"(string & "a")"); + CHECK(R"("a")" == toString(ty)); + } + + SUBCASE("boolean_and_true") + { + TypeId ty = reductionof("boolean & true"); + CHECK("true" == toString(ty)); + } + + SUBCASE("boolean_and_a") + { + TypeId ty = reductionof(R"(boolean & "a")"); + CHECK("never" == toString(ty)); + } + + SUBCASE("a_and_a") + { + TypeId ty = reductionof(R"("a" & "a")"); + CHECK(R"("a")" == toString(ty)); + } + + SUBCASE("a_and_b") + { + TypeId ty = reductionof(R"("a" & "b")"); + CHECK("never" == toString(ty)); + } + + SUBCASE("a_and_true") + { + TypeId ty = reductionof(R"("a" & true)"); + CHECK("never" == toString(ty)); + } + + SUBCASE("a_and_true") + { + TypeId ty = reductionof(R"(true & false)"); + CHECK("never" == toString(ty)); + } + + SUBCASE("function_type_and_function") + { + TypeId ty = reductionof("() -> () & fun"); + CHECK("() -> ()" == toString(ty)); + } + + SUBCASE("function_type_and_string") + { + TypeId ty = reductionof("() -> () & string"); + CHECK("never" == toString(ty)); + } + + SUBCASE("parent_and_child") + { + TypeId ty = reductionof("Parent & Child"); + CHECK("Child" == toString(ty)); + } + + SUBCASE("child_and_parent") + { + TypeId ty = reductionof("Child & Parent"); + CHECK("Child" == toString(ty)); + } + + SUBCASE("child_and_unrelated") + { + TypeId ty = reductionof("Child & Unrelated"); + CHECK("never" == toString(ty)); + } + + SUBCASE("string_and_table") + { + TypeId ty = reductionof("string & {}"); + CHECK("never" == toString(ty)); + } + + SUBCASE("string_and_child") + { + TypeId ty = reductionof("string & Child"); + CHECK("never" == toString(ty)); + } + + SUBCASE("string_and_function") + { + TypeId ty = reductionof("string & () -> ()"); + CHECK("never" == toString(ty)); + } + + SUBCASE("function_and_table") + { + TypeId ty = reductionof("() -> () & {}"); + CHECK("never" == toString(ty)); + } + + SUBCASE("function_and_class") + { + TypeId ty = reductionof("() -> () & Child"); + CHECK("never" == toString(ty)); + } + + SUBCASE("function_and_function") + { + TypeId ty = reductionof("() -> () & () -> ()"); + CHECK("(() -> ()) & (() -> ())" == toString(ty)); + } + + SUBCASE("table_and_table") + { + TypeId ty = reductionof("{} & {}"); + CHECK("{| |}" == toString(ty)); + } + + SUBCASE("table_and_metatable") + { + // No setmetatable in ReductionFixture, so we mix and match. + BuiltinsFixture fixture; + fixture.check(R"( + type Ty = {} & typeof(setmetatable({}, {})) + )"); + + TypeId ty = reductionof(fixture.requireTypeAlias("Ty")); + CHECK("{ @metatable { }, { } } & {| |}" == toString(ty)); + } + + SUBCASE("a_and_string") + { + TypeId ty = reductionof(R"("a" & string)"); + CHECK(R"("a")" == toString(ty)); + } + + SUBCASE("reducible_function_and_function") + { + TypeId ty = reductionof("((string | string) -> (number | number)) & fun"); + CHECK("(string) -> number" == toString(ty)); + } + + SUBCASE("string_and_error") + { + TypeId ty = reductionof("string & err"); + CHECK("*error-type* & string" == toString(ty)); + } + + SUBCASE("table_p_string_and_table_p_number") + { + TypeId ty = reductionof("{ p: string } & { p: number }"); + CHECK("never" == toString(ty)); + } + + SUBCASE("table_p_string_and_table_p_string") + { + TypeId ty = reductionof("{ p: string } & { p: string }"); + CHECK("{| p: string |}" == toString(ty)); + } + + SUBCASE("table_x_table_p_string_and_table_x_table_p_number") + { + TypeId ty = reductionof("{ x: { p: string } } & { x: { p: number } }"); + CHECK("never" == toString(ty)); + } + + SUBCASE("table_p_and_table_q") + { + TypeId ty = reductionof("{ p: string } & { q: number }"); + CHECK("{| p: string, q: number |}" == toString(ty)); + } + + SUBCASE("table_tag_a_or_table_tag_b_and_table_b") + { + TypeId ty = reductionof("({ tag: string, a: number } | { tag: number, b: string }) & { b: string }"); + CHECK("{| a: number, b: string, tag: string |} | {| b: string, tag: number |}" == toString(ty)); + } + + SUBCASE("table_string_number_indexer_and_table_string_number_indexer") + { + TypeId ty = reductionof("{ [string]: number } & { [string]: number }"); + CHECK("{| [string]: number |}" == toString(ty)); + } + + SUBCASE("table_string_number_indexer_and_empty_table") + { + TypeId ty = reductionof("{ [string]: number } & {}"); + CHECK("{| [string]: number |}" == toString(ty)); + } + + SUBCASE("empty_table_table_string_number_indexer") + { + TypeId ty = reductionof("{} & { [string]: number }"); + CHECK("{| [string]: number |}" == toString(ty)); + } + + SUBCASE("string_number_indexer_and_number_number_indexer") + { + TypeId ty = reductionof("{ [string]: number } & { [number]: number }"); + CHECK("never" == toString(ty)); + } + + SUBCASE("table_p_string_and_indexer_number_number") + { + TypeId ty = reductionof("{ p: string } & { [number]: number }"); + CHECK("{| [number]: number, p: string |}" == toString(ty)); + } + + SUBCASE("table_p_string_and_indexer_string_number") + { + TypeId ty = reductionof("{ p: string } & { [string]: number }"); + CHECK("{| [string]: number, p: string |}" == toString(ty)); + } + + SUBCASE("table_p_string_and_table_p_string_plus_indexer_string_number") + { + TypeId ty = reductionof("{ p: string } & { p: string, [string]: number }"); + CHECK("{| [string]: number, p: string |}" == toString(ty)); + } +} // intersections_without_negations + +TEST_CASE_FIXTURE(ReductionFixture, "intersections_with_negations") +{ + SUBCASE("nil_and_not_nil") + { + TypeId ty = reductionof("nil & Not"); + CHECK("never" == toString(ty)); + } + + SUBCASE("nil_and_not_false") + { + TypeId ty = reductionof("nil & Not"); + CHECK("nil" == toString(ty)); + } + + SUBCASE("string_or_nil_and_not_nil") + { + TypeId ty = reductionof("(string?) & Not"); + CHECK("string" == toString(ty)); + } + + SUBCASE("string_or_nil_and_not_false_or_nil") + { + TypeId ty = reductionof("(string?) & Not"); + CHECK("string" == toString(ty)); + } + + SUBCASE("string_or_nil_and_not_false_and_not_nil") + { + TypeId ty = reductionof("(string?) & Not & Not"); + CHECK("string" == toString(ty)); + } + + SUBCASE("not_false_and_bool") + { + TypeId ty = reductionof("Not & boolean"); + CHECK("true" == toString(ty)); + } + + SUBCASE("function_type_and_not_function") + { + TypeId ty = reductionof("() -> () & Not"); + CHECK("never" == toString(ty)); + } + + SUBCASE("function_type_and_not_string") + { + TypeId ty = reductionof("() -> () & Not"); + CHECK("() -> ()" == toString(ty)); + } + + SUBCASE("not_a_and_string_or_nil") + { + TypeId ty = reductionof(R"(Not<"a"> & (string | nil))"); + CHECK(R"((string & ~"a")?)" == toString(ty)); + } + + SUBCASE("not_a_and_a") + { + TypeId ty = reductionof(R"(Not<"a"> & "a")"); + CHECK("never" == toString(ty)); + } + + SUBCASE("not_a_and_b") + { + TypeId ty = reductionof(R"(Not<"a"> & "b")"); + CHECK(R"("b")" == toString(ty)); + } + + SUBCASE("not_string_and_a") + { + TypeId ty = reductionof(R"(Not & "a")"); + CHECK("never" == toString(ty)); + } + + SUBCASE("not_bool_and_true") + { + TypeId ty = reductionof("Not & true"); + CHECK("never" == toString(ty)); + } + + SUBCASE("not_string_and_true") + { + TypeId ty = reductionof("Not & true"); + CHECK("true" == toString(ty)); + } + + SUBCASE("parent_and_not_child") + { + TypeId ty = reductionof("Parent & Not"); + CHECK("Parent & ~Child" == toString(ty)); + } + + SUBCASE("not_child_and_parent") + { + TypeId ty = reductionof("Not & Parent"); + CHECK("Parent & ~Child" == toString(ty)); + } + + SUBCASE("child_and_not_parent") + { + TypeId ty = reductionof("Child & Not"); + CHECK("never" == toString(ty)); + } + + SUBCASE("not_parent_and_child") + { + TypeId ty = reductionof("Not & Child"); + CHECK("never" == toString(ty)); + } + + SUBCASE("not_parent_and_unrelated") + { + TypeId ty = reductionof("Not & Unrelated"); + CHECK("Unrelated" == toString(ty)); + } + + SUBCASE("unrelated_and_not_parent") + { + TypeId ty = reductionof("Unrelated & Not"); + CHECK("Unrelated" == toString(ty)); + } + + SUBCASE("not_unrelated_and_parent") + { + TypeId ty = reductionof("Not & Parent"); + CHECK("Parent" == toString(ty)); + } + + SUBCASE("parent_and_not_unrelated") + { + TypeId ty = reductionof("Parent & Not"); + CHECK("Parent" == toString(ty)); + } + + SUBCASE("reducible_function_and_not_function") + { + TypeId ty = reductionof("((string | string) -> (number | number)) & Not"); + CHECK("never" == toString(ty)); + } + + SUBCASE("string_and_not_error") + { + TypeId ty = reductionof("string & Not"); + CHECK("string & ~*error-type*" == toString(ty)); + } + + SUBCASE("table_p_string_and_table_p_not_number") + { + TypeId ty = reductionof("{ p: string } & { p: Not }"); + CHECK("{| p: string |}" == toString(ty)); + } + + SUBCASE("table_p_string_and_table_p_not_string") + { + TypeId ty = reductionof("{ p: string } & { p: Not }"); + CHECK("never" == toString(ty)); + } + + SUBCASE("table_x_table_p_string_and_table_x_table_p_not_number") + { + TypeId ty = reductionof("{ x: { p: string } } & { x: { p: Not } }"); + CHECK("{| x: {| p: string |} |}" == toString(ty)); + } +} // intersections_with_negations + +TEST_CASE_FIXTURE(ReductionFixture, "unions_without_negations") +{ + SUBCASE("never_or_string") + { + TypeId ty = reductionof("never | string"); + CHECK("string" == toString(ty)); + } + + SUBCASE("string_or_never") + { + TypeId ty = reductionof("string | never"); + CHECK("string" == toString(ty)); + } + + SUBCASE("unknown_or_string") + { + TypeId ty = reductionof("unknown | string"); + CHECK("unknown" == toString(ty)); + } + + SUBCASE("string_or_unknown") + { + TypeId ty = reductionof("string | unknown"); + CHECK("unknown" == toString(ty)); + } + + SUBCASE("any_or_string") + { + TypeId ty = reductionof("any | string"); + CHECK("any" == toString(ty)); + } + + SUBCASE("string_or_any") + { + TypeId ty = reductionof("string | any"); + CHECK("any" == toString(ty)); + } + + SUBCASE("string_or_string_and_number") + { + TypeId ty = reductionof("string | (string & number)"); + CHECK("string" == toString(ty)); + } + + SUBCASE("string_or_string") + { + TypeId ty = reductionof("string | string"); + CHECK("string" == toString(ty)); + } + + SUBCASE("string_or_number") + { + TypeId ty = reductionof("string | number"); + CHECK("number | string" == toString(ty)); + } + + SUBCASE("number_or_string") + { + TypeId ty = reductionof("number | string"); + CHECK("number | string" == toString(ty)); + } + + SUBCASE("string_or_number_or_string") + { + TypeId ty = reductionof("(string | number) | string"); + CHECK("number | string" == toString(ty)); + } + + SUBCASE("string_or_number_or_string_2") + { + TypeId ty = reductionof("string | (number | string)"); + CHECK("number | string" == toString(ty)); + } + + SUBCASE("string_or_string_or_number") + { + TypeId ty = reductionof("string | (string | number)"); + CHECK("number | string" == toString(ty)); + } + + SUBCASE("string_or_string_or_number_or_boolean") + { + TypeId ty = reductionof("string | (string | number | boolean)"); + CHECK("boolean | number | string" == toString(ty)); + } + + SUBCASE("string_or_string_or_boolean_or_number") + { + TypeId ty = reductionof("string | (string | boolean | number)"); + CHECK("boolean | number | string" == toString(ty)); + } + + SUBCASE("string_or_boolean_or_string_or_number") + { + TypeId ty = reductionof("string | (boolean | string | number)"); + CHECK("boolean | number | string" == toString(ty)); + } + + SUBCASE("boolean_or_string_or_number_or_string") + { + TypeId ty = reductionof("(boolean | string | number) | string"); + CHECK("boolean | number | string" == toString(ty)); + } + + SUBCASE("boolean_or_true") + { + TypeId ty = reductionof("boolean | true"); + CHECK("boolean" == toString(ty)); + } + + SUBCASE("boolean_or_false") + { + TypeId ty = reductionof("boolean | false"); + CHECK("boolean" == toString(ty)); + } + + SUBCASE("boolean_or_true_or_false") + { + TypeId ty = reductionof("boolean | true | false"); + CHECK("boolean" == toString(ty)); + } + + SUBCASE("string_or_a") + { + TypeId ty = reductionof(R"(string | "a")"); + CHECK("string" == toString(ty)); + } + + SUBCASE("a_or_a") + { + TypeId ty = reductionof(R"("a" | "a")"); + CHECK(R"("a")" == toString(ty)); + } + + SUBCASE("a_or_b") + { + TypeId ty = reductionof(R"("a" | "b")"); + CHECK(R"("a" | "b")" == toString(ty)); + } + + SUBCASE("a_or_b_or_string") + { + TypeId ty = reductionof(R"("a" | "b" | string)"); + CHECK("string" == toString(ty)); + } + + SUBCASE("unknown_or_any") + { + TypeId ty = reductionof("unknown | any"); + CHECK("unknown" == toString(ty)); + } + + SUBCASE("any_or_unknown") + { + TypeId ty = reductionof("any | unknown"); + CHECK("unknown" == toString(ty)); + } + + SUBCASE("function_type_or_function") + { + TypeId ty = reductionof("() -> () | fun"); + CHECK("function" == toString(ty)); + } + + SUBCASE("function_or_string") + { + TypeId ty = reductionof("fun | string"); + CHECK("function | string" == toString(ty)); + } + + SUBCASE("parent_or_child") + { + TypeId ty = reductionof("Parent | Child"); + CHECK("Parent" == toString(ty)); + } + + SUBCASE("child_or_parent") + { + TypeId ty = reductionof("Child | Parent"); + CHECK("Parent" == toString(ty)); + } + + SUBCASE("parent_or_unrelated") + { + TypeId ty = reductionof("Parent | Unrelated"); + CHECK("Parent | Unrelated" == toString(ty)); + } + + SUBCASE("parent_or_child_or_unrelated") + { + TypeId ty = reductionof("Parent | Child | Unrelated"); + CHECK("Parent | Unrelated" == toString(ty)); + } + + SUBCASE("parent_or_unrelated_or_child") + { + TypeId ty = reductionof("Parent | Unrelated | Child"); + CHECK("Parent | Unrelated" == toString(ty)); + } + + SUBCASE("parent_or_child_or_unrelated_or_child") + { + TypeId ty = reductionof("Parent | Child | Unrelated | Child"); + CHECK("Parent | Unrelated" == toString(ty)); + } + + SUBCASE("string_or_true") + { + TypeId ty = reductionof("string | true"); + CHECK("string | true" == toString(ty)); + } + + SUBCASE("string_or_function") + { + TypeId ty = reductionof("string | () -> ()"); + CHECK("(() -> ()) | string" == toString(ty)); + } + + SUBCASE("string_or_err") + { + TypeId ty = reductionof("string | err"); + CHECK("*error-type* | string" == toString(ty)); + } +} // unions_without_negations + +TEST_CASE_FIXTURE(ReductionFixture, "unions_with_negations") +{ + SUBCASE("string_or_not_string") + { + TypeId ty = reductionof("string | Not"); + CHECK("unknown" == toString(ty)); + } + + SUBCASE("not_string_or_string") + { + TypeId ty = reductionof("Not | string"); + CHECK("unknown" == toString(ty)); + } + + SUBCASE("not_number_or_string") + { + TypeId ty = reductionof("Not | string"); + CHECK("~number" == toString(ty)); + } + + SUBCASE("string_or_not_number") + { + TypeId ty = reductionof("string | Not"); + CHECK("~number" == toString(ty)); + } + + SUBCASE("not_hi_or_string_and_not_hi") + { + TypeId ty = reductionof(R"(Not<"hi"> | (string & Not<"hi">))"); + CHECK(R"(~"hi")" == toString(ty)); + } + + SUBCASE("string_and_not_hi_or_not_hi") + { + TypeId ty = reductionof(R"((string & Not<"hi">) | Not<"hi">)"); + CHECK(R"(~"hi")" == toString(ty)); + } + + SUBCASE("string_or_not_never") + { + TypeId ty = reductionof("string | Not"); + CHECK("unknown" == toString(ty)); + } + + SUBCASE("not_a_or_not_a") + { + TypeId ty = reductionof(R"(Not<"a"> | Not<"a">)"); + CHECK(R"(~"a")" == toString(ty)); + } + + SUBCASE("not_a_or_a") + { + TypeId ty = reductionof(R"(Not<"a"> | "a")"); + CHECK("unknown" == toString(ty)); + } + + SUBCASE("a_or_not_a") + { + TypeId ty = reductionof(R"("a" | Not<"a">)"); + CHECK("unknown" == toString(ty)); + } + + SUBCASE("not_a_or_string") + { + TypeId ty = reductionof(R"(Not<"a"> | string)"); + CHECK("unknown" == toString(ty)); + } + + SUBCASE("string_or_not_a") + { + TypeId ty = reductionof(R"(string | Not<"a">)"); + CHECK("unknown" == toString(ty)); + } + + SUBCASE("not_string_or_a") + { + TypeId ty = reductionof(R"(Not | "a")"); + CHECK(R"("a" | ~string)" == toString(ty)); + } + + SUBCASE("a_or_not_string") + { + TypeId ty = reductionof(R"("a" | Not)"); + CHECK(R"("a" | ~string)" == toString(ty)); + } + + SUBCASE("not_number_or_a") + { + TypeId ty = reductionof(R"(Not | "a")"); + CHECK("~number" == toString(ty)); + } + + SUBCASE("a_or_not_number") + { + TypeId ty = reductionof(R"("a" | Not)"); + CHECK("~number" == toString(ty)); + } + + SUBCASE("not_a_or_not_b") + { + TypeId ty = reductionof(R"(Not<"a"> | Not<"b">)"); + CHECK("unknown" == toString(ty)); + } + + SUBCASE("boolean_or_not_false") + { + TypeId ty = reductionof("boolean | Not"); + CHECK("unknown" == toString(ty)); + } + + SUBCASE("boolean_or_not_true") + { + TypeId ty = reductionof("boolean | Not"); + CHECK("unknown" == toString(ty)); + } + + SUBCASE("false_or_not_false") + { + TypeId ty = reductionof("false | Not"); + CHECK("unknown" == toString(ty)); + } + + SUBCASE("true_or_not_false") + { + TypeId ty = reductionof("true | Not"); + CHECK("~false" == toString(ty)); + } + + SUBCASE("not_boolean_or_true") + { + TypeId ty = reductionof("Not | true"); + CHECK("~false" == toString(ty)); + } + + SUBCASE("not_false_or_not_boolean") + { + TypeId ty = reductionof("Not | Not"); + CHECK("~false" == toString(ty)); + } + + SUBCASE("function_type_or_not_function") + { + TypeId ty = reductionof("() -> () | Not"); + CHECK("(() -> ()) | ~function" == toString(ty)); + } + + SUBCASE("not_parent_or_child") + { + TypeId ty = reductionof("Not | Child"); + CHECK("Child | ~Parent" == toString(ty)); + } + + SUBCASE("child_or_not_parent") + { + TypeId ty = reductionof("Child | Not"); + CHECK("Child | ~Parent" == toString(ty)); + } + + SUBCASE("parent_or_not_child") + { + TypeId ty = reductionof("Parent | Not"); + CHECK("unknown" == toString(ty)); + } + + SUBCASE("not_child_or_parent") + { + TypeId ty = reductionof("Not | Parent"); + CHECK("unknown" == toString(ty)); + } + + SUBCASE("parent_or_not_unrelated") + { + TypeId ty = reductionof("Parent | Not"); + CHECK("~Unrelated" == toString(ty)); + } + + SUBCASE("not_string_or_string_and_not_a") + { + TypeId ty = reductionof(R"(Not | (string & Not<"a">))"); + CHECK(R"(~"a")" == toString(ty)); + } + + SUBCASE("not_string_or_not_string") + { + TypeId ty = reductionof("Not | Not"); + CHECK("~string" == toString(ty)); + } + + SUBCASE("not_string_or_not_number") + { + TypeId ty = reductionof("Not | Not"); + CHECK("unknown" == toString(ty)); + } + + SUBCASE("not_a_or_not_boolean") + { + TypeId ty = reductionof(R"(Not<"a"> | Not)"); + CHECK("unknown" == toString(ty)); + } + + SUBCASE("not_a_or_boolean") + { + TypeId ty = reductionof(R"(Not<"a"> | boolean)"); + CHECK(R"(~"a")" == toString(ty)); + } + + SUBCASE("string_or_err") + { + TypeId ty = reductionof("string | Not"); + CHECK("string | ~*error-type*" == toString(ty)); + } +} // unions_with_negations + +TEST_CASE_FIXTURE(ReductionFixture, "tables") +{ + SUBCASE("reduce_props") + { + ToStringOptions opts; + opts.exhaustive = true; + + TypeId ty = reductionof("{ x: string | string, y: number | number }"); + CHECK("{| x: string, y: number |}" == toString(ty, opts)); + } + + SUBCASE("reduce_indexers") + { + ToStringOptions opts; + opts.exhaustive = true; + + TypeId ty = reductionof("{ [string | string]: number | number }"); + CHECK("{| [string]: number |}" == toString(ty, opts)); + } + + SUBCASE("reduce_instantiated_type_parameters") + { + check(R"( + type Foo = { x: T } + local foo: Foo = { x = "hello" } + )"); + + TypeId ty = reductionof(requireType("foo")); + CHECK("Foo" == toString(ty)); + } + + SUBCASE("reduce_instantiated_type_pack_parameters") + { + check(R"( + type Foo = { x: () -> T... } + local foo: Foo = { x = function() return "hi", 5 end } + )"); + + TypeId ty = reductionof(requireType("foo")); + CHECK("Foo" == toString(ty)); + } + + SUBCASE("reduce_tables_within_tables") + { + ToStringOptions opts; + opts.exhaustive = true; + + TypeId ty = reductionof("{ x: { y: string & number } }"); + CHECK("{| x: {| y: never |} |}" == toString(ty, opts)); + } +} + +TEST_CASE_FIXTURE(ReductionFixture, "metatables") +{ + SUBCASE("reduce_table_part") + { + TableType table; + table.props["x"] = {arena.addType(UnionType{{builtinTypes->stringType, builtinTypes->stringType}})}; + TypeId tableTy = arena.addType(std::move(table)); + + TypeId ty = reductionof(arena.addType(MetatableType{tableTy, arena.addType(TableType{})})); + CHECK("{ @metatable { }, { x: string } }" == toString(ty)); + } + + SUBCASE("reduce_metatable_part") + { + TableType table; + table.props["x"] = {arena.addType(UnionType{{builtinTypes->stringType, builtinTypes->stringType}})}; + TypeId tableTy = arena.addType(std::move(table)); + + TypeId ty = reductionof(arena.addType(MetatableType{arena.addType(TableType{}), tableTy})); + CHECK("{ @metatable { x: string }, { } }" == toString(ty)); + } +} + +TEST_CASE_FIXTURE(ReductionFixture, "functions") +{ + SUBCASE("reduce_parameters") + { + TypeId ty = reductionof("(string | string) -> ()"); + CHECK("(string) -> ()" == toString(ty)); + } + + SUBCASE("reduce_returns") + { + TypeId ty = reductionof("() -> (string | string)"); + CHECK("() -> string" == toString(ty)); + } + + SUBCASE("reduce_parameters_and_returns") + { + TypeId ty = reductionof("(string | string) -> (number | number)"); + CHECK("(string) -> number" == toString(ty)); + } + + SUBCASE("reduce_tail") + { + TypeId ty = reductionof("() -> ...(string | string)"); + CHECK("() -> (...string)" == toString(ty)); + } + + SUBCASE("reduce_head_and_tail") + { + TypeId ty = reductionof("() -> (string | string, number | number, ...(boolean | boolean))"); + CHECK("() -> (string, number, ...boolean)" == toString(ty)); + } + + SUBCASE("reduce_overloaded_functions") + { + TypeId ty = reductionof("((number | number) -> ()) & ((string | string) -> ())"); + CHECK("((number) -> ()) & ((string) -> ())" == toString(ty)); + } +} // functions + +TEST_CASE_FIXTURE(ReductionFixture, "negations") +{ + SUBCASE("not_unknown") + { + TypeId ty = reductionof("Not"); + CHECK("never" == toString(ty)); + } + + SUBCASE("not_never") + { + TypeId ty = reductionof("Not"); + CHECK("unknown" == toString(ty)); + } + + SUBCASE("not_any") + { + TypeId ty = reductionof("Not"); + CHECK("any" == toString(ty)); + } + + SUBCASE("not_not_reduction") + { + TypeId ty = reductionof("Not>"); + CHECK("never" == toString(ty)); + } + + SUBCASE("not_string") + { + TypeId ty = reductionof("Not"); + CHECK("~string" == toString(ty)); + } + + SUBCASE("not_string_or_number") + { + TypeId ty = reductionof("Not"); + CHECK("~number & ~string" == toString(ty)); + } + + SUBCASE("not_string_and_number") + { + TypeId ty = reductionof("Not"); + CHECK("unknown" == toString(ty)); + } + + SUBCASE("not_error") + { + TypeId ty = reductionof("Not"); + CHECK("~*error-type*" == toString(ty)); + } +} // negations + +TEST_CASE_FIXTURE(ReductionFixture, "discriminable_unions") +{ + SUBCASE("cat_or_dog_and_dog") + { + TypeId ty = reductionof(R"(({ tag: "cat", catfood: string } | { tag: "dog", dogfood: string }) & { tag: "dog" })"); + CHECK(R"({| dogfood: string, tag: "dog" |})" == toString(ty)); + } + + SUBCASE("cat_or_dog_and_not_dog") + { + TypeId ty = reductionof(R"(({ tag: "cat", catfood: string } | { tag: "dog", dogfood: string }) & { tag: Not<"dog"> })"); + CHECK(R"({| catfood: string, tag: "cat" |})" == toString(ty)); + } + + SUBCASE("string_or_number_and_number") + { + TypeId ty = reductionof("({ tag: string, a: number } | { tag: number, b: string }) & { tag: string }"); + CHECK("{| a: number, tag: string |}" == toString(ty)); + } + + SUBCASE("string_or_number_and_number") + { + TypeId ty = reductionof("({ tag: string, a: number } | { tag: number, b: string }) & { tag: number }"); + CHECK("{| b: string, tag: number |}" == toString(ty)); + } + + SUBCASE("child_or_unrelated_and_parent") + { + TypeId ty = reductionof("({ tag: Child, x: number } | { tag: Unrelated, y: string }) & { tag: Parent }"); + CHECK("{| tag: Child, x: number |}" == toString(ty)); + } + + SUBCASE("child_or_unrelated_and_not_parent") + { + TypeId ty = reductionof("({ tag: Child, x: number } | { tag: Unrelated, y: string }) & { tag: Not }"); + CHECK("{| tag: Unrelated, y: string |}" == toString(ty)); + } +} + +TEST_CASE_FIXTURE(ReductionFixture, "cycles") +{ + SUBCASE("recursively_defined_function") + { + check("type F = (f: F) -> ()"); + + TypeId ty = reductionof(requireTypeAlias("F")); + CHECK("(t1) -> () where t1 = (t1) -> ()" == toString(ty)); + } + + SUBCASE("recursively_defined_function_and_function") + { + check("type F = (f: F & fun) -> ()"); + + TypeId ty = reductionof(requireTypeAlias("F")); + CHECK("(t1) -> () where t1 = (function & t1) -> ()" == toString(ty)); + } + + SUBCASE("recursively_defined_table") + { + ToStringOptions opts; + opts.exhaustive = true; + + check("type T = { x: T }"); + + TypeId ty = reductionof(requireTypeAlias("T")); + CHECK("{| x: t1 |} where t1 = {| x: t1 |}" == toString(ty, opts)); + } + + SUBCASE("recursively_defined_table_and_table") + { + ToStringOptions opts; + opts.exhaustive = true; + + check("type T = { x: T & {} }"); + + TypeId ty = reductionof(requireTypeAlias("T")); + CHECK("{| x: t1 & {| |} |} where t1 = {| x: t1 & {| |} |}" == toString(ty, opts)); + } + + SUBCASE("recursively_defined_table_and_table_2") + { + ToStringOptions opts; + opts.exhaustive = true; + + check("type T = { x: T } & { x: number }"); + + TypeId ty = reductionof(requireTypeAlias("T")); + CHECK("never" == toString(ty)); + } + + SUBCASE("recursively_defined_table_and_table_3") + { + ToStringOptions opts; + opts.exhaustive = true; + + check("type T = { x: T } & { x: T }"); + + TypeId ty = reductionof(requireTypeAlias("T")); + CHECK("{| x: {| x: t1 |} & {| x: t1 |} & {| x: t2 & t2 & {| x: t1 |} & {| x: t1 |} |} |} where t1 = t2 & {| x: t1 |} ; t2 = {| x: t1 |}" == + toString(ty)); + } +} + +TEST_CASE_FIXTURE(ReductionFixture, "stress_test_recursion_limits") +{ + TypeId ty = arena.addType(IntersectionType{{builtinTypes->numberType, builtinTypes->stringType}}); + for (size_t i = 0; i < 20'000; ++i) + { + TableType table; + table.state = TableState::Sealed; + table.props["x"] = {ty}; + ty = arena.addType(IntersectionType{{arena.addType(table), arena.addType(table)}}); + } + + CHECK(!reduction.reduce(ty)); +} + +TEST_SUITE_END(); diff --git a/tests/TypeVar.test.cpp b/tests/TypeVar.test.cpp index ec0a2473..36e437e2 100644 --- a/tests/TypeVar.test.cpp +++ b/tests/TypeVar.test.cpp @@ -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" diff --git a/tools/faillist.txt b/tools/faillist.txt index 233c75c1..f336bb22 100644 --- a/tools/faillist.txt +++ b/tools/faillist.txt @@ -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