From 68cdcc4a3a5f3ed23186c4f7f6b8a5aacf835bee Mon Sep 17 00:00:00 2001 From: vegorov-rbx <75688451+vegorov-rbx@users.noreply.github.com> Date: Fri, 6 Jun 2025 11:52:47 -0700 Subject: [PATCH] Sync to upstream/release/677 (#1872) # What's Changed? This week comes with many improvements to the new type solver and an important fix to the garbage collection to make it more robust in memory constrained scenarios. # Runtime - Garbage collection will no longer run out of memory itself, which could have happened when resizing arrays to a smaller size # New Type Solver - Type refinements on external types should now work and should no longer normalize the type into `never` - Improved error reporting when `string.format` is used with a dynamic format string - Updated type signature of `getmetatable` library function to use the corresponding type function and produce better type inference - Restored a type mismatch error when converting function types with different number of generic parameters, like `() -> ()` into `() -> ()` - Types resulting from compound assignments have been simplified, reducing cyclic type introduction and inference failures - Fixed function generic types leaking into tables during bidirectional type inference (Fixes #1808 and #1821 ) - Stability and performance improvements (Fixes #1860 ) # Internal Contributors Co-authored-by: Andy Friesen Co-authored-by: Ariel Weiss Co-authored-by: Hunter Goldstein Co-authored-by: Sora Kanosue Co-authored-by: Varun Saini Co-authored-by: Vighnesh Vijay Co-authored-by: Vyacheslav Egorov --- Analysis/include/Luau/Differ.h | 208 --- Analysis/include/Luau/Error.h | 31 +- Analysis/include/Luau/FileResolver.h | 1 - Analysis/include/Luau/TypeIds.h | 6 + Analysis/include/Luau/TypeUtils.h | 3 + Analysis/src/BuiltinDefinitions.cpp | 343 +++-- Analysis/src/Constraint.cpp | 10 +- Analysis/src/ConstraintGenerator.cpp | 88 +- Analysis/src/ConstraintSolver.cpp | 396 +++-- Analysis/src/DataFlowGraph.cpp | 14 +- Analysis/src/Differ.cpp | 967 ------------- Analysis/src/Error.cpp | 44 +- Analysis/src/Frontend.cpp | 4 +- Analysis/src/Generalization.cpp | 229 ++- Analysis/src/InferPolarity.cpp | 4 +- Analysis/src/IostreamHelpers.cpp | 15 + Analysis/src/Normalize.cpp | 60 +- Analysis/src/Simplify.cpp | 47 +- Analysis/src/Subtyping.cpp | 48 +- Analysis/src/TableLiteralInference.cpp | 4 - Analysis/src/ToString.cpp | 51 +- Analysis/src/Type.cpp | 23 +- Analysis/src/TypeChecker2.cpp | 54 +- Analysis/src/TypeFunction.cpp | 160 +-- Analysis/src/TypeIds.cpp | 7 + Analysis/src/TypeUtils.cpp | 15 +- Analysis/src/Unifier2.cpp | 8 +- Common/include/Luau/ExperimentalFlags.h | 9 +- Sources.cmake | 5 - VM/src/lgc.cpp | 84 +- VM/src/lstrlib.cpp | 16 +- tests/Autocomplete.test.cpp | 4 +- tests/Conformance.test.cpp | 45 +- tests/DiffAsserts.cpp | 31 - tests/DiffAsserts.h | 46 - tests/Differ.test.cpp | 1759 ----------------------- tests/Fixture.h | 81 +- tests/FragmentAutocomplete.test.cpp | 2 - tests/Generalization.test.cpp | 99 +- tests/InferPolarity.test.cpp | 4 +- tests/Linter.test.cpp | 5 +- tests/Normalize.test.cpp | 36 +- tests/Subtyping.test.cpp | 7 +- tests/TypeFunction.test.cpp | 13 +- tests/TypeFunction.user.test.cpp | 62 +- tests/TypeInfer.builtins.test.cpp | 60 +- tests/TypeInfer.definitions.test.cpp | 4 +- tests/TypeInfer.functions.test.cpp | 26 +- tests/TypeInfer.generics.test.cpp | 123 +- tests/TypeInfer.loops.test.cpp | 12 +- tests/TypeInfer.modules.test.cpp | 14 +- tests/TypeInfer.operators.test.cpp | 10 +- tests/TypeInfer.primitives.test.cpp | 7 +- tests/TypeInfer.refinements.test.cpp | 93 +- tests/TypeInfer.tables.test.cpp | 37 +- tests/TypeInfer.test.cpp | 200 ++- tests/TypeInfer.typePacks.test.cpp | 7 +- tests/TypeInfer.typestates.test.cpp | 11 +- tests/TypeInfer.unionTypes.test.cpp | 4 +- tests/TypeInfer.unknownnever.test.cpp | 5 +- tests/conformance/gc.luau | 43 + 61 files changed, 1801 insertions(+), 4003 deletions(-) delete mode 100644 Analysis/include/Luau/Differ.h delete mode 100644 Analysis/src/Differ.cpp delete mode 100644 tests/DiffAsserts.cpp delete mode 100644 tests/DiffAsserts.h delete mode 100644 tests/Differ.test.cpp diff --git a/Analysis/include/Luau/Differ.h b/Analysis/include/Luau/Differ.h deleted file mode 100644 index d9b78939..00000000 --- a/Analysis/include/Luau/Differ.h +++ /dev/null @@ -1,208 +0,0 @@ -// This file is part of the Luau programming language and is licensed under MIT License; see LICENSE.txt for details -#pragma once - -#include "Luau/DenseHash.h" -#include "Luau/TypeFwd.h" -#include "Luau/UnifierSharedState.h" - -#include -#include -#include - -namespace Luau -{ -struct DiffPathNode -{ - // TODO: consider using Variants to simplify toString implementation - enum Kind - { - TableProperty, - FunctionArgument, - FunctionReturn, - Union, - Intersection, - Negation, - }; - Kind kind; - // non-null when TableProperty - std::optional tableProperty; - // non-null when FunctionArgument (unless variadic arg), FunctionReturn (unless variadic arg), Union, or Intersection (i.e. anonymous fields) - std::optional index; - - /** - * Do not use for leaf nodes - */ - DiffPathNode(Kind kind) - : kind(kind) - { - } - - DiffPathNode(Kind kind, std::optional tableProperty, std::optional index) - : kind(kind) - , tableProperty(tableProperty) - , index(index) - { - } - - std::string toString() const; - - static DiffPathNode constructWithTableProperty(Name tableProperty); - - static DiffPathNode constructWithKindAndIndex(Kind kind, size_t index); - - static DiffPathNode constructWithKind(Kind kind); -}; - -struct DiffPathNodeLeaf -{ - std::optional ty; - std::optional tableProperty; - std::optional minLength; - bool isVariadic; - // TODO: Rename to anonymousIndex, for both union and Intersection - std::optional unionIndex; - DiffPathNodeLeaf( - std::optional ty, - std::optional tableProperty, - std::optional minLength, - bool isVariadic, - std::optional unionIndex - ) - : ty(ty) - , tableProperty(tableProperty) - , minLength(minLength) - , isVariadic(isVariadic) - , unionIndex(unionIndex) - { - } - - static DiffPathNodeLeaf detailsNormal(TypeId ty); - - static DiffPathNodeLeaf detailsTableProperty(TypeId ty, Name tableProperty); - - static DiffPathNodeLeaf detailsUnionIndex(TypeId ty, size_t index); - - static DiffPathNodeLeaf detailsLength(int minLength, bool isVariadic); - - static DiffPathNodeLeaf nullopts(); -}; - -struct DiffPath -{ - std::vector path; - - std::string toString(bool prependDot) const; -}; -struct DiffError -{ - enum Kind - { - Normal, - MissingTableProperty, - MissingUnionMember, - MissingIntersectionMember, - IncompatibleGeneric, - LengthMismatchInFnArgs, - LengthMismatchInFnRets, - }; - Kind kind; - - DiffPath diffPath; - DiffPathNodeLeaf left; - DiffPathNodeLeaf right; - - std::string leftRootName; - std::string rightRootName; - - DiffError(Kind kind, DiffPathNodeLeaf left, DiffPathNodeLeaf right, std::string leftRootName, std::string rightRootName) - : kind(kind) - , left(left) - , right(right) - , leftRootName(leftRootName) - , rightRootName(rightRootName) - { - checkValidInitialization(left, right); - } - DiffError(Kind kind, DiffPath diffPath, DiffPathNodeLeaf left, DiffPathNodeLeaf right, std::string leftRootName, std::string rightRootName) - : kind(kind) - , diffPath(diffPath) - , left(left) - , right(right) - , leftRootName(leftRootName) - , rightRootName(rightRootName) - { - checkValidInitialization(left, right); - } - - std::string toString(bool multiLine = false) const; - -private: - std::string toStringALeaf(std::string rootName, const DiffPathNodeLeaf& leaf, const DiffPathNodeLeaf& otherLeaf, bool multiLine) const; - void checkValidInitialization(const DiffPathNodeLeaf& left, const DiffPathNodeLeaf& right); - void checkNonMissingPropertyLeavesHaveNulloptTableProperty() const; -}; - -struct DifferResult -{ - std::optional diffError; - - DifferResult() {} - DifferResult(DiffError diffError) - : diffError(diffError) - { - } - - void wrapDiffPath(DiffPathNode node); -}; -struct DifferEnvironment -{ - TypeId rootLeft; - TypeId rootRight; - std::optional externalSymbolLeft; - std::optional externalSymbolRight; - DenseHashMap genericMatchedPairs; - DenseHashMap genericTpMatchedPairs; - - DifferEnvironment( - TypeId rootLeft, - TypeId rootRight, - std::optional externalSymbolLeft, - std::optional externalSymbolRight - ) - : rootLeft(rootLeft) - , rootRight(rootRight) - , externalSymbolLeft(externalSymbolLeft) - , externalSymbolRight(externalSymbolRight) - , genericMatchedPairs(nullptr) - , genericTpMatchedPairs(nullptr) - { - } - - bool isProvenEqual(TypeId left, TypeId right) const; - bool isAssumedEqual(TypeId left, TypeId right) const; - void recordProvenEqual(TypeId left, TypeId right); - void pushVisiting(TypeId left, TypeId right); - void popVisiting(); - std::vector>::const_reverse_iterator visitingBegin() const; - std::vector>::const_reverse_iterator visitingEnd() const; - std::string getDevFixFriendlyNameLeft() const; - std::string getDevFixFriendlyNameRight() const; - -private: - // TODO: consider using DenseHashSet - std::unordered_set, TypeIdPairHash> provenEqual; - // Ancestors of current types - std::unordered_set, TypeIdPairHash> visiting; - std::vector> visitingStack; -}; -DifferResult diff(TypeId ty1, TypeId ty2); -DifferResult diffWithSymbols(TypeId ty1, TypeId ty2, std::optional symbol1, std::optional symbol2); - -/** - * True if ty is a "simple" type, i.e. cannot contain types. - * string, number, boolean are simple types. - * function and table are not simple types. - */ -bool isSimple(TypeId ty); - -} // namespace Luau diff --git a/Analysis/include/Luau/Error.h b/Analysis/include/Luau/Error.h index 994f1140..2ad82fb9 100644 --- a/Analysis/include/Luau/Error.h +++ b/Analysis/include/Luau/Error.h @@ -470,6 +470,32 @@ struct UnexpectedArrayLikeTableItem } }; +struct CannotCheckDynamicStringFormatCalls +{ + bool operator==(const CannotCheckDynamicStringFormatCalls&) const + { + return true; + } +}; + +// Error during subtyping when the number of generic types between compared types does not match +struct GenericTypeCountMismatch +{ + size_t subTyGenericCount; + size_t superTyGenericCount; + + bool operator==(const GenericTypeCountMismatch& rhs) const; +}; + +// Error during subtyping when the number of generic type packs between compared types does not match +struct GenericTypePackCountMismatch +{ + size_t subTyGenericPackCount; + size_t superTyGenericPackCount; + + bool operator==(const GenericTypePackCountMismatch& rhs) const; +}; + using TypeErrorData = Variant< TypeMismatch, UnknownSymbol, @@ -521,7 +547,10 @@ using TypeErrorData = Variant< ExplicitFunctionAnnotationRecommended, UserDefinedTypeFunctionError, ReservedIdentifier, - UnexpectedArrayLikeTableItem>; + UnexpectedArrayLikeTableItem, + CannotCheckDynamicStringFormatCalls, + GenericTypeCountMismatch, + GenericTypePackCountMismatch>; struct TypeErrorSummary { diff --git a/Analysis/include/Luau/FileResolver.h b/Analysis/include/Luau/FileResolver.h index bcc2e0e0..613ebe24 100644 --- a/Analysis/include/Luau/FileResolver.h +++ b/Analysis/include/Luau/FileResolver.h @@ -20,7 +20,6 @@ struct SourceCode None, Module, Script, - Local_DEPRECATED }; std::string source; diff --git a/Analysis/include/Luau/TypeIds.h b/Analysis/include/Luau/TypeIds.h index e74ae4d6..ca40f00e 100644 --- a/Analysis/include/Luau/TypeIds.h +++ b/Analysis/include/Luau/TypeIds.h @@ -62,6 +62,12 @@ public: bool operator==(const TypeIds& there) const; size_t getHash() const; bool isNever() const; + + /** + * Moves the contents of this container into a `std::vector` and returns it. + * This container will be empty after `take` is called. + */ + std::vector take(); }; } // namespace Luau diff --git a/Analysis/include/Luau/TypeUtils.h b/Analysis/include/Luau/TypeUtils.h index 8479ae90..f4549f0d 100644 --- a/Analysis/include/Luau/TypeUtils.h +++ b/Analysis/include/Luau/TypeUtils.h @@ -318,4 +318,7 @@ std::optional extractMatchingTableType(std::vector& tables, Type */ bool isRecord(const AstExprTable::Item& item); +// Unwraps any grouping expressions iteratively. +AstExpr* unwrapGroup(AstExpr* expr); + } // namespace Luau diff --git a/Analysis/src/BuiltinDefinitions.cpp b/Analysis/src/BuiltinDefinitions.cpp index 1a9f4f51..adf727e4 100644 --- a/Analysis/src/BuiltinDefinitions.cpp +++ b/Analysis/src/BuiltinDefinitions.cpp @@ -21,6 +21,7 @@ #include "Luau/TypeUtils.h" #include +#include /** FIXME: Many of these type definitions are not quite completely accurate. * @@ -29,11 +30,12 @@ */ LUAU_FASTFLAG(LuauSolverV2) -LUAU_FASTFLAG(LuauEagerGeneralization2) +LUAU_FASTFLAG(LuauEagerGeneralization3) LUAU_FASTFLAGVARIABLE(LuauTableCloneClonesType3) +LUAU_FASTFLAGVARIABLE(LuauStringFormatImprovements) LUAU_FASTFLAGVARIABLE(LuauMagicFreezeCheckBlocked2) -LUAU_FASTFLAGVARIABLE(LuauFormatUseLastPosition) LUAU_FASTFLAGVARIABLE(LuauUpdateSetMetatableTypeSignature) +LUAU_FASTFLAGVARIABLE(LuauUpdateGetMetatableTypeSignature) namespace Luau { @@ -311,7 +313,7 @@ void registerBuiltinGlobals(Frontend& frontend, GlobalTypes& globals, bool typeC TypeArena& arena = globals.globalTypes; NotNull builtinTypes = globals.builtinTypes; Scope* globalScope = nullptr; // NotNull when removing FFlag::LuauEagerGeneralization2 - if (FFlag::LuauEagerGeneralization2) + if (FFlag::LuauEagerGeneralization3) globalScope = globals.globalScope.get(); if (FFlag::LuauSolverV2) @@ -378,12 +380,22 @@ void registerBuiltinGlobals(Frontend& frontend, GlobalTypes& globals, bool typeC TypeId tableMetaMT = arena.addType(MetatableType{tabTy, genericMT}); - // getmetatable : ({ @metatable MT, {+ +} }) -> MT - addGlobalBinding(globals, "getmetatable", makeFunction(arena, std::nullopt, {genericMT}, {}, {tableMetaMT}, {genericMT}), "@luau"); + TypeId genericT = arena.addType(GenericType{globalScope, "T"}); + + if (FFlag::LuauSolverV2 && FFlag::LuauUpdateGetMetatableTypeSignature) + { + // getmetatable : (T) -> getmetatable + TypeId getmtReturn = arena.addType(TypeFunctionInstanceType{builtinTypeFunctions().getmetatableFunc, {genericT}}); + addGlobalBinding(globals, "getmetatable", makeFunction(arena, std::nullopt, {genericT}, {}, {genericT}, {getmtReturn}), "@luau"); + } + else + { + // getmetatable : ({ @metatable MT, {+ +} }) -> MT + addGlobalBinding(globals, "getmetatable", makeFunction(arena, std::nullopt, {genericMT}, {}, {tableMetaMT}, {genericMT}), "@luau"); + } if (FFlag::LuauSolverV2) { - TypeId genericT = arena.addType(GenericType{globalScope, "T"}); TypeId tMetaMT = arena.addType(MetatableType{genericT, genericMT}); if (FFlag::LuauUpdateSetMetatableTypeSignature) @@ -633,110 +645,251 @@ bool MagicFormat::infer(const MagicFunctionCallContext& context) { TypeArena* arena = context.solver->arena; - AstExprConstantString* fmt = nullptr; - if (auto index = context.callSite->func->as(); index && context.callSite->self) + if (FFlag::LuauStringFormatImprovements) { - if (auto group = index->expr->as()) - fmt = group->expr->as(); - else - fmt = index->expr->as(); + auto iter = begin(context.arguments); + + // we'll suppress any errors for `string.format` if the format string is error suppressing. + if (iter == end(context.arguments) || shouldSuppressErrors(context.solver->normalizer, follow(*iter)) == ErrorSuppression::Suppress) + { + TypePackId resultPack = arena->addTypePack({context.solver->builtinTypes->stringType}); + asMutable(context.result)->ty.emplace(resultPack); + return true; + } + + AstExprConstantString* fmt = nullptr; + if (auto index = context.callSite->func->as(); index && context.callSite->self) + fmt = unwrapGroup(index->expr)->as(); + + if (!context.callSite->self && context.callSite->args.size > 0) + fmt = context.callSite->args.data[0]->as(); + + std::optional formatString; + if (fmt) + formatString = {fmt->value.data, fmt->value.size}; + else if (auto singleton = get(follow(*iter))) + { + if (auto stringSingleton = get(singleton)) + formatString = {stringSingleton->value}; + } + + if (!formatString) + return false; + + std::vector expected = parseFormatString(context.solver->builtinTypes, formatString->data(), formatString->size()); + const auto& [params, tail] = flatten(context.arguments); + + size_t paramOffset = 1; + + // unify the prefix one argument at a time - needed if any of the involved types are free + for (size_t i = 0; i < expected.size() && i + paramOffset < params.size(); ++i) + { + context.solver->unify(context.constraint, params[i + paramOffset], expected[i]); + } + + // if we know the argument count or if we have too many arguments for sure, we can issue an error + size_t numActualParams = params.size(); + size_t numExpectedParams = expected.size() + 1; // + 1 for the format string + + if (numExpectedParams != numActualParams && (!tail || numExpectedParams < numActualParams)) + context.solver->reportError(TypeError{context.callSite->location, CountMismatch{numExpectedParams, std::nullopt, numActualParams}}); + + // This is invoked at solve time, so we just need to provide a type for the result of :/.format + TypePackId resultPack = arena->addTypePack({context.solver->builtinTypes->stringType}); + asMutable(context.result)->ty.emplace(resultPack); + + return true; } - - if (!context.callSite->self && context.callSite->args.size > 0) - fmt = context.callSite->args.data[0]->as(); - - if (!fmt) - return false; - - std::vector expected = parseFormatString(context.solver->builtinTypes, fmt->value.data, fmt->value.size); - const auto& [params, tail] = flatten(context.arguments); - - size_t paramOffset = 1; - - // unify the prefix one argument at a time - needed if any of the involved types are free - for (size_t i = 0; i < expected.size() && i + paramOffset < params.size(); ++i) + else { - context.solver->unify(context.constraint, params[i + paramOffset], expected[i]); + AstExprConstantString* fmt = nullptr; + if (auto index = context.callSite->func->as(); index && context.callSite->self) + { + if (auto group = index->expr->as()) + fmt = group->expr->as(); + else + fmt = index->expr->as(); + } + + if (!context.callSite->self && context.callSite->args.size > 0) + fmt = context.callSite->args.data[0]->as(); + + if (!fmt) + return false; + + std::vector expected = parseFormatString(context.solver->builtinTypes, fmt->value.data, fmt->value.size); + const auto& [params, tail] = flatten(context.arguments); + + size_t paramOffset = 1; + + // unify the prefix one argument at a time - needed if any of the involved types are free + for (size_t i = 0; i < expected.size() && i + paramOffset < params.size(); ++i) + { + context.solver->unify(context.constraint, params[i + paramOffset], expected[i]); + } + + // if we know the argument count or if we have too many arguments for sure, we can issue an error + size_t numActualParams = params.size(); + size_t numExpectedParams = expected.size() + 1; // + 1 for the format string + + if (numExpectedParams != numActualParams && (!tail || numExpectedParams < numActualParams)) + context.solver->reportError(TypeError{context.callSite->location, CountMismatch{numExpectedParams, std::nullopt, numActualParams}}); + + // This is invoked at solve time, so we just need to provide a type for the result of :/.format + TypePackId resultPack = arena->addTypePack({context.solver->builtinTypes->stringType}); + asMutable(context.result)->ty.emplace(resultPack); + + return true; } - - // if we know the argument count or if we have too many arguments for sure, we can issue an error - size_t numActualParams = params.size(); - size_t numExpectedParams = expected.size() + 1; // + 1 for the format string - - if (numExpectedParams != numActualParams && (!tail || numExpectedParams < numActualParams)) - context.solver->reportError(TypeError{context.callSite->location, CountMismatch{numExpectedParams, std::nullopt, numActualParams}}); - - // This is invoked at solve time, so we just need to provide a type for the result of :/.format - TypePackId resultPack = arena->addTypePack({context.solver->builtinTypes->stringType}); - asMutable(context.result)->ty.emplace(resultPack); - - return true; } bool MagicFormat::typeCheck(const MagicFunctionTypeCheckContext& context) { - AstExprConstantString* fmt = nullptr; - if (auto index = context.callSite->func->as(); index && context.callSite->self) + if (FFlag::LuauStringFormatImprovements) { - if (auto group = index->expr->as()) - fmt = group->expr->as(); - else - fmt = index->expr->as(); - } + auto iter = begin(context.arguments); - if (!context.callSite->self && context.callSite->args.size > 0) - fmt = context.callSite->args.data[0]->as(); - - if (!fmt) - { - context.typechecker->reportError(CountMismatch{1, std::nullopt, 0, CountMismatch::Arg, true, "string.format"}, context.callSite->location); - return true; - } - - // CLI-150726: The block below effectively constructs a type pack and then type checks it by going parameter-by-parameter. - // This does _not_ handle cases like: - // - // local foo : () -> (...string) = (nil :: any) - // print(string.format("%s %d %s", foo())) - // - // ... which should be disallowed. - - std::vector expected = parseFormatString(context.builtinTypes, fmt->value.data, fmt->value.size); - const auto& [params, tail] = flatten(context.arguments); - - size_t paramOffset = 1; - // Compare the expressions passed with the types the function expects to determine whether this function was called with : or . - bool calledWithSelf = expected.size() == context.callSite->args.size; - // unify the prefix one argument at a time - for (size_t i = 0; i < expected.size() && i + paramOffset < params.size(); ++i) - { - TypeId actualTy = params[i + paramOffset]; - TypeId expectedTy = expected[i]; - Location location = - FFlag::LuauFormatUseLastPosition - ? context.callSite->args.data[std::min(context.callSite->args.size - 1, i + (calledWithSelf ? 0 : paramOffset))]->location - : context.callSite->args.data[i + (calledWithSelf ? 0 : paramOffset)]->location; - // use subtyping instead here - SubtypingResult result = context.typechecker->subtyping->isSubtype(actualTy, expectedTy, context.checkScope); - - if (!result.isSubtype) + if (iter == end(context.arguments)) { - switch (shouldSuppressErrors(NotNull{&context.typechecker->normalizer}, actualTy)) - { - case ErrorSuppression::Suppress: - break; - case ErrorSuppression::NormalizationFailed: - break; - case ErrorSuppression::DoNotSuppress: - Reasonings reasonings = context.typechecker->explainReasonings(actualTy, expectedTy, location, result); + context.typechecker->reportError(CountMismatch{1, std::nullopt, 0, CountMismatch::Arg, true, "string.format"}, context.callSite->location); + return true; + } - if (!reasonings.suppressed) - context.typechecker->reportError(TypeMismatch{expectedTy, actualTy, reasonings.toString()}, location); + // we'll suppress any errors for `string.format` if the format string is error suppressing. + if (shouldSuppressErrors(NotNull{&context.typechecker->normalizer}, follow(*iter)) == ErrorSuppression::Suppress) + { + return true; + } + + AstExprConstantString* fmt = nullptr; + if (auto index = context.callSite->func->as(); index && context.callSite->self) + fmt = unwrapGroup(index->expr)->as(); + + if (!context.callSite->self && context.callSite->args.size > 0) + fmt = context.callSite->args.data[0]->as(); + + std::optional formatString; + if (fmt) + formatString = {fmt->value.data, fmt->value.size}; + else if (auto singleton = get(follow(*iter))) + { + if (auto stringSingleton = get(singleton)) + formatString = {stringSingleton->value}; + } + + if (!formatString) + { + context.typechecker->reportError(CannotCheckDynamicStringFormatCalls{}, context.callSite->location); + return true; + } + + // CLI-150726: The block below effectively constructs a type pack and then type checks it by going parameter-by-parameter. + // This does _not_ handle cases like: + // + // local foo : () -> (...string) = (nil :: any) + // print(string.format("%s %d %s", foo())) + // + // ... which should be disallowed. + + std::vector expected = parseFormatString(context.builtinTypes, formatString->data(), formatString->size()); + + const auto& [params, tail] = flatten(context.arguments); + + size_t paramOffset = 1; + // Compare the expressions passed with the types the function expects to determine whether this function was called with : or . + bool calledWithSelf = expected.size() == context.callSite->args.size; + // unify the prefix one argument at a time + for (size_t i = 0; i < expected.size() && i + paramOffset < params.size(); ++i) + { + TypeId actualTy = params[i + paramOffset]; + TypeId expectedTy = expected[i]; + Location location = context.callSite->args.data[std::min(context.callSite->args.size - 1, i + (calledWithSelf ? 0 : paramOffset))]->location; + // use subtyping instead here + SubtypingResult result = context.typechecker->subtyping->isSubtype(actualTy, expectedTy, context.checkScope); + + if (!result.isSubtype) + { + switch (shouldSuppressErrors(NotNull{&context.typechecker->normalizer}, actualTy)) + { + case ErrorSuppression::Suppress: + break; + case ErrorSuppression::NormalizationFailed: + break; + case ErrorSuppression::DoNotSuppress: + Reasonings reasonings = context.typechecker->explainReasonings(actualTy, expectedTy, location, result); + + if (!reasonings.suppressed) + context.typechecker->reportError(TypeMismatch{expectedTy, actualTy, reasonings.toString()}, location); + } } } - } - return true; + return true; + + } + else + { + AstExprConstantString* fmt = nullptr; + if (auto index = context.callSite->func->as(); index && context.callSite->self) + { + if (auto group = index->expr->as()) + fmt = group->expr->as(); + else + fmt = index->expr->as(); + } + + if (!context.callSite->self && context.callSite->args.size > 0) + fmt = context.callSite->args.data[0]->as(); + + if (!fmt) + { + context.typechecker->reportError(CountMismatch{1, std::nullopt, 0, CountMismatch::Arg, true, "string.format"}, context.callSite->location); + return true; + } + + // CLI-150726: The block below effectively constructs a type pack and then type checks it by going parameter-by-parameter. + // This does _not_ handle cases like: + // + // local foo : () -> (...string) = (nil :: any) + // print(string.format("%s %d %s", foo())) + // + // ... which should be disallowed. + + std::vector expected = parseFormatString(context.builtinTypes, fmt->value.data, fmt->value.size); + + const auto& [params, tail] = flatten(context.arguments); + + size_t paramOffset = 1; + // Compare the expressions passed with the types the function expects to determine whether this function was called with : or . + bool calledWithSelf = expected.size() == context.callSite->args.size; + // unify the prefix one argument at a time + for (size_t i = 0; i < expected.size() && i + paramOffset < params.size(); ++i) + { + TypeId actualTy = params[i + paramOffset]; + TypeId expectedTy = expected[i]; + Location location = context.callSite->args.data[std::min(context.callSite->args.size - 1, i + (calledWithSelf ? 0 : paramOffset))]->location; + // use subtyping instead here + SubtypingResult result = context.typechecker->subtyping->isSubtype(actualTy, expectedTy, context.checkScope); + + if (!result.isSubtype) + { + switch (shouldSuppressErrors(NotNull{&context.typechecker->normalizer}, actualTy)) + { + case ErrorSuppression::Suppress: + break; + case ErrorSuppression::NormalizationFailed: + break; + case ErrorSuppression::DoNotSuppress: + Reasonings reasonings = context.typechecker->explainReasonings(actualTy, expectedTy, location, result); + + if (!reasonings.suppressed) + context.typechecker->reportError(TypeMismatch{expectedTy, actualTy, reasonings.toString()}, location); + } + } + } + + return true; + } } static std::vector parsePatternString(NotNull builtinTypes, const char* data, size_t size) diff --git a/Analysis/src/Constraint.cpp b/Analysis/src/Constraint.cpp index 253a5002..5d02967e 100644 --- a/Analysis/src/Constraint.cpp +++ b/Analysis/src/Constraint.cpp @@ -3,7 +3,7 @@ #include "Luau/Constraint.h" #include "Luau/VisitType.h" -LUAU_FASTFLAG(LuauEagerGeneralization2) +LUAU_FASTFLAG(LuauEagerGeneralization3) namespace Luau { @@ -51,7 +51,7 @@ struct ReferenceCountInitializer : TypeOnceVisitor bool visit(TypeId, const TypeFunctionInstanceType&) override { - return FFlag::LuauEagerGeneralization2 && traverseIntoTypeFunctions; + return FFlag::LuauEagerGeneralization3 && traverseIntoTypeFunctions; } }; @@ -104,7 +104,7 @@ DenseHashSet Constraint::getMaybeMutatedFreeTypes() const { rci.traverse(fchc->argsPack); } - else if (auto fcc = get(*this); fcc && FFlag::LuauEagerGeneralization2) + else if (auto fcc = get(*this); fcc && FFlag::LuauEagerGeneralization3) { rci.traverseIntoTypeFunctions = false; rci.traverse(fcc->fn); @@ -118,12 +118,12 @@ DenseHashSet Constraint::getMaybeMutatedFreeTypes() const else if (auto hpc = get(*this)) { rci.traverse(hpc->resultType); - if (FFlag::LuauEagerGeneralization2) + if (FFlag::LuauEagerGeneralization3) rci.traverse(hpc->subjectType); } else if (auto hic = get(*this)) { - if (FFlag::LuauEagerGeneralization2) + if (FFlag::LuauEagerGeneralization3) rci.traverse(hic->subjectType); rci.traverse(hic->resultType); // `HasIndexerConstraint` should not mutate `indexType`. diff --git a/Analysis/src/ConstraintGenerator.cpp b/Analysis/src/ConstraintGenerator.cpp index ad287883..25ef8816 100644 --- a/Analysis/src/ConstraintGenerator.cpp +++ b/Analysis/src/ConstraintGenerator.cpp @@ -34,22 +34,21 @@ LUAU_FASTINT(LuauCheckRecursionLimit) LUAU_FASTFLAG(DebugLuauLogSolverToJson) LUAU_FASTFLAG(DebugLuauMagicTypes) -LUAU_FASTFLAG(LuauEagerGeneralization2) -LUAU_FASTFLAG(LuauEagerGeneralization2) -LUAU_FASTFLAGVARIABLE(LuauWeakNilRefinementType) +LUAU_FASTFLAG(LuauEagerGeneralization3) +LUAU_FASTFLAG(LuauEagerGeneralization3) LUAU_FASTFLAG(LuauStoreReturnTypesAsPackOnAst) LUAU_FASTFLAG(LuauGlobalVariableModuleIsolation) -LUAU_FASTFLAG(LuauNoMoreInjectiveTypeFunctions) LUAU_FASTFLAGVARIABLE(LuauEnableWriteOnlyProperties) LUAU_FASTFLAG(LuauAddCallConstraintForIterableFunctions) LUAU_FASTFLAG(LuauDoNotAddUpvalueTypesToLocalType) LUAU_FASTFLAGVARIABLE(LuauAvoidDoubleNegation) -LUAU_FASTFLAGVARIABLE(LuauSimplifyOutOfLine) +LUAU_FASTFLAGVARIABLE(LuauSimplifyOutOfLine2) LUAU_FASTFLAG(LuauTableLiteralSubtypeSpecificCheck) LUAU_FASTFLAG(LuauDfgAllowUpdatesInLoops) LUAU_FASTFLAGVARIABLE(LuauDisablePrimitiveInferenceInLargeTables) LUAU_FASTINTVARIABLE(LuauPrimitiveInferenceInTableLimit, 500) LUAU_FASTFLAGVARIABLE(LuauUserTypeFunctionAliases) +LUAU_FASTFLAGVARIABLE(LuauSkipLvalueForCompoundAssignment) namespace Luau { @@ -257,7 +256,7 @@ void ConstraintGenerator::visitModuleRoot(AstStatBlock* block) rootScope->location = block->location; module->astScopes[block] = NotNull{scope.get()}; - if (FFlag::LuauEagerGeneralization2) + if (FFlag::LuauEagerGeneralization3) interiorFreeTypes.emplace_back(); else DEPRECATED_interiorTypes.emplace_back(); @@ -293,7 +292,7 @@ void ConstraintGenerator::visitModuleRoot(AstStatBlock* block) } ); - if (FFlag::LuauEagerGeneralization2) + if (FFlag::LuauEagerGeneralization3) { scope->interiorFreeTypes = std::move(interiorFreeTypes.back().types); scope->interiorFreeTypePacks = std::move(interiorFreeTypes.back().typePacks); @@ -312,7 +311,7 @@ void ConstraintGenerator::visitModuleRoot(AstStatBlock* block) } ); - if (FFlag::LuauEagerGeneralization2) + if (FFlag::LuauEagerGeneralization3) interiorFreeTypes.pop_back(); else DEPRECATED_interiorTypes.pop_back(); @@ -338,7 +337,7 @@ void ConstraintGenerator::visitModuleRoot(AstStatBlock* block) asMutable(ty)->ty.emplace(domainTy); } - if (FFlag::LuauSimplifyOutOfLine) + if (FFlag::LuauSimplifyOutOfLine2) { for (TypeId ty : unionsToSimplify) addConstraint(scope, block->location, SimplifyConstraint{ty}); @@ -350,13 +349,13 @@ void ConstraintGenerator::visitFragmentRoot(const ScopePtr& resumeScope, AstStat // We prepopulate global data in the resumeScope to avoid writing data into the old modules scopes prepopulateGlobalScopeForFragmentTypecheck(globalScope, resumeScope, block); // Pre - if (FFlag::LuauEagerGeneralization2) + if (FFlag::LuauEagerGeneralization3) interiorFreeTypes.emplace_back(); else DEPRECATED_interiorTypes.emplace_back(); visitBlockWithoutChildScope(resumeScope, block); // Post - if (FFlag::LuauEagerGeneralization2) + if (FFlag::LuauEagerGeneralization3) interiorFreeTypes.pop_back(); else DEPRECATED_interiorTypes.pop_back(); @@ -386,12 +385,12 @@ void ConstraintGenerator::visitFragmentRoot(const ScopePtr& resumeScope, AstStat TypeId ConstraintGenerator::freshType(const ScopePtr& scope, Polarity polarity) { - if (FFlag::LuauEagerGeneralization2) + if (FFlag::LuauEagerGeneralization3) { auto ft = Luau::freshType(arena, builtinTypes, scope.get(), polarity); interiorFreeTypes.back().types.push_back(ft); - if (FFlag::LuauEagerGeneralization2) + if (FFlag::LuauEagerGeneralization3) freeTypes.insert(ft); return ft; @@ -408,7 +407,7 @@ TypePackId ConstraintGenerator::freshTypePack(const ScopePtr& scope, Polarity po { FreeTypePack f{scope.get(), polarity}; TypePackId result = arena->addTypePack(TypePackVar{std::move(f)}); - if (FFlag::LuauEagerGeneralization2) + if (FFlag::LuauEagerGeneralization3) interiorFreeTypes.back().typePacks.push_back(result); return result; } @@ -719,8 +718,6 @@ void ConstraintGenerator::applyRefinements(const ScopePtr& scope, Location locat if (std::optional defTy = lookup(scope, location, def)) { TypeId ty = *defTy; - if (!FFlag::LuauWeakNilRefinementType && partition.shouldAppendNilType) - ty = arena->addType(UnionType{{ty, builtinTypes->nilType}}); // Intersect ty with every discriminant type. If either type is not // sufficiently solved, we queue the intersection up via an // IntersectConstraint. @@ -767,7 +764,7 @@ void ConstraintGenerator::applyRefinements(const ScopePtr& scope, Location locat if (kind != RefinementsOpKind::None) ty = flushConstraints(kind, ty, discriminants); - if (FFlag::LuauWeakNilRefinementType && partition.shouldAppendNilType) + if (partition.shouldAppendNilType) ty = createTypeFunctionInstance(builtinTypeFunctions().weakoptionalFunc, {ty}, {}, scope, location); scope->rvalueRefinements[def] = ty; @@ -1420,7 +1417,7 @@ ControlFlow ConstraintGenerator::visit(const ScopePtr& scope, AstStatLocalFuncti FunctionSignature sig = checkFunctionSignature(scope, function->func, /* expectedType */ std::nullopt, function->name->location); sig.bodyScope->bindings[function->name] = Binding{sig.signature, function->name->location}; - bool sigFullyDefined = FFlag::LuauEagerGeneralization2 ? false : !hasFreeType(sig.signature); + bool sigFullyDefined = FFlag::LuauEagerGeneralization3 ? false : !hasFreeType(sig.signature); if (sigFullyDefined) emplaceType(asMutable(functionType), sig.signature); @@ -1480,7 +1477,7 @@ ControlFlow ConstraintGenerator::visit(const ScopePtr& scope, AstStatFunction* f Checkpoint start = checkpoint(this); FunctionSignature sig = checkFunctionSignature(scope, function->func, /* expectedType */ std::nullopt, function->name->location); - bool sigFullyDefined = FFlag::LuauEagerGeneralization2 ? false : !hasFreeType(sig.signature); + bool sigFullyDefined = FFlag::LuauEagerGeneralization3 ? false : !hasFreeType(sig.signature); DefId def = dfg->getDef(function->name); @@ -1669,11 +1666,14 @@ ControlFlow ConstraintGenerator::visit(const ScopePtr& scope, AstStatCompoundAss TypeId resultTy = checkAstExprBinary(scope, assign->location, assign->op, assign->var, assign->value, std::nullopt).ty; module->astCompoundAssignResultTypes[assign] = resultTy; - TypeId lhsType = check(scope, assign->var).ty; - visitLValue(scope, assign->var, lhsType); + if (!FFlag::LuauSkipLvalueForCompoundAssignment) + { + TypeId lhsType = check(scope, assign->var).ty; + visitLValue(scope, assign->var, lhsType); - follow(lhsType); - follow(resultTy); + follow(lhsType); + follow(resultTy); + } return ControlFlow::None; } @@ -1794,7 +1794,7 @@ ControlFlow ConstraintGenerator::visit(const ScopePtr& scope, AstStatTypeFunctio // Place this function as a child of the non-type function scope scope->children.push_back(NotNull{sig.signatureScope.get()}); - if (FFlag::LuauEagerGeneralization2) + if (FFlag::LuauEagerGeneralization3) interiorFreeTypes.emplace_back(); else DEPRECATED_interiorTypes.push_back(std::vector{}); @@ -1812,7 +1812,7 @@ ControlFlow ConstraintGenerator::visit(const ScopePtr& scope, AstStatTypeFunctio } ); - if (FFlag::LuauEagerGeneralization2) + if (FFlag::LuauEagerGeneralization3) { sig.signatureScope->interiorFreeTypes = std::move(interiorFreeTypes.back().types); sig.signatureScope->interiorFreeTypePacks = std::move(interiorFreeTypes.back().typePacks); @@ -1821,7 +1821,7 @@ ControlFlow ConstraintGenerator::visit(const ScopePtr& scope, AstStatTypeFunctio sig.signatureScope->interiorFreeTypes = std::move(DEPRECATED_interiorTypes.back()); getMutable(generalizedTy)->setOwner(gc); - if (FFlag::LuauEagerGeneralization2) + if (FFlag::LuauEagerGeneralization3) interiorFreeTypes.pop_back(); else DEPRECATED_interiorTypes.pop_back(); @@ -2491,7 +2491,7 @@ Inference ConstraintGenerator::check(const ScopePtr& scope, AstExprConstantStrin return Inference{builtinTypes->stringType}; TypeId freeTy = nullptr; - if (FFlag::LuauEagerGeneralization2) + if (FFlag::LuauEagerGeneralization3) { freeTy = freshType(scope, Polarity::Positive); FreeType* ft = getMutable(freeTy); @@ -2532,7 +2532,7 @@ Inference ConstraintGenerator::check(const ScopePtr& scope, AstExprConstantBool* return Inference{builtinTypes->booleanType}; TypeId freeTy = nullptr; - if (FFlag::LuauEagerGeneralization2) + if (FFlag::LuauEagerGeneralization3) { freeTy = freshType(scope, Polarity::Positive); FreeType* ft = getMutable(freeTy); @@ -2691,7 +2691,7 @@ Inference ConstraintGenerator::check(const ScopePtr& scope, AstExprFunction* fun Checkpoint startCheckpoint = checkpoint(this); FunctionSignature sig = checkFunctionSignature(scope, func, expectedType); - if (FFlag::LuauEagerGeneralization2) + if (FFlag::LuauEagerGeneralization3) interiorFreeTypes.emplace_back(); else DEPRECATED_interiorTypes.push_back(std::vector{}); @@ -2709,7 +2709,7 @@ Inference ConstraintGenerator::check(const ScopePtr& scope, AstExprFunction* fun } ); - if (FFlag::LuauEagerGeneralization2) + if (FFlag::LuauEagerGeneralization3) { sig.signatureScope->interiorFreeTypes = std::move(interiorFreeTypes.back().types); sig.signatureScope->interiorFreeTypePacks = std::move(interiorFreeTypes.back().typePacks); @@ -2849,16 +2849,14 @@ Inference ConstraintGenerator::checkAstExprBinary( } case AstExprBinary::Op::CompareLt: { - if (FFlag::LuauNoMoreInjectiveTypeFunctions) - addConstraint(scope, location, EqualityConstraint{leftType, rightType}); + addConstraint(scope, location, EqualityConstraint{leftType, rightType}); TypeId resultType = createTypeFunctionInstance(builtinTypeFunctions().ltFunc, {leftType, rightType}, {}, scope, location); return Inference{resultType, std::move(refinement)}; } case AstExprBinary::Op::CompareGe: { - if (FFlag::LuauNoMoreInjectiveTypeFunctions) - addConstraint(scope, location, EqualityConstraint{leftType, rightType}); + addConstraint(scope, location, EqualityConstraint{leftType, rightType}); TypeId resultType = createTypeFunctionInstance( builtinTypeFunctions().ltFunc, @@ -2871,16 +2869,14 @@ Inference ConstraintGenerator::checkAstExprBinary( } case AstExprBinary::Op::CompareLe: { - if (FFlag::LuauNoMoreInjectiveTypeFunctions) - addConstraint(scope, location, EqualityConstraint{leftType, rightType}); + addConstraint(scope, location, EqualityConstraint{leftType, rightType}); TypeId resultType = createTypeFunctionInstance(builtinTypeFunctions().leFunc, {leftType, rightType}, {}, scope, location); return Inference{resultType, std::move(refinement)}; } case AstExprBinary::Op::CompareGt: { - if (FFlag::LuauNoMoreInjectiveTypeFunctions) - addConstraint(scope, location, EqualityConstraint{leftType, rightType}); + addConstraint(scope, location, EqualityConstraint{leftType, rightType}); TypeId resultType = createTypeFunctionInstance( builtinTypeFunctions().leFunc, @@ -3217,7 +3213,7 @@ Inference ConstraintGenerator::check(const ScopePtr& scope, AstExprTable* expr, expr->items.size > size_t(FInt::LuauPrimitiveInferenceInTableLimit)) largeTableDepth++; - if (FFlag::LuauEagerGeneralization2) + if (FFlag::LuauEagerGeneralization3) interiorFreeTypes.back().types.push_back(ty); else DEPRECATED_interiorTypes.back().push_back(ty); @@ -3270,7 +3266,7 @@ Inference ConstraintGenerator::check(const ScopePtr& scope, AstExprTable* expr, { LUAU_ASSERT(!indexValueLowerBound.empty()); - if (FFlag::LuauSimplifyOutOfLine) + if (FFlag::LuauSimplifyOutOfLine2) { TypeId indexKey = nullptr; TypeId indexValue = nullptr; @@ -3478,7 +3474,7 @@ ConstraintGenerator::FunctionSignature ConstraintGenerator::checkFunctionSignatu LUAU_ASSERT(nullptr != varargPack); - if (FFlag::LuauEagerGeneralization2) + if (FFlag::LuauEagerGeneralization3) { // Some of the types in argTypes will eventually be generics, and some // will not. The ones that are not generic will be pruned when @@ -3543,7 +3539,7 @@ ConstraintGenerator::FunctionSignature ConstraintGenerator::checkFunctionSignatu if (expectedType && get(*expectedType)) bindFreeType(*expectedType, actualFunctionType); - if (FFlag::LuauEagerGeneralization2) + if (FFlag::LuauEagerGeneralization3) scopeToFunction[signatureScope.get()] = actualFunctionType; return { @@ -4064,7 +4060,7 @@ TypeId ConstraintGenerator::makeUnion(const ScopePtr& scope, Location location, if (get(follow(rhs))) return lhs; - if (FFlag::LuauSimplifyOutOfLine) + if (FFlag::LuauSimplifyOutOfLine2) { TypeId result = simplifyUnion(scope, location, lhs, rhs); if (is(follow(result))) @@ -4080,7 +4076,7 @@ TypeId ConstraintGenerator::makeUnion(const ScopePtr& scope, Location location, TypeId ConstraintGenerator::makeUnion(std::vector options) { - LUAU_ASSERT(FFlag::LuauSimplifyOutOfLine); + LUAU_ASSERT(FFlag::LuauSimplifyOutOfLine2); TypeId result = arena->addType(UnionType{std::move(options)}); unionsToSimplify.push_back(result); return result; @@ -4304,7 +4300,7 @@ void ConstraintGenerator::fillInInferredBindings(const ScopePtr& globalScope, As scope->bindings[symbol] = Binding{tys.front(), location}; else { - if (FFlag::LuauSimplifyOutOfLine) + if (FFlag::LuauSimplifyOutOfLine2) { TypeId ty = makeUnion(std::move(tys)); scope->bindings[symbol] = Binding{ty, location}; @@ -4352,7 +4348,7 @@ std::vector> ConstraintGenerator::getExpectedCallTypesForF el = result[0]; else { - if (FFlag::LuauSimplifyOutOfLine) + if (FFlag::LuauSimplifyOutOfLine2) el = makeUnion(std::move(result)); else el = module->internalTypes.addType(UnionType{std::move(result)}); diff --git a/Analysis/src/ConstraintSolver.cpp b/Analysis/src/ConstraintSolver.cpp index d62b688f..0aba6a22 100644 --- a/Analysis/src/ConstraintSolver.cpp +++ b/Analysis/src/ConstraintSolver.cpp @@ -32,17 +32,15 @@ LUAU_FASTFLAGVARIABLE(DebugLuauLogSolverIncludeDependencies) LUAU_FASTFLAGVARIABLE(DebugLuauLogBindings) LUAU_FASTINTVARIABLE(LuauSolverRecursionLimit, 500) LUAU_FASTFLAGVARIABLE(DebugLuauEqSatSimplification) -LUAU_FASTFLAGVARIABLE(LuauHasPropProperBlock) -LUAU_FASTFLAG(LuauEagerGeneralization2) +LUAU_FASTFLAG(LuauEagerGeneralization3) LUAU_FASTFLAG(LuauDeprecatedAttribute) -LUAU_FASTFLAGVARIABLE(LuauTrackInferredFunctionTypeFromCall) LUAU_FASTFLAGVARIABLE(LuauAddCallConstraintForIterableFunctions) LUAU_FASTFLAGVARIABLE(LuauGuardAgainstMalformedTypeAliasExpansion2) LUAU_FASTFLAGVARIABLE(LuauInsertErrorTypesIntoIndexerResult) LUAU_FASTFLAGVARIABLE(LuauClipVariadicAnysFromArgsToGenericFuncs2) -LUAU_FASTFLAG(LuauSubtypeGenericsAndNegations) -LUAU_FASTFLAG(LuauNoMoreInjectiveTypeFunctions) LUAU_FASTFLAG(LuauTableLiteralSubtypeSpecificCheck) +LUAU_FASTFLAGVARIABLE(LuauAvoidGenericsLeakingDuringFunctionCallCheck) +LUAU_FASTFLAGVARIABLE(LuauMissingFollowInAssignIndexConstraint) namespace Luau { @@ -418,7 +416,7 @@ void ConstraintSolver::run() } // Free types that have no constraints at all can be generalized right away. - if (FFlag::LuauEagerGeneralization2) + if (FFlag::LuauEagerGeneralization3) { for (TypeId ty : constraintSet.freeTypes) { @@ -479,7 +477,7 @@ void ConstraintSolver::run() // expansion types, etc, so we need to follow it. ty = follow(ty); - if (FFlag::LuauEagerGeneralization2) + if (FFlag::LuauEagerGeneralization3) { if (seen.contains(ty)) continue; @@ -498,7 +496,7 @@ void ConstraintSolver::run() if (refCount <= 1) unblock(ty, Location{}); - if (FFlag::LuauEagerGeneralization2 && refCount == 0) + if (FFlag::LuauEagerGeneralization3 && refCount == 0) generalizeOneType(ty); } } @@ -676,7 +674,7 @@ void ConstraintSolver::initFreeTypeTracking() auto [refCount, _] = unresolvedConstraints.try_insert(ty, 0); refCount += 1; - if (FFlag::LuauEagerGeneralization2) + if (FFlag::LuauEagerGeneralization3) { auto [it, fresh] = mutatedFreeTypeToConstraint.try_emplace(ty, nullptr); it->second.insert(c.get()); @@ -689,13 +687,6 @@ void ConstraintSolver::initFreeTypeTracking() block(dep, c); } } - - // Also check flag integrity while we're here - if (FFlag::LuauEagerGeneralization2) - { - LUAU_ASSERT(FFlag::LuauSubtypeGenericsAndNegations); - LUAU_ASSERT(FFlag::LuauNoMoreInjectiveTypeFunctions); - } } void ConstraintSolver::generalizeOneType(TypeId ty) @@ -739,7 +730,7 @@ void ConstraintSolver::bind(NotNull constraint, TypeId ty, Typ constraint, ty, constraint->scope, builtinTypes->neverType, builtinTypes->unknownType, Polarity::Mixed ); // FIXME? Is this the right polarity? - if (FFlag::LuauEagerGeneralization2) + if (FFlag::LuauEagerGeneralization3) trackInteriorFreeType(constraint->scope, ty); return; @@ -900,7 +891,7 @@ bool ConstraintSolver::tryDispatch(const GeneralizationConstraint& c, NotNullscope->interiorFreeTypes) // NOLINT(bugprone-unchecked-optional-access) { - if (FFlag::LuauEagerGeneralization2) + if (FFlag::LuauEagerGeneralization3) { ty = follow(ty); if (auto freeTy = get(ty)) @@ -909,7 +900,6 @@ bool ConstraintSolver::tryDispatch(const GeneralizationConstraint& c, NotNullpolarity; - GeneralizationResult res = generalizeType(arena, builtinTypes, constraint->scope, ty, params); if (res.resourceLimitsExceeded) reportError(CodeTooComplex{}, constraint->scope->location); // FIXME: We don't have a very good location for this. @@ -922,7 +912,7 @@ bool ConstraintSolver::tryDispatch(const GeneralizationConstraint& c, NotNullscope->interiorFreeTypePacks) { @@ -942,7 +932,7 @@ bool ConstraintSolver::tryDispatch(const GeneralizationConstraint& c, NotNullscope, freeTy); @@ -1612,23 +1602,14 @@ bool ConstraintSolver::tryDispatch(const FunctionCallConstraint& c, NotNullscope, inferredTy); + trackInteriorFreeType(constraint->scope, inferredTy); unblock(c.result, constraint->location); return true; } -static AstExpr* unwrapGroup(AstExpr* expr) -{ - while (auto group = expr->as()) - expr = group->expr; - - return expr; -} - -struct ContainsGenerics : public TypeOnceVisitor +struct ContainsGenerics_DEPRECATED : public TypeOnceVisitor { DenseHashSet generics{nullptr}; @@ -1665,6 +1646,93 @@ struct ContainsGenerics : public TypeOnceVisitor } }; +namespace +{ +struct ReferentialReplacer : Substitution +{ + NotNull> replacements; + NotNull> replacementPacks; + + ReferentialReplacer( + NotNull arena, + NotNull> replacements, + NotNull> replacementPacks + ) + : Substitution(TxnLog::empty(), arena) + , replacements(std::move(replacements)) + , replacementPacks(std::move(replacementPacks)) + { + } + + bool isDirty(TypeId ty) override + { + return replacements->find(ty) != nullptr; + } + + bool isDirty(TypePackId tp) override + { + return replacementPacks->find(tp) != nullptr; + } + + TypeId clean(TypeId ty) override + { + TypeId res = (*replacements)[ty]; + LUAU_ASSERT(res); + dontTraverseInto(res); + return res; + } + + TypePackId clean(TypePackId tp) override + { + TypePackId res = (*replacementPacks)[tp]; + LUAU_ASSERT(res); + dontTraverseInto(res); + return res; + } +}; +struct ContainsGenerics : public TypeOnceVisitor +{ + NotNull> generics; + + explicit ContainsGenerics(NotNull> generics) + : generics{generics} + { + } + + bool found = false; + + bool visit(TypeId ty) override + { + return !found; + } + + bool visit(TypeId ty, const GenericType&) override + { + found |= generics->contains(ty); + return true; + } + + bool visit(TypeId ty, const TypeFunctionInstanceType&) override + { + return !found; + } + + bool visit(TypePackId tp, const GenericTypePack&) override + { + found |= generics->contains(tp); + return !found; + } + + static bool hasGeneric(TypeId ty, NotNull> generics) + { + ContainsGenerics cg{generics}; + cg.traverse(ty); + return cg.found; + } +}; + +} // namespace + bool ConstraintSolver::tryDispatch(const FunctionCheckConstraint& c, NotNull constraint) { TypeId fn = follow(c.fn); @@ -1707,92 +1775,175 @@ bool ConstraintSolver::tryDispatch(const FunctionCheckConstraint& c, NotNull replacements{nullptr}; DenseHashMap replacementPacks{nullptr}; - ContainsGenerics containsGenerics; - - for (auto generic : ftv->generics) + if (FFlag::LuauAvoidGenericsLeakingDuringFunctionCallCheck) { - replacements[generic] = builtinTypes->unknownType; - containsGenerics.generics.insert(generic); - } - for (auto genericPack : ftv->genericPacks) - { - replacementPacks[genericPack] = builtinTypes->unknownTypePack; - containsGenerics.generics.insert(genericPack); - } + DenseHashSet genericTypesAndPacks{nullptr}; - const std::vector expectedArgs = flatten(ftv->argTypes).first; - const std::vector argPackHead = flatten(argsPack).first; + Unifier2 u2{arena, builtinTypes, constraint->scope, NotNull{&iceReporter}}; - // If this is a self call, the types will have more elements than the AST call. - // We don't attempt to perform bidirectional inference on the self type. - const size_t typeOffset = c.callSite->self ? 1 : 0; - - for (size_t i = 0; i < c.callSite->args.size && i + typeOffset < expectedArgs.size() && i + typeOffset < argPackHead.size(); ++i) - { - const TypeId expectedArgTy = follow(expectedArgs[i + typeOffset]); - const TypeId actualArgTy = follow(argPackHead[i + typeOffset]); - AstExpr* expr = unwrapGroup(c.callSite->args.data[i]); - - (*c.astExpectedTypes)[expr] = expectedArgTy; - - const FunctionType* lambdaTy = get(actualArgTy); - // Generic types are skipped over entirely, for now. - if (containsGenerics.hasGeneric(expectedArgTy)) + for (auto generic : ftv->generics) { - if (!FFlag::LuauClipVariadicAnysFromArgsToGenericFuncs2 || !lambdaTy || !lambdaTy->argTypes) - continue; - - const TypePack* argTp = get(follow(lambdaTy->argTypes)); - if (!argTp || !argTp->tail) - continue; - - if (const VariadicTypePack* argTpTail = get(follow(argTp->tail)); - argTpTail && argTpTail->hidden && argTpTail->ty == builtinTypes->anyType) + // We may see non-generic types here, for example when evaluating a + // recursive function call. + if (auto gty = get(follow(generic))) { - // Strip variadic any - const TypePackId anyLessArgTp = arena->addTypePack(TypePack{argTp->head}); - const TypeId newFuncTypeId = arena->addType(FunctionType{anyLessArgTp, lambdaTy->retTypes}); - FunctionType* newFunc = getMutable(newFuncTypeId); - newFunc->argNames = lambdaTy->argNames; - (*c.astTypes)[expr] = newFuncTypeId; + replacements[generic] = gty->polarity == Polarity::Negative ? builtinTypes->neverType : builtinTypes->unknownType; + genericTypesAndPacks.insert(generic); } - - continue; } - const FunctionType* expectedLambdaTy = get(expectedArgTy); - const AstExprFunction* lambdaExpr = expr->as(); - - if (expectedLambdaTy && lambdaTy && lambdaExpr) + for (auto genericPack : ftv->genericPacks) { - const std::vector expectedLambdaArgTys = flatten(expectedLambdaTy->argTypes).first; - const std::vector lambdaArgTys = flatten(lambdaTy->argTypes).first; + replacementPacks[genericPack] = builtinTypes->unknownTypePack; + genericTypesAndPacks.insert(genericPack); + } - for (size_t j = 0; j < expectedLambdaArgTys.size() && j < lambdaArgTys.size() && j < lambdaExpr->args.size; ++j) + const std::vector expectedArgs = flatten(ftv->argTypes).first; + const std::vector argPackHead = flatten(argsPack).first; + + // If this is a self call, the types will have more elements than the AST call. + // We don't attempt to perform bidirectional inference on the self type. + const size_t typeOffset = c.callSite->self ? 1 : 0; + + for (size_t i = 0; i < c.callSite->args.size && i + typeOffset < expectedArgs.size() && i + typeOffset < argPackHead.size(); ++i) + { + const TypeId expectedArgTy = follow(expectedArgs[i + typeOffset]); + const TypeId actualArgTy = follow(argPackHead[i + typeOffset]); + AstExpr* expr = unwrapGroup(c.callSite->args.data[i]); + + (*c.astExpectedTypes)[expr] = expectedArgTy; + + const auto lambdaTy = get(actualArgTy); + const auto expectedLambdaTy = get(expectedArgTy); + const auto lambdaExpr = expr->as(); + + if (expectedLambdaTy && lambdaTy && lambdaExpr) { - if (!lambdaExpr->args.data[j]->annotation && get(follow(lambdaArgTys[j]))) + if (ContainsGenerics::hasGeneric(expectedArgTy, NotNull{&genericTypesAndPacks})) + continue; + + const std::vector expectedLambdaArgTys = flatten(expectedLambdaTy->argTypes).first; + const std::vector lambdaArgTys = flatten(lambdaTy->argTypes).first; + + for (size_t j = 0; j < expectedLambdaArgTys.size() && j < lambdaArgTys.size() && j < lambdaExpr->args.size; ++j) { - shiftReferences(lambdaArgTys[j], expectedLambdaArgTys[j]); - bind(constraint, lambdaArgTys[j], expectedLambdaArgTys[j]); + if (!lambdaExpr->args.data[j]->annotation && get(follow(lambdaArgTys[j]))) + { + shiftReferences(lambdaArgTys[j], expectedLambdaArgTys[j]); + bind(constraint, lambdaArgTys[j], expectedLambdaArgTys[j]); + } } } + else if (expr->is() || expr->is() || expr->is() || + expr->is()) + { + ReferentialReplacer replacer{arena, NotNull{&replacements}, NotNull{&replacementPacks}}; + if (auto res = replacer.substitute(expectedArgTy)) + u2.unify(actualArgTy, *res); + else + u2.unify(actualArgTy, expectedArgTy); + } + else if (expr->is() && !ContainsGenerics::hasGeneric(expectedArgTy, NotNull{&genericTypesAndPacks})) + { + Subtyping sp{builtinTypes, arena, simplifier, normalizer, typeFunctionRuntime, NotNull{&iceReporter}}; + std::vector toBlock; + (void)matchLiteralType( + c.astTypes, c.astExpectedTypes, builtinTypes, arena, NotNull{&u2}, NotNull{&sp}, expectedArgTy, actualArgTy, expr, toBlock + ); + LUAU_ASSERT(toBlock.empty()); + } } - else if (expr->is() || expr->is() || expr->is() || - expr->is()) + } + else + { + ContainsGenerics_DEPRECATED containsGenerics; + + for (auto generic : ftv->generics) { - Unifier2 u2{arena, builtinTypes, constraint->scope, NotNull{&iceReporter}}; - u2.unify(actualArgTy, expectedArgTy); + replacements[generic] = builtinTypes->unknownType; + containsGenerics.generics.insert(generic); } - else if (expr->is()) + + for (auto genericPack : ftv->genericPacks) { - Unifier2 u2{arena, builtinTypes, constraint->scope, NotNull{&iceReporter}}; - Subtyping sp{builtinTypes, arena, simplifier, normalizer, typeFunctionRuntime, NotNull{&iceReporter}}; - std::vector toBlock; - (void)matchLiteralType( - c.astTypes, c.astExpectedTypes, builtinTypes, arena, NotNull{&u2}, NotNull{&sp}, expectedArgTy, actualArgTy, expr, toBlock - ); - LUAU_ASSERT(toBlock.empty()); + replacementPacks[genericPack] = builtinTypes->unknownTypePack; + containsGenerics.generics.insert(genericPack); + } + + const std::vector expectedArgs = flatten(ftv->argTypes).first; + const std::vector argPackHead = flatten(argsPack).first; + + // If this is a self call, the types will have more elements than the AST call. + // We don't attempt to perform bidirectional inference on the self type. + const size_t typeOffset = c.callSite->self ? 1 : 0; + + for (size_t i = 0; i < c.callSite->args.size && i + typeOffset < expectedArgs.size() && i + typeOffset < argPackHead.size(); ++i) + { + const TypeId expectedArgTy = follow(expectedArgs[i + typeOffset]); + const TypeId actualArgTy = follow(argPackHead[i + typeOffset]); + AstExpr* expr = unwrapGroup(c.callSite->args.data[i]); + + (*c.astExpectedTypes)[expr] = expectedArgTy; + + const FunctionType* lambdaTy = get(actualArgTy); + // Generic types are skipped over entirely, for now. + if (containsGenerics.hasGeneric(expectedArgTy)) + { + if (!FFlag::LuauClipVariadicAnysFromArgsToGenericFuncs2 || !lambdaTy || !lambdaTy->argTypes) + continue; + + const TypePack* argTp = get(follow(lambdaTy->argTypes)); + if (!argTp || !argTp->tail) + continue; + + if (const VariadicTypePack* argTpTail = get(follow(argTp->tail)); + argTpTail && argTpTail->hidden && argTpTail->ty == builtinTypes->anyType) + { + // Strip variadic any + const TypePackId anyLessArgTp = arena->addTypePack(TypePack{argTp->head}); + const TypeId newFuncTypeId = arena->addType(FunctionType{anyLessArgTp, lambdaTy->retTypes}); + FunctionType* newFunc = getMutable(newFuncTypeId); + newFunc->argNames = lambdaTy->argNames; + (*c.astTypes)[expr] = newFuncTypeId; + } + + continue; + } + + const FunctionType* expectedLambdaTy = get(expectedArgTy); + const AstExprFunction* lambdaExpr = expr->as(); + + if (expectedLambdaTy && lambdaTy && lambdaExpr) + { + const std::vector expectedLambdaArgTys = flatten(expectedLambdaTy->argTypes).first; + const std::vector lambdaArgTys = flatten(lambdaTy->argTypes).first; + + for (size_t j = 0; j < expectedLambdaArgTys.size() && j < lambdaArgTys.size() && j < lambdaExpr->args.size; ++j) + { + if (!lambdaExpr->args.data[j]->annotation && get(follow(lambdaArgTys[j]))) + { + shiftReferences(lambdaArgTys[j], expectedLambdaArgTys[j]); + bind(constraint, lambdaArgTys[j], expectedLambdaArgTys[j]); + } + } + } + else if (expr->is() || expr->is() || expr->is() || + expr->is()) + { + Unifier2 u2{arena, builtinTypes, constraint->scope, NotNull{&iceReporter}}; + u2.unify(actualArgTy, expectedArgTy); + } + else if (expr->is()) + { + Unifier2 u2{arena, builtinTypes, constraint->scope, NotNull{&iceReporter}}; + Subtyping sp{builtinTypes, arena, simplifier, normalizer, typeFunctionRuntime, NotNull{&iceReporter}}; + std::vector toBlock; + (void)matchLiteralType( + c.astTypes, c.astExpectedTypes, builtinTypes, arena, NotNull{&u2}, NotNull{&sp}, expectedArgTy, actualArgTy, expr, toBlock + ); + LUAU_ASSERT(toBlock.empty()); + } } } @@ -1864,16 +2015,8 @@ bool ConstraintSolver::tryDispatch(const HasPropConstraint& c, NotNull(resultType)); LUAU_ASSERT(canMutate(resultType, constraint)); - if (FFlag::LuauHasPropProperBlock) - { - if (isBlocked(subjectType)) - return block(subjectType, constraint); - } - else - { - if (isBlocked(subjectType) || get(subjectType) || get(subjectType)) - return block(subjectType, constraint); - } + if (isBlocked(subjectType)) + return block(subjectType, constraint); if (const TableType* subjectTable = getTableType(subjectType)) { @@ -1941,7 +2084,7 @@ bool ConstraintSolver::tryDispatchHasIndexer( TypeId upperBound = arena->addType(TableType{/* props */ {}, TableIndexer{indexType, resultType}, TypeLevel{}, ft->scope, TableState::Unsealed}); - if (FFlag::LuauEagerGeneralization2) + if (FFlag::LuauEagerGeneralization3) { TypeId sr = follow(simplifyIntersection(constraint->scope, constraint->location, ft->upperBound, upperBound)); @@ -1972,7 +2115,7 @@ bool ConstraintSolver::tryDispatchHasIndexer( FreeType freeResult{tt->scope, builtinTypes->neverType, builtinTypes->unknownType, Polarity::Mixed}; emplace(constraint, resultType, freeResult); - if (FFlag::LuauEagerGeneralization2) + if (FFlag::LuauEagerGeneralization3) trackInteriorFreeType(constraint->scope, resultType); tt->indexer = TableIndexer{indexType, resultType}; @@ -2161,7 +2304,7 @@ bool ConstraintSolver::tryDispatch(const AssignPropConstraint& c, NotNullupperBound); - if (FFlag::LuauEagerGeneralization2) + if (FFlag::LuauEagerGeneralization3) { const auto [blocked, maybeTy, isIndex] = lookupTableProp(constraint, lhsType, propName, ValueContext::LValue); if (!blocked.empty()) @@ -2341,10 +2484,21 @@ bool ConstraintSolver::tryDispatch(const AssignIndexConstraint& c, NotNull(lhsType)) { - if (auto lhsTable = getMutable(lhsFree->upperBound)) + if (FFlag::LuauMissingFollowInAssignIndexConstraint) { - if (auto res = tableStuff(lhsTable)) - return *res; + if (auto lhsTable = getMutable(follow(lhsFree->upperBound))) + { + if (auto res = tableStuff(lhsTable)) + return *res; + } + } + else + { + if (auto lhsTable = getMutable(lhsFree->upperBound)) + { + if (auto res = tableStuff(lhsTable)) + return *res; + } } TypeId newUpperBound = @@ -2603,8 +2757,8 @@ bool ConstraintSolver::tryDispatch(const EqualityConstraint& c, NotNull recordedTys{nullptr}; - DenseHashSet blockedTys{nullptr}; + TypeIds recordedTys; + TypeIds blockedTys; FindAllUnionMembers() : TypeOnceVisitor(/* skipBoundTypes */ true) @@ -3062,7 +3216,7 @@ TablePropLookupResult ConstraintSolver::lookupTableProp( { const TypeId upperBound = follow(ft->upperBound); - if (FFlag::LuauEagerGeneralization2) + if (FFlag::LuauEagerGeneralization3) { if (get(upperBound) || get(upperBound)) { @@ -3530,7 +3684,7 @@ void ConstraintSolver::shiftReferences(TypeId source, TypeId target) // Any constraint that might have mutated source may now mutate target - if (FFlag::LuauEagerGeneralization2) + if (FFlag::LuauEagerGeneralization3) { auto it = mutatedFreeTypeToConstraint.find(source); if (it != mutatedFreeTypeToConstraint.end()) diff --git a/Analysis/src/DataFlowGraph.cpp b/Analysis/src/DataFlowGraph.cpp index 0bdfdb19..d12b2e19 100644 --- a/Analysis/src/DataFlowGraph.cpp +++ b/Analysis/src/DataFlowGraph.cpp @@ -13,7 +13,6 @@ LUAU_FASTFLAG(DebugLuauFreezeArena) LUAU_FASTFLAG(LuauSolverV2) -LUAU_FASTFLAGVARIABLE(LuauPreprocessTypestatedArgument) LUAU_FASTFLAGVARIABLE(LuauDfgScopeStackNotNull) LUAU_FASTFLAG(LuauStoreReturnTypesAsPackOnAst) LUAU_FASTFLAGVARIABLE(LuauDoNotAddUpvalueTypesToLocalType) @@ -1069,11 +1068,8 @@ DataFlowResult DataFlowGraphBuilder::visitExpr(AstExprCall* c) { visitExpr(c->func); - if (FFlag::LuauPreprocessTypestatedArgument) - { - for (AstExpr* arg : c->args) - visitExpr(arg); - } + for (AstExpr* arg : c->args) + visitExpr(arg); if (shouldTypestateForFirstArgument(*c) && c->args.size > 1 && isLValue(*c->args.begin())) { @@ -1105,12 +1101,6 @@ DataFlowResult DataFlowGraphBuilder::visitExpr(AstExprCall* c) visitLValue(firstArg, def); } - if (!FFlag::LuauPreprocessTypestatedArgument) - { - for (AstExpr* arg : c->args) - visitExpr(arg); - } - // We treat function calls as "subscripted" as they could potentially // return a subscripted value, consider: // diff --git a/Analysis/src/Differ.cpp b/Analysis/src/Differ.cpp deleted file mode 100644 index dd026f47..00000000 --- a/Analysis/src/Differ.cpp +++ /dev/null @@ -1,967 +0,0 @@ -// This file is part of the Luau programming language and is licensed under MIT License; see LICENSE.txt for details -#include "Luau/Differ.h" -#include "Luau/Common.h" -#include "Luau/Error.h" -#include "Luau/ToString.h" -#include "Luau/Type.h" -#include "Luau/TypePack.h" -#include "Luau/Unifiable.h" -#include -#include -#include -#include - -namespace Luau -{ -std::string DiffPathNode::toString() const -{ - switch (kind) - { - case DiffPathNode::Kind::TableProperty: - { - if (!tableProperty.has_value()) - throw InternalCompilerError{"DiffPathNode has kind TableProperty but tableProperty is nullopt"}; - return *tableProperty; - break; - } - case DiffPathNode::Kind::FunctionArgument: - { - if (!index.has_value()) - return "Arg[Variadic]"; - // Add 1 because Lua is 1-indexed - return "Arg[" + std::to_string(*index + 1) + "]"; - } - case DiffPathNode::Kind::FunctionReturn: - { - if (!index.has_value()) - return "Ret[Variadic]"; - // Add 1 because Lua is 1-indexed - return "Ret[" + std::to_string(*index + 1) + "]"; - } - case DiffPathNode::Kind::Negation: - { - return "Negation"; - } - default: - { - throw InternalCompilerError{"DiffPathNode::toString is not exhaustive"}; - } - } -} - -DiffPathNode DiffPathNode::constructWithTableProperty(Name tableProperty) -{ - return DiffPathNode{DiffPathNode::Kind::TableProperty, tableProperty, std::nullopt}; -} - -DiffPathNode DiffPathNode::constructWithKindAndIndex(Kind kind, size_t index) -{ - return DiffPathNode{kind, std::nullopt, index}; -} - -DiffPathNode DiffPathNode::constructWithKind(Kind kind) -{ - return DiffPathNode{kind, std::nullopt, std::nullopt}; -} - -DiffPathNodeLeaf DiffPathNodeLeaf::detailsNormal(TypeId ty) -{ - return DiffPathNodeLeaf{ty, std::nullopt, std::nullopt, false, std::nullopt}; -} - -DiffPathNodeLeaf DiffPathNodeLeaf::detailsTableProperty(TypeId ty, Name tableProperty) -{ - return DiffPathNodeLeaf{ty, tableProperty, std::nullopt, false, std::nullopt}; -} - -DiffPathNodeLeaf DiffPathNodeLeaf::detailsUnionIndex(TypeId ty, size_t index) -{ - return DiffPathNodeLeaf{ty, std::nullopt, std::nullopt, false, index}; -} - -DiffPathNodeLeaf DiffPathNodeLeaf::detailsLength(int minLength, bool isVariadic) -{ - return DiffPathNodeLeaf{std::nullopt, std::nullopt, minLength, isVariadic, std::nullopt}; -} - -DiffPathNodeLeaf DiffPathNodeLeaf::nullopts() -{ - return DiffPathNodeLeaf{std::nullopt, std::nullopt, std::nullopt, false, std::nullopt}; -} - -std::string DiffPath::toString(bool prependDot) const -{ - std::string pathStr; - bool isFirstInForLoop = !prependDot; - for (auto node = path.rbegin(); node != path.rend(); node++) - { - if (isFirstInForLoop) - { - isFirstInForLoop = false; - } - else - { - pathStr += "."; - } - pathStr += node->toString(); - } - return pathStr; -} -std::string DiffError::toStringALeaf(std::string rootName, const DiffPathNodeLeaf& leaf, const DiffPathNodeLeaf& otherLeaf, bool multiLine) const -{ - std::string conditionalNewline = multiLine ? "\n" : " "; - std::string conditionalIndent = multiLine ? " " : ""; - std::string pathStr{rootName + diffPath.toString(true)}; - switch (kind) - { - case DiffError::Kind::Normal: - { - checkNonMissingPropertyLeavesHaveNulloptTableProperty(); - return pathStr + conditionalNewline + "has type" + conditionalNewline + conditionalIndent + Luau::toString(*leaf.ty); - } - case DiffError::Kind::MissingTableProperty: - { - if (leaf.ty.has_value()) - { - if (!leaf.tableProperty.has_value()) - throw InternalCompilerError{"leaf.tableProperty is nullopt"}; - return pathStr + "." + *leaf.tableProperty + conditionalNewline + "has type" + conditionalNewline + conditionalIndent + - Luau::toString(*leaf.ty); - } - else if (otherLeaf.ty.has_value()) - { - if (!otherLeaf.tableProperty.has_value()) - throw InternalCompilerError{"otherLeaf.tableProperty is nullopt"}; - return pathStr + conditionalNewline + "is missing the property" + conditionalNewline + conditionalIndent + *otherLeaf.tableProperty; - } - throw InternalCompilerError{"Both leaf.ty and otherLeaf.ty is nullopt"}; - } - case DiffError::Kind::MissingUnionMember: - { - // TODO: do normal case - if (leaf.ty.has_value()) - { - if (!leaf.unionIndex.has_value()) - throw InternalCompilerError{"leaf.unionIndex is nullopt"}; - return pathStr + conditionalNewline + "is a union containing type" + conditionalNewline + conditionalIndent + Luau::toString(*leaf.ty); - } - else if (otherLeaf.ty.has_value()) - { - return pathStr + conditionalNewline + "is a union missing type" + conditionalNewline + conditionalIndent + Luau::toString(*otherLeaf.ty); - } - throw InternalCompilerError{"Both leaf.ty and otherLeaf.ty is nullopt"}; - } - case DiffError::Kind::MissingIntersectionMember: - { - // TODO: better message for intersections - // An intersection of just functions is always an "overloaded function" - // An intersection of just tables is always a "joined table" - if (leaf.ty.has_value()) - { - if (!leaf.unionIndex.has_value()) - throw InternalCompilerError{"leaf.unionIndex is nullopt"}; - return pathStr + conditionalNewline + "is an intersection containing type" + conditionalNewline + conditionalIndent + - Luau::toString(*leaf.ty); - } - else if (otherLeaf.ty.has_value()) - { - return pathStr + conditionalNewline + "is an intersection missing type" + conditionalNewline + conditionalIndent + - Luau::toString(*otherLeaf.ty); - } - throw InternalCompilerError{"Both leaf.ty and otherLeaf.ty is nullopt"}; - } - case DiffError::Kind::LengthMismatchInFnArgs: - { - if (!leaf.minLength.has_value()) - throw InternalCompilerError{"leaf.minLength is nullopt"}; - return pathStr + conditionalNewline + "takes " + std::to_string(*leaf.minLength) + (leaf.isVariadic ? " or more" : "") + " arguments"; - } - case DiffError::Kind::LengthMismatchInFnRets: - { - if (!leaf.minLength.has_value()) - throw InternalCompilerError{"leaf.minLength is nullopt"}; - return pathStr + conditionalNewline + "returns " + std::to_string(*leaf.minLength) + (leaf.isVariadic ? " or more" : "") + " values"; - } - default: - { - throw InternalCompilerError{"DiffPath::toStringALeaf is not exhaustive"}; - } - } -} - -void DiffError::checkNonMissingPropertyLeavesHaveNulloptTableProperty() const -{ - if (left.tableProperty.has_value() || right.tableProperty.has_value()) - throw InternalCompilerError{"Non-MissingProperty DiffError should have nullopt tableProperty in both leaves"}; -} - -std::string getDevFixFriendlyName(const std::optional& maybeSymbol, TypeId ty) -{ - if (maybeSymbol.has_value()) - return *maybeSymbol; - - if (auto table = get(ty)) - { - if (table->name.has_value()) - return *table->name; - else if (table->syntheticName.has_value()) - return *table->syntheticName; - } - if (auto metatable = get(ty)) - { - if (metatable->syntheticName.has_value()) - { - return *metatable->syntheticName; - } - } - return ""; -} - -std::string DifferEnvironment::getDevFixFriendlyNameLeft() const -{ - return getDevFixFriendlyName(externalSymbolLeft, rootLeft); -} - -std::string DifferEnvironment::getDevFixFriendlyNameRight() const -{ - return getDevFixFriendlyName(externalSymbolRight, rootRight); -} - -std::string DiffError::toString(bool multiLine) const -{ - std::string conditionalNewline = multiLine ? "\n" : " "; - std::string conditionalIndent = multiLine ? " " : ""; - switch (kind) - { - case DiffError::Kind::IncompatibleGeneric: - { - std::string diffPathStr{diffPath.toString(true)}; - return "DiffError: these two types are not equal because the left generic at" + conditionalNewline + conditionalIndent + leftRootName + - diffPathStr + conditionalNewline + "cannot be the same type parameter as the right generic at" + conditionalNewline + - conditionalIndent + rightRootName + diffPathStr; - } - default: - { - return "DiffError: these two types are not equal because the left type at" + conditionalNewline + conditionalIndent + - toStringALeaf(leftRootName, left, right, multiLine) + "," + conditionalNewline + "while the right type at" + conditionalNewline + - conditionalIndent + toStringALeaf(rightRootName, right, left, multiLine); - } - } -} - -void DiffError::checkValidInitialization(const DiffPathNodeLeaf& left, const DiffPathNodeLeaf& right) -{ - if (!left.ty.has_value() || !right.ty.has_value()) - { - // TODO: think about whether this should be always thrown! - // For example, Kind::Primitive doesn't make too much sense to have a TypeId - // throw InternalCompilerError{"Left and Right fields are leaf nodes and must have a TypeId"}; - } -} - -void DifferResult::wrapDiffPath(DiffPathNode node) -{ - if (!diffError.has_value()) - { - throw InternalCompilerError{"Cannot wrap diffPath because there is no diffError"}; - } - - diffError->diffPath.path.push_back(node); -} - -static DifferResult diffUsingEnv(DifferEnvironment& env, TypeId left, TypeId right); -static DifferResult diffTable(DifferEnvironment& env, TypeId left, TypeId right); -static DifferResult diffMetatable(DifferEnvironment& env, TypeId left, TypeId right); -static DifferResult diffPrimitive(DifferEnvironment& env, TypeId left, TypeId right); -static DifferResult diffSingleton(DifferEnvironment& env, TypeId left, TypeId right); -static DifferResult diffFunction(DifferEnvironment& env, TypeId left, TypeId right); -static DifferResult diffGeneric(DifferEnvironment& env, TypeId left, TypeId right); -static DifferResult diffNegation(DifferEnvironment& env, TypeId left, TypeId right); -static DifferResult diffExternType(DifferEnvironment& env, TypeId left, TypeId right); -struct FindSeteqCounterexampleResult -{ - // nullopt if no counterexample found - std::optional mismatchIdx; - // true if counterexample is in the left, false if cex is in the right - bool inLeft; -}; -static FindSeteqCounterexampleResult findSeteqCounterexample( - DifferEnvironment& env, - const std::vector& left, - const std::vector& right -); -static DifferResult diffUnion(DifferEnvironment& env, TypeId left, TypeId right); -static DifferResult diffIntersection(DifferEnvironment& env, TypeId left, TypeId right); -/** - * The last argument gives context info on which complex type contained the TypePack. - */ -static DifferResult diffTpi(DifferEnvironment& env, DiffError::Kind possibleNonNormalErrorKind, TypePackId left, TypePackId right); -static DifferResult diffCanonicalTpShape( - DifferEnvironment& env, - DiffError::Kind possibleNonNormalErrorKind, - const std::pair, std::optional>& left, - const std::pair, std::optional>& right -); -static DifferResult diffHandleFlattenedTail(DifferEnvironment& env, DiffError::Kind possibleNonNormalErrorKind, TypePackId left, TypePackId right); -static DifferResult diffGenericTp(DifferEnvironment& env, TypePackId left, TypePackId right); - -static DifferResult diffTable(DifferEnvironment& env, TypeId left, TypeId right) -{ - const TableType* leftTable = get(left); - const TableType* rightTable = get(right); - LUAU_ASSERT(leftTable); - LUAU_ASSERT(rightTable); - - for (auto const& [field, value] : leftTable->props) - { - if (rightTable->props.find(field) == rightTable->props.end()) - { - // left has a field the right doesn't - return DifferResult{DiffError{ - DiffError::Kind::MissingTableProperty, - DiffPathNodeLeaf::detailsTableProperty(value.type(), field), - DiffPathNodeLeaf::nullopts(), - env.getDevFixFriendlyNameLeft(), - env.getDevFixFriendlyNameRight(), - }}; - } - } - for (auto const& [field, value] : rightTable->props) - { - if (leftTable->props.find(field) == leftTable->props.end()) - { - // right has a field the left doesn't - return DifferResult{DiffError{ - DiffError::Kind::MissingTableProperty, - DiffPathNodeLeaf::nullopts(), - DiffPathNodeLeaf::detailsTableProperty(value.type(), field), - env.getDevFixFriendlyNameLeft(), - env.getDevFixFriendlyNameRight() - }}; - } - } - // left and right have the same set of keys - for (auto const& [field, leftValue] : leftTable->props) - { - auto const& rightValue = rightTable->props.at(field); - DifferResult differResult = diffUsingEnv(env, leftValue.type(), rightValue.type()); - if (differResult.diffError.has_value()) - { - differResult.wrapDiffPath(DiffPathNode::constructWithTableProperty(field)); - return differResult; - } - } - return DifferResult{}; -} - -static DifferResult diffMetatable(DifferEnvironment& env, TypeId left, TypeId right) -{ - const MetatableType* leftMetatable = get(left); - const MetatableType* rightMetatable = get(right); - LUAU_ASSERT(leftMetatable); - LUAU_ASSERT(rightMetatable); - - DifferResult diffRes = diffUsingEnv(env, leftMetatable->table, rightMetatable->table); - if (diffRes.diffError.has_value()) - { - return diffRes; - } - - diffRes = diffUsingEnv(env, leftMetatable->metatable, rightMetatable->metatable); - if (diffRes.diffError.has_value()) - { - diffRes.wrapDiffPath(DiffPathNode::constructWithTableProperty("__metatable")); - return diffRes; - } - return DifferResult{}; -} - -static DifferResult diffPrimitive(DifferEnvironment& env, TypeId left, TypeId right) -{ - const PrimitiveType* leftPrimitive = get(left); - const PrimitiveType* rightPrimitive = get(right); - LUAU_ASSERT(leftPrimitive); - LUAU_ASSERT(rightPrimitive); - - if (leftPrimitive->type != rightPrimitive->type) - { - return DifferResult{DiffError{ - DiffError::Kind::Normal, - DiffPathNodeLeaf::detailsNormal(left), - DiffPathNodeLeaf::detailsNormal(right), - env.getDevFixFriendlyNameLeft(), - env.getDevFixFriendlyNameRight(), - }}; - } - return DifferResult{}; -} - -static DifferResult diffSingleton(DifferEnvironment& env, TypeId left, TypeId right) -{ - const SingletonType* leftSingleton = get(left); - const SingletonType* rightSingleton = get(right); - LUAU_ASSERT(leftSingleton); - LUAU_ASSERT(rightSingleton); - - if (*leftSingleton != *rightSingleton) - { - return DifferResult{DiffError{ - DiffError::Kind::Normal, - DiffPathNodeLeaf::detailsNormal(left), - DiffPathNodeLeaf::detailsNormal(right), - env.getDevFixFriendlyNameLeft(), - env.getDevFixFriendlyNameRight(), - }}; - } - return DifferResult{}; -} - -static DifferResult diffFunction(DifferEnvironment& env, TypeId left, TypeId right) -{ - const FunctionType* leftFunction = get(left); - const FunctionType* rightFunction = get(right); - LUAU_ASSERT(leftFunction); - LUAU_ASSERT(rightFunction); - - DifferResult differResult = diffTpi(env, DiffError::Kind::LengthMismatchInFnArgs, leftFunction->argTypes, rightFunction->argTypes); - if (differResult.diffError.has_value()) - return differResult; - return diffTpi(env, DiffError::Kind::LengthMismatchInFnRets, leftFunction->retTypes, rightFunction->retTypes); -} - -static DifferResult diffGeneric(DifferEnvironment& env, TypeId left, TypeId right) -{ - LUAU_ASSERT(get(left)); - LUAU_ASSERT(get(right)); - // Try to pair up the generics - bool isLeftFree = !env.genericMatchedPairs.contains(left); - bool isRightFree = !env.genericMatchedPairs.contains(right); - if (isLeftFree && isRightFree) - { - env.genericMatchedPairs[left] = right; - env.genericMatchedPairs[right] = left; - return DifferResult{}; - } - else if (isLeftFree || isRightFree) - { - return DifferResult{DiffError{ - DiffError::Kind::IncompatibleGeneric, - DiffPathNodeLeaf::nullopts(), - DiffPathNodeLeaf::nullopts(), - env.getDevFixFriendlyNameLeft(), - env.getDevFixFriendlyNameRight(), - }}; - } - - // Both generics are already paired up - if (*env.genericMatchedPairs.find(left) == right) - return DifferResult{}; - - return DifferResult{DiffError{ - DiffError::Kind::IncompatibleGeneric, - DiffPathNodeLeaf::nullopts(), - DiffPathNodeLeaf::nullopts(), - env.getDevFixFriendlyNameLeft(), - env.getDevFixFriendlyNameRight(), - }}; -} - -static DifferResult diffNegation(DifferEnvironment& env, TypeId left, TypeId right) -{ - const NegationType* leftNegation = get(left); - const NegationType* rightNegation = get(right); - LUAU_ASSERT(leftNegation); - LUAU_ASSERT(rightNegation); - - DifferResult differResult = diffUsingEnv(env, leftNegation->ty, rightNegation->ty); - if (!differResult.diffError.has_value()) - return DifferResult{}; - - differResult.wrapDiffPath(DiffPathNode::constructWithKind(DiffPathNode::Kind::Negation)); - return differResult; -} - -static DifferResult diffExternType(DifferEnvironment& env, TypeId left, TypeId right) -{ - const ExternType* leftExternType = get(left); - const ExternType* rightExternType = get(right); - LUAU_ASSERT(leftExternType); - LUAU_ASSERT(rightExternType); - - if (leftExternType == rightExternType) - { - return DifferResult{}; - } - - return DifferResult{DiffError{ - DiffError::Kind::Normal, - DiffPathNodeLeaf::detailsNormal(left), - DiffPathNodeLeaf::detailsNormal(right), - env.getDevFixFriendlyNameLeft(), - env.getDevFixFriendlyNameRight(), - }}; -} - -static FindSeteqCounterexampleResult findSeteqCounterexample( - DifferEnvironment& env, - const std::vector& left, - const std::vector& right -) -{ - std::unordered_set unmatchedRightIdxes; - for (size_t i = 0; i < right.size(); i++) - unmatchedRightIdxes.insert(i); - for (size_t leftIdx = 0; leftIdx < left.size(); leftIdx++) - { - bool leftIdxIsMatched = false; - auto unmatchedRightIdxIt = unmatchedRightIdxes.begin(); - while (unmatchedRightIdxIt != unmatchedRightIdxes.end()) - { - DifferResult differResult = diffUsingEnv(env, left[leftIdx], right[*unmatchedRightIdxIt]); - if (differResult.diffError.has_value()) - { - unmatchedRightIdxIt++; - continue; - } - // unmatchedRightIdxIt is matched with current leftIdx - env.recordProvenEqual(left[leftIdx], right[*unmatchedRightIdxIt]); - leftIdxIsMatched = true; - unmatchedRightIdxIt = unmatchedRightIdxes.erase(unmatchedRightIdxIt); - } - if (!leftIdxIsMatched) - { - return FindSeteqCounterexampleResult{leftIdx, true}; - } - } - if (unmatchedRightIdxes.empty()) - return FindSeteqCounterexampleResult{std::nullopt, false}; - return FindSeteqCounterexampleResult{*unmatchedRightIdxes.begin(), false}; -} - -static DifferResult diffUnion(DifferEnvironment& env, TypeId left, TypeId right) -{ - const UnionType* leftUnion = get(left); - const UnionType* rightUnion = get(right); - LUAU_ASSERT(leftUnion); - LUAU_ASSERT(rightUnion); - - FindSeteqCounterexampleResult findSeteqCexResult = findSeteqCounterexample(env, leftUnion->options, rightUnion->options); - if (findSeteqCexResult.mismatchIdx.has_value()) - { - if (findSeteqCexResult.inLeft) - return DifferResult{DiffError{ - DiffError::Kind::MissingUnionMember, - DiffPathNodeLeaf::detailsUnionIndex(leftUnion->options[*findSeteqCexResult.mismatchIdx], *findSeteqCexResult.mismatchIdx), - DiffPathNodeLeaf::nullopts(), - env.getDevFixFriendlyNameLeft(), - env.getDevFixFriendlyNameRight(), - }}; - else - return DifferResult{DiffError{ - DiffError::Kind::MissingUnionMember, - DiffPathNodeLeaf::nullopts(), - DiffPathNodeLeaf::detailsUnionIndex(rightUnion->options[*findSeteqCexResult.mismatchIdx], *findSeteqCexResult.mismatchIdx), - env.getDevFixFriendlyNameLeft(), - env.getDevFixFriendlyNameRight(), - }}; - } - - // TODO: somehow detect mismatch index, likely using heuristics - - return DifferResult{}; -} - -static DifferResult diffIntersection(DifferEnvironment& env, TypeId left, TypeId right) -{ - const IntersectionType* leftIntersection = get(left); - const IntersectionType* rightIntersection = get(right); - LUAU_ASSERT(leftIntersection); - LUAU_ASSERT(rightIntersection); - - FindSeteqCounterexampleResult findSeteqCexResult = findSeteqCounterexample(env, leftIntersection->parts, rightIntersection->parts); - if (findSeteqCexResult.mismatchIdx.has_value()) - { - if (findSeteqCexResult.inLeft) - return DifferResult{DiffError{ - DiffError::Kind::MissingIntersectionMember, - DiffPathNodeLeaf::detailsUnionIndex(leftIntersection->parts[*findSeteqCexResult.mismatchIdx], *findSeteqCexResult.mismatchIdx), - DiffPathNodeLeaf::nullopts(), - env.getDevFixFriendlyNameLeft(), - env.getDevFixFriendlyNameRight(), - }}; - else - return DifferResult{DiffError{ - DiffError::Kind::MissingIntersectionMember, - DiffPathNodeLeaf::nullopts(), - DiffPathNodeLeaf::detailsUnionIndex(rightIntersection->parts[*findSeteqCexResult.mismatchIdx], *findSeteqCexResult.mismatchIdx), - env.getDevFixFriendlyNameLeft(), - env.getDevFixFriendlyNameRight(), - }}; - } - - // TODO: somehow detect mismatch index, likely using heuristics - - return DifferResult{}; -} - -static DifferResult diffUsingEnv(DifferEnvironment& env, TypeId left, TypeId right) -{ - left = follow(left); - right = follow(right); - - if (left->ty.index() != right->ty.index()) - { - return DifferResult{DiffError{ - DiffError::Kind::Normal, - DiffPathNodeLeaf::detailsNormal(left), - DiffPathNodeLeaf::detailsNormal(right), - env.getDevFixFriendlyNameLeft(), - env.getDevFixFriendlyNameRight(), - }}; - } - - // Both left and right are the same variant - - // Check cycles & caches - if (env.isAssumedEqual(left, right) || env.isProvenEqual(left, right)) - return DifferResult{}; - - if (isSimple(left)) - { - if (auto lp = get(left)) - return diffPrimitive(env, left, right); - else if (auto ls = get(left)) - { - return diffSingleton(env, left, right); - } - else if (auto la = get(left)) - { - // Both left and right must be Any if either is Any for them to be equal! - return DifferResult{}; - } - else if (auto lu = get(left)) - { - return DifferResult{}; - } - else if (auto ln = get(left)) - { - return DifferResult{}; - } - else if (auto ln = get(left)) - { - return diffNegation(env, left, right); - } - else if (auto lc = get(left)) - { - return diffExternType(env, left, right); - } - - throw InternalCompilerError{"Unimplemented Simple TypeId variant for diffing"}; - } - - // Both left and right are the same non-Simple - // Non-simple types must record visits in the DifferEnvironment - env.pushVisiting(left, right); - - if (auto lt = get(left)) - { - DifferResult diffRes = diffTable(env, left, right); - if (!diffRes.diffError.has_value()) - { - env.recordProvenEqual(left, right); - } - env.popVisiting(); - return diffRes; - } - if (auto lm = get(left)) - { - env.popVisiting(); - return diffMetatable(env, left, right); - } - if (auto lf = get(left)) - { - DifferResult diffRes = diffFunction(env, left, right); - if (!diffRes.diffError.has_value()) - { - env.recordProvenEqual(left, right); - } - env.popVisiting(); - return diffRes; - } - if (auto lg = get(left)) - { - DifferResult diffRes = diffGeneric(env, left, right); - if (!diffRes.diffError.has_value()) - { - env.recordProvenEqual(left, right); - } - env.popVisiting(); - return diffRes; - } - if (auto lu = get(left)) - { - DifferResult diffRes = diffUnion(env, left, right); - if (!diffRes.diffError.has_value()) - { - env.recordProvenEqual(left, right); - } - env.popVisiting(); - return diffRes; - } - if (auto li = get(left)) - { - DifferResult diffRes = diffIntersection(env, left, right); - if (!diffRes.diffError.has_value()) - { - env.recordProvenEqual(left, right); - } - env.popVisiting(); - return diffRes; - } - if (auto le = get(left)) - { - // TODO: return debug-friendly result state - env.popVisiting(); - return DifferResult{}; - } - - throw InternalCompilerError{"Unimplemented non-simple TypeId variant for diffing"}; -} - -static DifferResult diffTpi(DifferEnvironment& env, DiffError::Kind possibleNonNormalErrorKind, TypePackId left, TypePackId right) -{ - left = follow(left); - right = follow(right); - - // Canonicalize - std::pair, std::optional> leftFlatTpi = flatten(left); - std::pair, std::optional> rightFlatTpi = flatten(right); - - // Check for shape equality - DifferResult diffResult = diffCanonicalTpShape(env, possibleNonNormalErrorKind, leftFlatTpi, rightFlatTpi); - if (diffResult.diffError.has_value()) - { - return diffResult; - } - - // Left and Right have the same shape - for (size_t i = 0; i < leftFlatTpi.first.size(); i++) - { - DifferResult differResult = diffUsingEnv(env, leftFlatTpi.first[i], rightFlatTpi.first[i]); - if (!differResult.diffError.has_value()) - continue; - - switch (possibleNonNormalErrorKind) - { - case DiffError::Kind::LengthMismatchInFnArgs: - { - differResult.wrapDiffPath(DiffPathNode::constructWithKindAndIndex(DiffPathNode::Kind::FunctionArgument, i)); - return differResult; - } - case DiffError::Kind::LengthMismatchInFnRets: - { - differResult.wrapDiffPath(DiffPathNode::constructWithKindAndIndex(DiffPathNode::Kind::FunctionReturn, i)); - return differResult; - } - default: - { - throw InternalCompilerError{"Unhandled Tpi diffing case with same shape"}; - } - } - } - if (!leftFlatTpi.second.has_value()) - return DifferResult{}; - - return diffHandleFlattenedTail(env, possibleNonNormalErrorKind, *leftFlatTpi.second, *rightFlatTpi.second); -} - -static DifferResult diffCanonicalTpShape( - DifferEnvironment& env, - DiffError::Kind possibleNonNormalErrorKind, - const std::pair, std::optional>& left, - const std::pair, std::optional>& right -) -{ - if (left.first.size() == right.first.size() && left.second.has_value() == right.second.has_value()) - return DifferResult{}; - - return DifferResult{DiffError{ - possibleNonNormalErrorKind, - DiffPathNodeLeaf::detailsLength(int(left.first.size()), left.second.has_value()), - DiffPathNodeLeaf::detailsLength(int(right.first.size()), right.second.has_value()), - env.getDevFixFriendlyNameLeft(), - env.getDevFixFriendlyNameRight(), - }}; -} - -static DifferResult diffHandleFlattenedTail(DifferEnvironment& env, DiffError::Kind possibleNonNormalErrorKind, TypePackId left, TypePackId right) -{ - left = follow(left); - right = follow(right); - - if (left->ty.index() != right->ty.index()) - { - return DifferResult{DiffError{ - DiffError::Kind::Normal, - DiffPathNodeLeaf::detailsNormal(env.visitingBegin()->first), - DiffPathNodeLeaf::detailsNormal(env.visitingBegin()->second), - env.getDevFixFriendlyNameLeft(), - env.getDevFixFriendlyNameRight(), - }}; - } - - // Both left and right are the same variant - - if (auto lv = get(left)) - { - auto rv = get(right); - DifferResult differResult = diffUsingEnv(env, lv->ty, rv->ty); - if (!differResult.diffError.has_value()) - return DifferResult{}; - - switch (possibleNonNormalErrorKind) - { - case DiffError::Kind::LengthMismatchInFnArgs: - { - differResult.wrapDiffPath(DiffPathNode::constructWithKind(DiffPathNode::Kind::FunctionArgument)); - return differResult; - } - case DiffError::Kind::LengthMismatchInFnRets: - { - differResult.wrapDiffPath(DiffPathNode::constructWithKind(DiffPathNode::Kind::FunctionReturn)); - return differResult; - } - default: - { - throw InternalCompilerError{"Unhandled flattened tail case for VariadicTypePack"}; - } - } - } - if (auto lg = get(left)) - { - DifferResult diffRes = diffGenericTp(env, left, right); - if (!diffRes.diffError.has_value()) - return DifferResult{}; - switch (possibleNonNormalErrorKind) - { - case DiffError::Kind::LengthMismatchInFnArgs: - { - diffRes.wrapDiffPath(DiffPathNode::constructWithKind(DiffPathNode::Kind::FunctionArgument)); - return diffRes; - } - case DiffError::Kind::LengthMismatchInFnRets: - { - diffRes.wrapDiffPath(DiffPathNode::constructWithKind(DiffPathNode::Kind::FunctionReturn)); - return diffRes; - } - default: - { - throw InternalCompilerError{"Unhandled flattened tail case for GenericTypePack"}; - } - } - } - - throw InternalCompilerError{"Unhandled tail type pack variant for flattened tails"}; -} - -static DifferResult diffGenericTp(DifferEnvironment& env, TypePackId left, TypePackId right) -{ - LUAU_ASSERT(get(left)); - LUAU_ASSERT(get(right)); - // Try to pair up the generics - bool isLeftFree = !env.genericTpMatchedPairs.contains(left); - bool isRightFree = !env.genericTpMatchedPairs.contains(right); - if (isLeftFree && isRightFree) - { - env.genericTpMatchedPairs[left] = right; - env.genericTpMatchedPairs[right] = left; - return DifferResult{}; - } - else if (isLeftFree || isRightFree) - { - return DifferResult{DiffError{ - DiffError::Kind::IncompatibleGeneric, - DiffPathNodeLeaf::nullopts(), - DiffPathNodeLeaf::nullopts(), - env.getDevFixFriendlyNameLeft(), - env.getDevFixFriendlyNameRight(), - }}; - } - - // Both generics are already paired up - if (*env.genericTpMatchedPairs.find(left) == right) - return DifferResult{}; - - return DifferResult{DiffError{ - DiffError::Kind::IncompatibleGeneric, - DiffPathNodeLeaf::nullopts(), - DiffPathNodeLeaf::nullopts(), - env.getDevFixFriendlyNameLeft(), - env.getDevFixFriendlyNameRight(), - }}; -} - -bool DifferEnvironment::isProvenEqual(TypeId left, TypeId right) const -{ - return provenEqual.find({left, right}) != provenEqual.end(); -} - -bool DifferEnvironment::isAssumedEqual(TypeId left, TypeId right) const -{ - return visiting.find({left, right}) != visiting.end(); -} - -void DifferEnvironment::recordProvenEqual(TypeId left, TypeId right) -{ - provenEqual.insert({left, right}); - provenEqual.insert({right, left}); -} - -void DifferEnvironment::pushVisiting(TypeId left, TypeId right) -{ - LUAU_ASSERT(visiting.find({left, right}) == visiting.end()); - LUAU_ASSERT(visiting.find({right, left}) == visiting.end()); - visitingStack.push_back({left, right}); - visiting.insert({left, right}); - visiting.insert({right, left}); -} - -void DifferEnvironment::popVisiting() -{ - auto tyPair = visitingStack.back(); - visiting.erase({tyPair.first, tyPair.second}); - visiting.erase({tyPair.second, tyPair.first}); - visitingStack.pop_back(); -} - -std::vector>::const_reverse_iterator DifferEnvironment::visitingBegin() const -{ - return visitingStack.crbegin(); -} - -std::vector>::const_reverse_iterator DifferEnvironment::visitingEnd() const -{ - return visitingStack.crend(); -} - -DifferResult diff(TypeId ty1, TypeId ty2) -{ - DifferEnvironment differEnv{ty1, ty2, std::nullopt, std::nullopt}; - return diffUsingEnv(differEnv, ty1, ty2); -} - -DifferResult diffWithSymbols(TypeId ty1, TypeId ty2, std::optional symbol1, std::optional symbol2) -{ - DifferEnvironment differEnv{ty1, ty2, symbol1, symbol2}; - return diffUsingEnv(differEnv, ty1, ty2); -} - -bool isSimple(TypeId ty) -{ - ty = follow(ty); - // TODO: think about GenericType, etc. - return get(ty) || get(ty) || get(ty) || get(ty) || get(ty) || - get(ty) || get(ty); -} - -} // namespace Luau diff --git a/Analysis/src/Error.cpp b/Analysis/src/Error.cpp index 5d512c4a..fa2df2da 100644 --- a/Analysis/src/Error.cpp +++ b/Analysis/src/Error.cpp @@ -18,7 +18,7 @@ #include LUAU_FASTINTVARIABLE(LuauIndentTypeMismatchMaxTypeLength, 10) -LUAU_FASTFLAG(LuauEagerGeneralization2) +LUAU_FASTFLAG(LuauEagerGeneralization3) LUAU_FASTFLAGVARIABLE(LuauBetterCannotCallFunctionPrimitive) @@ -663,7 +663,7 @@ struct ErrorConverter } // binary operators - const auto binaryOps = FFlag::LuauEagerGeneralization2 ? kBinaryOps : DEPRECATED_kBinaryOps; + const auto binaryOps = FFlag::LuauEagerGeneralization3 ? kBinaryOps : DEPRECATED_kBinaryOps; if (auto binaryString = binaryOps.find(tfit->function->name); binaryString != binaryOps.end()) { std::string result = "Operator '" + std::string(binaryString->second) + "' could not be applied to operands of types "; @@ -718,7 +718,7 @@ struct ErrorConverter "'"; } - if ((FFlag::LuauEagerGeneralization2 ? kUnreachableTypeFunctions : DEPRECATED_kUnreachableTypeFunctions).count(tfit->function->name)) + if ((FFlag::LuauEagerGeneralization3 ? kUnreachableTypeFunctions : DEPRECATED_kUnreachableTypeFunctions).count(tfit->function->name)) { return "Type function instance " + Luau::toString(e.ty) + " is uninhabited\n" + "This is likely to be a bug, please report it at https://github.com/luau-lang/luau/issues"; @@ -852,6 +852,25 @@ struct ErrorConverter { return "Unexpected array-like table item: the indexer key type of this table is not `number`."; } + + std::string operator()(const CannotCheckDynamicStringFormatCalls& e) const + { + return "We cannot statically check the type of `string.format` when called with a format string that is not statically known.\n" + "If you'd like to use an unchecked `string.format` call, you can cast the format string to `any` using `:: any`."; + } + + + std::string operator()(const GenericTypeCountMismatch& e) const + { + return "Different number of generic type parameters: subtype had " + std::to_string(e.subTyGenericCount) + ", supertype had " + + std::to_string(e.superTyGenericCount) + "."; + } + + std::string operator()(const GenericTypePackCountMismatch& e) const + { + return "Different number of generic type pack parameters: subtype had " + std::to_string(e.subTyGenericPackCount) + ", supertype had " + + std::to_string(e.superTyGenericPackCount) + "."; + } }; struct InvalidNameChecker @@ -1240,6 +1259,16 @@ bool CannotAssignToNever::operator==(const CannotAssignToNever& rhs) const return *rhsType == *rhs.rhsType && reason == rhs.reason; } +bool GenericTypeCountMismatch::operator==(const GenericTypeCountMismatch& rhs) const +{ + return subTyGenericCount == rhs.subTyGenericCount && superTyGenericCount == rhs.superTyGenericCount; +} + +bool GenericTypePackCountMismatch::operator==(const GenericTypePackCountMismatch& rhs) const +{ + return subTyGenericPackCount == rhs.subTyGenericPackCount && superTyGenericPackCount == rhs.superTyGenericPackCount; +} + std::string toString(const TypeError& error) { return toString(error, TypeErrorToStringOptions{}); @@ -1451,6 +1480,15 @@ void copyError(T& e, TypeArena& destArena, CloneState& cloneState) else if constexpr (std::is_same_v) { } + else if constexpr (std::is_same_v) + { + } + else if constexpr (std::is_same_v) + { + } + else if constexpr (std::is_same_v) + { + } else static_assert(always_false_v, "Non-exhaustive type switch"); } diff --git a/Analysis/src/Frontend.cpp b/Analysis/src/Frontend.cpp index 2ef14b11..0e8c48be 100644 --- a/Analysis/src/Frontend.cpp +++ b/Analysis/src/Frontend.cpp @@ -40,7 +40,7 @@ LUAU_FASTINT(LuauTarjanChildLimit) LUAU_FASTFLAG(LuauInferInNoCheckMode) LUAU_FASTFLAGVARIABLE(LuauKnowsTheDataModel3) LUAU_FASTFLAG(LuauSolverV2) -LUAU_FASTFLAG(LuauEagerGeneralization2) +LUAU_FASTFLAG(LuauEagerGeneralization3) LUAU_FASTFLAGVARIABLE(DebugLuauLogSolverToJson) LUAU_FASTFLAGVARIABLE(DebugLuauLogSolverToJsonFile) LUAU_FASTFLAGVARIABLE(DebugLuauForbidInternalTypes) @@ -1444,7 +1444,7 @@ ModulePtr check( // is set, and another when it is unset. std::optional cs; - if (FFlag::LuauEagerGeneralization2) + if (FFlag::LuauEagerGeneralization3) { ConstraintSet constraintSet = cg.run(sourceModule.root); result->errors = std::move(constraintSet.errors); diff --git a/Analysis/src/Generalization.cpp b/Analysis/src/Generalization.cpp index bd26d2a2..5e38ba43 100644 --- a/Analysis/src/Generalization.cpp +++ b/Analysis/src/Generalization.cpp @@ -10,13 +10,14 @@ #include "Luau/ToString.h" #include "Luau/Type.h" #include "Luau/TypeArena.h" +#include "Luau/TypeIds.h" #include "Luau/TypePack.h" -#include "Luau/Substitution.h" #include "Luau/VisitType.h" LUAU_FASTFLAG(LuauEnableWriteOnlyProperties) -LUAU_FASTFLAGVARIABLE(LuauEagerGeneralization2) +LUAU_FASTFLAGVARIABLE(LuauEagerGeneralization3) +LUAU_FASTFLAGVARIABLE(LuauGeneralizationCannotMutateAcrossModules) namespace Luau { @@ -469,7 +470,7 @@ struct FreeTypeSearcher : TypeVisitor bool visit(TypeId ty, const FreeType& ft) override { - if (FFlag::LuauEagerGeneralization2) + if (FFlag::LuauEagerGeneralization3) { if (!subsumes(scope, ft.scope)) return true; @@ -520,7 +521,7 @@ struct FreeTypeSearcher : TypeVisitor if ((tt.state == TableState::Free || tt.state == TableState::Unsealed) && subsumes(scope, tt.scope)) { - if (FFlag::LuauEagerGeneralization2) + if (FFlag::LuauEagerGeneralization3) unsealedTables.insert(ty); else { @@ -593,7 +594,7 @@ struct FreeTypeSearcher : TypeVisitor if (tt.indexer) { - if (FFlag::LuauEagerGeneralization2) + if (FFlag::LuauEagerGeneralization3) { // {[K]: V} is equivalent to three functions: get, set, and iterate // @@ -651,7 +652,7 @@ struct FreeTypeSearcher : TypeVisitor if (!subsumes(scope, ftp.scope)) return true; - if (FFlag::LuauEagerGeneralization2) + if (FFlag::LuauEagerGeneralization3) { GeneralizationParams& params = typePacks[tp]; ++params.useCount; @@ -1127,129 +1128,77 @@ struct TypeCacher : TypeOnceVisitor } }; -struct RemoveType : Substitution // NOLINT +namespace +{ + +struct TypeRemover { NotNull builtinTypes; + NotNull arena; + TypeId needle; + DenseHashSet seen{nullptr}; - RemoveType(NotNull builtinTypes, TypeArena* arena, TypeId needle) - : Substitution(arena) - , builtinTypes(builtinTypes) - , needle(needle) + void process(TypeId item) { - } + item = follow(item); - bool ignoreChildren(TypeId ty) override - { - if (get(ty) || get(ty)) - return false; - else - return true; - } + // If we've already visited this item, or it's outside our arena, then + // do not try to mutate it. + if (seen.contains(item) || item->owningArena != arena || item->persistent) + return; + seen.insert(item); - bool isDirty(TypeId ty) override - { - // A union or intersection is dirty if it contains the needle or if it has any duplicate members. - if (auto ut = get(ty)) + if (auto ut = getMutable(item)) { - DenseHashSet distinctParts{nullptr}; - size_t count = 0; - for (TypeId part : ut) + TypeIds newOptions; + for (TypeId option : ut->options) { - ++count; - if (part == needle) - return true; - distinctParts.insert(follow(part)); + process(option); + option = follow(option); + if (option != needle && !is(option) && option != item) + newOptions.insert(option); + } + if (ut->options.size() != newOptions.size()) + { + if (newOptions.empty()) + emplaceType(asMutable(item), builtinTypes->neverType); + else if (newOptions.size() == 1) + emplaceType(asMutable(item), *newOptions.begin()); + else + emplaceType(asMutable(item), arena->addType(UnionType{newOptions.take()})); } - return distinctParts.size() != count; } - else if (auto it = get(ty)) + else if (auto it = getMutable(item)) { - DenseHashSet distinctParts{nullptr}; - size_t count = 0; - for (TypeId part : it) + TypeIds newParts; + for (TypeId part : it->parts) { - ++count; - if (part == needle) - return true; - distinctParts.insert(follow(part)); + process(part); + part = follow(part); + if (part != needle && !is(part) && part != item) + newParts.insert(part); + } + if (it->parts.size() != newParts.size()) + { + if (newParts.empty()) + emplaceType(asMutable(item), builtinTypes->unknownType); + else if (newParts.size() == 1) + emplaceType(asMutable(item), *newParts.begin()); + else + emplaceType(asMutable(item), arena->addType(IntersectionType{newParts.take()})); } - return distinctParts.size() != count; } - - return false; } - bool isDirty(TypePackId tp) override - { - return false; - } - - TypeId clean(TypeId ty) override - { - if (auto ut = get(ty)) - { - OrderedSet newParts; - - for (TypeId ty : ut) - { - if (ty != needle && !is(ty)) - newParts.insert(ty); - } - - if (newParts.empty()) - return builtinTypes->neverType; - else if (newParts.size() == 1) - { - TypeId onlyType = *newParts.begin(); - LUAU_ASSERT(onlyType != needle); - return onlyType; - } - else - return arena->addType(UnionType{newParts.takeVector()}); - } - else if (auto it = get(ty)) - { - OrderedSet newParts; - - for (TypeId ty : it) - { - if (ty != needle && !is(ty)) - newParts.insert(ty); - } - - if (newParts.empty()) - return builtinTypes->unknownType; - else if (newParts.size() == 1) - { - TypeId onlyType = *newParts.begin(); - LUAU_ASSERT(onlyType != needle); - return onlyType; - } - else - return arena->addType(IntersectionType{newParts.takeVector()}); - } - else - return ty; - } - - TypePackId clean(TypePackId tp) override - { - return tp; - } }; -/** - * Remove occurrences of `needle` within `haystack`. This is used to cull cyclic bounds from free types. - * - * @param haystack Either the upper or lower bound of a free type. - * @param needle The type to be removed. - */ -[[nodiscard]] -static std::optional removeType(NotNull arena, NotNull builtinTypes, TypeId haystack, TypeId needle) +void removeType(NotNull arena, NotNull builtinTypes, TypeId haystack, TypeId needle) { - RemoveType rt{builtinTypes, arena, needle}; - return rt.substitute(haystack); + TypeRemover tr{builtinTypes, arena, needle}; + tr.process(haystack); +} + } GeneralizationResult generalizeType( @@ -1274,7 +1223,7 @@ GeneralizationResult generalizeType( if (!hasLowerBound && !hasUpperBound) { - if (!isWithinFunction || (!FFlag::LuauEagerGeneralization2 && (params.polarity != Polarity::Mixed && params.useCount == 1))) + if (!isWithinFunction || (!FFlag::LuauEagerGeneralization3 && (params.polarity != Polarity::Mixed && params.useCount == 1))) emplaceType(asMutable(freeTy), builtinTypes->unknownType); else { @@ -1294,19 +1243,11 @@ GeneralizationResult generalizeType( if (FreeType* lowerFree = getMutable(lb); lowerFree && lowerFree->upperBound == freeTy) lowerFree->upperBound = builtinTypes->unknownType; else - { - std::optional removed = removeType(arena, builtinTypes, lb, freeTy); - if (removed) - lb = *removed; - else - return {std::nullopt, false, /*resourceLimitsExceeded*/ true}; - - ft->lowerBound = lb; - } + removeType(arena, builtinTypes, lb, freeTy); if (follow(lb) != freeTy) emplaceType(asMutable(freeTy), lb); - else if (!isWithinFunction || (!FFlag::LuauEagerGeneralization2 && params.useCount == 1)) + else if (!isWithinFunction || (!FFlag::LuauEagerGeneralization3 && params.useCount == 1)) emplaceType(asMutable(freeTy), builtinTypes->unknownType); else { @@ -1321,15 +1262,7 @@ GeneralizationResult generalizeType( if (FreeType* upperFree = getMutable(ub); upperFree && upperFree->lowerBound == freeTy) upperFree->lowerBound = builtinTypes->neverType; else - { - // If the free type appears within its own upper bound, cull that cycle. - std::optional removed = removeType(arena, builtinTypes, ub, freeTy); - if (removed) - ub = *removed; - else - return {std::nullopt, false, /*resourceLimitsExceeded*/ true}; - ft->upperBound = ub; - } + removeType(arena, builtinTypes, ub, freeTy); if (follow(ub) != freeTy) emplaceType(asMutable(freeTy), ub); @@ -1339,17 +1272,14 @@ GeneralizationResult generalizeType( // // A <: 'b < C // - // We can approximately generalize this to the intersection of it's + // We can approximately generalize this to the intersection of its // bounds, taking care to avoid constructing a degenerate // union or intersection by clipping the free type from the upper // and lower bounds, then also cleaning the resulting intersection. - std::optional removedLb = removeType(arena, builtinTypes, ft->lowerBound, freeTy); - if (!removedLb) - return {std::nullopt, false, true}; - std::optional cleanedTy = removeType(arena, builtinTypes, arena->addType(IntersectionType{{*removedLb, ub}}), freeTy); - if (!cleanedTy) - return {std::nullopt, false, true}; - emplaceType(asMutable(freeTy), *cleanedTy); + removeType(arena, builtinTypes, ft->lowerBound, freeTy); + TypeId cleanedTy = arena->addType(IntersectionType{{ft->lowerBound, ub}}); + removeType(arena, builtinTypes, cleanedTy, freeTy); + emplaceType(asMutable(freeTy), cleanedTy); } else { @@ -1423,7 +1353,7 @@ std::optional generalize( FreeTypeSearcher fts{scope, cachedTypes}; fts.traverse(ty); - if (FFlag::LuauEagerGeneralization2) + if (FFlag::LuauEagerGeneralization3) { FunctionType* functionTy = getMutable(ty); auto pushGeneric = [&](TypeId t) @@ -1521,6 +1451,12 @@ struct GenericCounter : TypeVisitor Polarity polarity = Polarity::None; }; + // This traversal does need to walk into types multiple times because we + // care about generics that are only refererd to once. If a type is present + // more than once, however, we don't care exactly how many times, so we also + // track counts in our "seen set." + DenseHashMap seenCounts{nullptr}; + NotNull> cachedTypes; DenseHashMap generics{nullptr}; DenseHashMap genericPacks{nullptr}; @@ -1537,6 +1473,12 @@ struct GenericCounter : TypeVisitor if (ty->persistent) return false; + size_t& seenCount = seenCounts[ty]; + if (seenCount > 1) + return false; + + ++seenCount; + polarity = invert(polarity); traverse(ft.argTypes); polarity = invert(polarity); @@ -1550,6 +1492,11 @@ struct GenericCounter : TypeVisitor if (ty->persistent) return false; + size_t& seenCount = seenCounts[ty]; + if (seenCount > 1) + return false; + ++seenCount; + const Polarity previous = polarity; for (const auto& [_name, prop] : tt.props) @@ -1650,7 +1597,7 @@ void pruneUnnecessaryGenerics( TypeId ty ) { - if (!FFlag::LuauEagerGeneralization2) + if (!FFlag::LuauEagerGeneralization3) return; ty = follow(ty); @@ -1696,7 +1643,11 @@ void pruneUnnecessaryGenerics( for (const auto& [generic, state] : counter.generics) { if (state.count == 1 && state.polarity != Polarity::Mixed) + { + if (FFlag::LuauGeneralizationCannotMutateAcrossModules && arena.get() != generic->owningArena) + continue; emplaceType(asMutable(generic), builtinTypes->unknownType); + } } // Remove duplicates and types that aren't actually generics. diff --git a/Analysis/src/InferPolarity.cpp b/Analysis/src/InferPolarity.cpp index 82dbd3d4..73518c0d 100644 --- a/Analysis/src/InferPolarity.cpp +++ b/Analysis/src/InferPolarity.cpp @@ -5,7 +5,7 @@ #include "Luau/Scope.h" #include "Luau/VisitType.h" -LUAU_FASTFLAG(LuauEagerGeneralization2) +LUAU_FASTFLAG(LuauEagerGeneralization3) namespace Luau { @@ -133,7 +133,7 @@ struct InferPolarity : TypeVisitor template static void inferGenericPolarities_(NotNull arena, NotNull scope, TID ty) { - if (!FFlag::LuauEagerGeneralization2) + if (!FFlag::LuauEagerGeneralization3) return; InferPolarity infer{arena, scope}; diff --git a/Analysis/src/IostreamHelpers.cpp b/Analysis/src/IostreamHelpers.cpp index 591b1366..d8bdbb8f 100644 --- a/Analysis/src/IostreamHelpers.cpp +++ b/Analysis/src/IostreamHelpers.cpp @@ -1,8 +1,11 @@ // This file is part of the Luau programming language and is licensed under MIT License; see LICENSE.txt for details #include "Luau/IostreamHelpers.h" +#include "Luau/Error.h" #include "Luau/ToString.h" #include "Luau/TypePath.h" +#include + namespace Luau { @@ -250,6 +253,18 @@ static void errorToString(std::ostream& stream, const T& err) } else if constexpr (std::is_same_v) stream << "UnexpectedArrayLikeTableItem {}"; + else if constexpr (std::is_same_v) + stream << "CannotCheckDynamicStringFormatCalls {}"; + else if constexpr (std::is_same_v) + { + stream << "GenericTypeCountMismatch { subTyGenericCount = " << err.subTyGenericCount << ", superTyGenericCount = " << err.superTyGenericCount + << " }"; + } + else if constexpr (std::is_same_v) + { + stream << "GenericTypePackCountMismatch { subTyGenericPackCount = " << err.subTyGenericPackCount + << ", superTyGenericPackCount = " << err.superTyGenericPackCount << " }"; + } else static_assert(always_false_v, "Non-exhaustive type switch"); } diff --git a/Analysis/src/Normalize.cpp b/Analysis/src/Normalize.cpp index b80ee2b8..3a75b43d 100644 --- a/Analysis/src/Normalize.cpp +++ b/Analysis/src/Normalize.cpp @@ -21,6 +21,8 @@ LUAU_FASTINTVARIABLE(LuauNormalizeCacheLimit, 100000) LUAU_FASTINTVARIABLE(LuauNormalizeIntersectionLimit, 200) LUAU_FASTINTVARIABLE(LuauNormalizeUnionLimit, 100) LUAU_FASTFLAG(LuauSolverV2) +LUAU_FASTFLAGVARIABLE(LuauNormalizationIntersectTablesPreservesExternTypes) +LUAU_FASTFLAGVARIABLE(LuauNormalizationReorderFreeTypeIntersect) namespace Luau { @@ -2897,6 +2899,24 @@ NormalizationResult Normalizer::intersectNormals(NormalizedType& here, const Nor if (here.functions.parts.size() * there.functions.parts.size() >= size_t(FInt::LuauNormalizeIntersectionLimit)) return NormalizationResult::HitLimits; + if (FFlag::LuauNormalizationReorderFreeTypeIntersect) + { + for (auto& [tyvar, inter] : there.tyvars) + { + int index = tyvarIndex(tyvar); + if (ignoreSmallerTyvars < index) + { + auto [found, fresh] = here.tyvars.emplace(tyvar, std::make_unique(NormalizedType{builtinTypes})); + if (fresh) + { + NormalizationResult res = unionNormals(*found->second, here, index); + if (res != NormalizationResult::True) + return res; + } + } + } + } + here.booleans = intersectionOfBools(here.booleans, there.booleans); intersectExternTypes(here.externTypes, there.externTypes); @@ -2909,20 +2929,24 @@ NormalizationResult Normalizer::intersectNormals(NormalizedType& here, const Nor intersectFunctions(here.functions, there.functions); intersectTables(here.tables, there.tables); - for (auto& [tyvar, inter] : there.tyvars) + if (!FFlag::LuauNormalizationReorderFreeTypeIntersect) { - int index = tyvarIndex(tyvar); - if (ignoreSmallerTyvars < index) + for (auto& [tyvar, inter] : there.tyvars) { - auto [found, fresh] = here.tyvars.emplace(tyvar, std::make_unique(NormalizedType{builtinTypes})); - if (fresh) + int index = tyvarIndex(tyvar); + if (ignoreSmallerTyvars < index) { - NormalizationResult res = unionNormals(*found->second, here, index); - if (res != NormalizationResult::True) - return res; + auto [found, fresh] = here.tyvars.emplace(tyvar, std::make_unique(NormalizedType{builtinTypes})); + if (fresh) + { + NormalizationResult res = unionNormals(*found->second, here, index); + if (res != NormalizationResult::True) + return res; + } } } } + for (auto it = here.tyvars.begin(); it != here.tyvars.end();) { TypeId tyvar = it->first; @@ -3016,10 +3040,22 @@ NormalizationResult Normalizer::intersectNormalWithTy( } else if (get(there) || get(there)) { - TypeIds tables = std::move(here.tables); - clearNormal(here); - intersectTablesWithTable(tables, there, seenTablePropPairs, seenSetTypes); - here.tables = std::move(tables); + if (FFlag::LuauSolverV2 && FFlag::LuauNormalizationIntersectTablesPreservesExternTypes) + { + NormalizedExternType externTypes = std::move(here.externTypes); + TypeIds tables = std::move(here.tables); + clearNormal(here); + intersectTablesWithTable(tables, there, seenTablePropPairs, seenSetTypes); + here.tables = std::move(tables); + here.externTypes = std::move(externTypes); + } + else + { + TypeIds tables = std::move(here.tables); + clearNormal(here); + intersectTablesWithTable(tables, there, seenTablePropPairs, seenSetTypes); + here.tables = std::move(tables); + } } else if (get(there)) { diff --git a/Analysis/src/Simplify.cpp b/Analysis/src/Simplify.cpp index f9d48a7d..915550eb 100644 --- a/Analysis/src/Simplify.cpp +++ b/Analysis/src/Simplify.cpp @@ -16,8 +16,6 @@ LUAU_FASTINT(LuauTypeReductionRecursionLimit) LUAU_FASTFLAG(LuauSolverV2) LUAU_DYNAMIC_FASTINTVARIABLE(LuauSimplificationComplexityLimit, 8) -LUAU_FASTFLAGVARIABLE(LuauSimplificationRecheckAssumption) -LUAU_FASTFLAGVARIABLE(LuauOptimizeFalsyAndTruthyIntersect) LUAU_FASTFLAGVARIABLE(LuauSimplificationTableExternType) namespace Luau @@ -771,12 +769,9 @@ TypeId TypeSimplifier::intersectUnionWithType(TypeId left, TypeId right) newParts.insert(simplified); - if (FFlag::LuauSimplificationRecheckAssumption) - { - // Initial combination size check could not predict nested union iteration - if (newParts.size() > maxSize) - return arena->addType(IntersectionType{{left, right}}); - } + // Initial combination size check could not predict nested union iteration + if (newParts.size() > maxSize) + return arena->addType(IntersectionType{{left, right}}); } if (!changed) @@ -818,12 +813,9 @@ TypeId TypeSimplifier::intersectUnions(TypeId left, TypeId right) newParts.insert(simplified); - if (FFlag::LuauSimplificationRecheckAssumption) - { - // Initial combination size check could not predict nested union iteration - if (newParts.size() > maxSize) - return arena->addType(IntersectionType{{left, right}}); - } + // Initial combination size check could not predict nested union iteration + if (newParts.size() > maxSize) + return arena->addType(IntersectionType{{left, right}}); } } @@ -1310,24 +1302,21 @@ std::optional TypeSimplifier::basicIntersect(TypeId left, TypeId right) return std::nullopt; } - if (FFlag::LuauOptimizeFalsyAndTruthyIntersect) - { - if (isTruthyType(left)) - if (auto res = basicIntersectWithTruthy(right)) - return res; + if (isTruthyType(left)) + if (auto res = basicIntersectWithTruthy(right)) + return res; - if (isTruthyType(right)) - if (auto res = basicIntersectWithTruthy(left)) - return res; + if (isTruthyType(right)) + if (auto res = basicIntersectWithTruthy(left)) + return res; - if (isFalsyType(left)) - if (auto res = basicIntersectWithFalsy(right)) - return res; + if (isFalsyType(left)) + if (auto res = basicIntersectWithFalsy(right)) + return res; - if (isFalsyType(right)) - if (auto res = basicIntersectWithFalsy(left)) - return res; - } + if (isFalsyType(right)) + if (auto res = basicIntersectWithFalsy(left)) + return res; Relation relation = relate(left, right); if (left == right || Relation::Coincident == relation) diff --git a/Analysis/src/Subtyping.cpp b/Analysis/src/Subtyping.cpp index 1f7fd49b..5fa81dda 100644 --- a/Analysis/src/Subtyping.cpp +++ b/Analysis/src/Subtyping.cpp @@ -8,7 +8,6 @@ #include "Luau/RecursionCounter.h" #include "Luau/Scope.h" #include "Luau/Substitution.h" -#include "Luau/ToString.h" #include "Luau/TxnLog.h" #include "Luau/Type.h" #include "Luau/TypeArena.h" @@ -19,9 +18,9 @@ LUAU_FASTFLAGVARIABLE(DebugLuauSubtypingCheckPathValidity) LUAU_FASTINTVARIABLE(LuauSubtypingReasoningLimit, 100) -LUAU_FASTFLAGVARIABLE(LuauSubtypeGenericsAndNegations) LUAU_FASTFLAG(LuauClipVariadicAnysFromArgsToGenericFuncs2) -LUAU_FASTFLAG(LuauEagerGeneralization2) +LUAU_FASTFLAGVARIABLE(LuauSubtypingCheckFunctionGenericCounts) +LUAU_FASTFLAG(LuauEagerGeneralization3) namespace Luau { @@ -669,27 +668,26 @@ SubtypingResult Subtyping::isCovariantWith(SubtypingEnvironment& env, TypeId sub result = {false}; else if (get(subTy)) result = {true}; - else if (auto subGeneric = get(subTy); FFlag::LuauSubtypeGenericsAndNegations && subGeneric && variance == Variance::Covariant) + else if (auto subGeneric = get(subTy); subGeneric && variance == Variance::Covariant) { bool ok = bindGeneric(env, subTy, superTy); result.isSubtype = ok; result.isCacheable = false; } - else if (auto superGeneric = get(superTy); - FFlag::LuauSubtypeGenericsAndNegations && superGeneric && variance == Variance::Contravariant) + else if (auto superGeneric = get(superTy); superGeneric && variance == Variance::Contravariant) { bool ok = bindGeneric(env, subTy, superTy); result.isSubtype = ok; result.isCacheable = false; } - else if (auto pair = get2(subTy, superTy); FFlag::LuauEagerGeneralization2 && pair) + else if (auto pair = get2(subTy, superTy); FFlag::LuauEagerGeneralization3 && pair) { // Any two free types are potentially subtypes of one another because // both of them could be narrowed to never. result = {true}; result.assumedConstraints.emplace_back(SubtypeConstraint{subTy, superTy}); } - else if (auto superFree = get(superTy); superFree && FFlag::LuauEagerGeneralization2) + else if (auto superFree = get(superTy); superFree && FFlag::LuauEagerGeneralization3) { // Given SubTy <: (LB <: SuperTy <: UB) // @@ -704,7 +702,7 @@ SubtypingResult Subtyping::isCovariantWith(SubtypingEnvironment& env, TypeId sub if (result.isSubtype) result.assumedConstraints.emplace_back(SubtypeConstraint{subTy, superTy}); } - else if (auto subFree = get(subTy); subFree && FFlag::LuauEagerGeneralization2) + else if (auto subFree = get(subTy); subFree && FFlag::LuauEagerGeneralization3) { // Given (LB <: SubTy <: UB) <: SuperTy // @@ -762,19 +760,6 @@ SubtypingResult Subtyping::isCovariantWith(SubtypingEnvironment& env, TypeId sub result = isCovariantWith(env, subTy, superTypeFunctionInstance, scope); } - else if (auto subGeneric = get(subTy); !FFlag::LuauSubtypeGenericsAndNegations && subGeneric && variance == Variance::Covariant) - { - bool ok = bindGeneric(env, subTy, superTy); - result.isSubtype = ok; - result.isCacheable = false; - } - else if (auto superGeneric = get(superTy); - !FFlag::LuauSubtypeGenericsAndNegations && superGeneric && variance == Variance::Contravariant) - { - bool ok = bindGeneric(env, subTy, superTy); - result.isSubtype = ok; - result.isCacheable = false; - } else if (auto p = get2(subTy, superTy)) result = isCovariantWith(env, p, scope); else if (auto p = get2(subTy, superTy)) @@ -1452,7 +1437,7 @@ SubtypingResult Subtyping::isCovariantWith(SubtypingEnvironment& env, const Tabl { SubtypingResult result{true}; - if (FFlag::LuauEagerGeneralization2) + if (FFlag::LuauEagerGeneralization3) { if (subTable->props.empty() && !subTable->indexer && subTable->state == TableState::Sealed && superTable->indexer) { @@ -1508,7 +1493,7 @@ SubtypingResult Subtyping::isCovariantWith(SubtypingEnvironment& env, const Tabl { if (subTable->indexer) result.andAlso(isInvariantWith(env, *subTable->indexer, *superTable->indexer, scope)); - else if (FFlag::LuauEagerGeneralization2 && subTable->state != TableState::Sealed) + else if (FFlag::LuauEagerGeneralization3 && subTable->state != TableState::Sealed) { // As above, we assume that {| |} <: {T} because the unsealed table // on the left will eventually gain the necessary indexer. @@ -1620,6 +1605,21 @@ SubtypingResult Subtyping::isCovariantWith( result.andAlso(isCovariantWith(env, subFunction->retTypes, superFunction->retTypes, scope).withBothComponent(TypePath::PackField::Returns)); + if (FFlag::LuauSubtypingCheckFunctionGenericCounts) + { + if (*subFunction->argTypes == *superFunction->argTypes && *subFunction->retTypes == *superFunction->retTypes) + { + if (superFunction->generics.size() != subFunction->generics.size()) + result.andAlso({false}).withError( + TypeError{scope->location, GenericTypeCountMismatch{superFunction->generics.size(), subFunction->generics.size()}} + ); + if (superFunction->genericPacks.size() != subFunction->genericPacks.size()) + result.andAlso({false}).withError( + TypeError{scope->location, GenericTypePackCountMismatch{superFunction->genericPacks.size(), subFunction->genericPacks.size()}} + ); + } + } + return result; } diff --git a/Analysis/src/TableLiteralInference.cpp b/Analysis/src/TableLiteralInference.cpp index 1285f7a3..8ed7558c 100644 --- a/Analysis/src/TableLiteralInference.cpp +++ b/Analysis/src/TableLiteralInference.cpp @@ -13,7 +13,6 @@ #include "Luau/TypeUtils.h" #include "Luau/Unifier2.h" -LUAU_FASTFLAGVARIABLE(LuauBidirectionalInferenceElideAssert) LUAU_FASTFLAG(LuauTableLiteralSubtypeSpecificCheck) namespace Luau @@ -297,9 +296,6 @@ TypeId matchLiteralType( } else if (item.kind == AstExprTable::Item::List) { - if (!FFlag::LuauBidirectionalInferenceElideAssert) - LUAU_ASSERT(tableTy->indexer); - if (expectedTableTy->indexer) { const TypeId* propTy = astTypes->find(item.value); diff --git a/Analysis/src/ToString.cpp b/Analysis/src/ToString.cpp index 031d0b7c..6a0b4c58 100644 --- a/Analysis/src/ToString.cpp +++ b/Analysis/src/ToString.cpp @@ -22,7 +22,6 @@ LUAU_FASTFLAGVARIABLE(LuauEnableDenseTableAlias) LUAU_FASTFLAG(LuauSolverV2) -LUAU_FASTFLAGVARIABLE(LuauStringPartLengthLimit) /* * Enables increasing levels of verbosity for Luau type names when stringifying. @@ -959,34 +958,21 @@ struct TypeStringifier if (needParens) state.emit(")"); - if (FFlag::LuauStringPartLengthLimit) - resultsLength += state.result.name.length(); - + resultsLength += state.result.name.length(); results.push_back(std::move(state.result.name)); state.result.name = std::move(saved); - if (FFlag::LuauStringPartLengthLimit) - { - lengthLimitHit = state.opts.maxTypeLength > 0 && resultsLength > state.opts.maxTypeLength; + lengthLimitHit = state.opts.maxTypeLength > 0 && resultsLength > state.opts.maxTypeLength; - if (lengthLimitHit) - break; - } + if (lengthLimitHit) + break; } state.unsee(&uv); - if (FFlag::LuauStringPartLengthLimit) - { - if (!lengthLimitHit && !FFlag::DebugLuauToStringNoLexicalSort) - std::sort(results.begin(), results.end()); - } - else - { - if (!FFlag::DebugLuauToStringNoLexicalSort) - std::sort(results.begin(), results.end()); - } + if (!lengthLimitHit && !FFlag::DebugLuauToStringNoLexicalSort) + std::sort(results.begin(), results.end()); if (optional && results.size() > 1) state.emit("("); @@ -1049,34 +1035,21 @@ struct TypeStringifier if (needParens) state.emit(")"); - if (FFlag::LuauStringPartLengthLimit) - resultsLength += state.result.name.length(); - + resultsLength += state.result.name.length(); results.push_back(std::move(state.result.name)); state.result.name = std::move(saved); - if (FFlag::LuauStringPartLengthLimit) - { - lengthLimitHit = state.opts.maxTypeLength > 0 && resultsLength > state.opts.maxTypeLength; + lengthLimitHit = state.opts.maxTypeLength > 0 && resultsLength > state.opts.maxTypeLength; - if (lengthLimitHit) - break; - } + if (lengthLimitHit) + break; } state.unsee(&uv); - if (FFlag::LuauStringPartLengthLimit) - { - if (!lengthLimitHit && !FFlag::DebugLuauToStringNoLexicalSort) - std::sort(results.begin(), results.end()); - } - else - { - if (!FFlag::DebugLuauToStringNoLexicalSort) - std::sort(results.begin(), results.end()); - } + if (!lengthLimitHit && !FFlag::DebugLuauToStringNoLexicalSort) + std::sort(results.begin(), results.end()); bool first = true; bool shouldPlaceOnNewlines = results.size() > state.opts.compositeTypesSingleLineLimit || isOverloadedFunction(ty); diff --git a/Analysis/src/Type.cpp b/Analysis/src/Type.cpp index 2b42bbbf..0f2619f1 100644 --- a/Analysis/src/Type.cpp +++ b/Analysis/src/Type.cpp @@ -23,10 +23,13 @@ LUAU_FASTFLAG(DebugLuauFreezeArena) +LUAU_FASTFLAG(LuauSolverV2) + LUAU_FASTINTVARIABLE(LuauTypeMaximumStringifierLength, 500) LUAU_FASTINTVARIABLE(LuauTableTypeMaximumStringifierLength, 0) LUAU_FASTINT(LuauTypeInferRecursionLimit) LUAU_FASTFLAG(LuauInstantiateInSubtyping) +LUAU_FASTFLAG(LuauSubtypingCheckFunctionGenericCounts) namespace Luau { @@ -831,7 +834,25 @@ bool areEqual(SeenSet& seen, const TableType& lhs, const TableType& rhs) if (l->first != r->first) return false; - if (!areEqual(seen, *l->second.type(), *r->second.type())) + if (FFlag::LuauSolverV2 && FFlag::LuauSubtypingCheckFunctionGenericCounts) + { + if (l->second.readTy && r->second.readTy) + { + if (!areEqual(seen, **l->second.readTy, **r->second.readTy)) + return false; + } + else if (l->second.readTy || r->second.readTy) + return false; + + if (l->second.writeTy && r->second.writeTy) + { + if (!areEqual(seen, **l->second.writeTy, **r->second.writeTy)) + return false; + } + else if (l->second.writeTy || r->second.writeTy) + return false; + } + else if (!areEqual(seen, *l->second.type(), *r->second.type())) return false; ++l; ++r; diff --git a/Analysis/src/TypeChecker2.cpp b/Analysis/src/TypeChecker2.cpp index 9ea64669..046684cb 100644 --- a/Analysis/src/TypeChecker2.cpp +++ b/Analysis/src/TypeChecker2.cpp @@ -30,7 +30,6 @@ LUAU_FASTFLAG(DebugLuauMagicTypes) -LUAU_FASTFLAGVARIABLE(LuauTypeCheckerAcceptNumberConcats) LUAU_FASTFLAGVARIABLE(LuauTypeCheckerStricterIndexCheck) LUAU_FASTFLAG(LuauStoreReturnTypesAsPackOnAst) LUAU_FASTFLAG(LuauEnableWriteOnlyProperties) @@ -38,6 +37,7 @@ LUAU_FASTFLAG(LuauNewNonStrictFixGenericTypePacks) LUAU_FASTFLAGVARIABLE(LuauReportSubtypingErrors) LUAU_FASTFLAGVARIABLE(LuauSkipMalformedTypeAliases) LUAU_FASTFLAGVARIABLE(LuauTableLiteralSubtypeSpecificCheck) +LUAU_FASTFLAG(LuauSubtypingCheckFunctionGenericCounts) namespace Luau { @@ -1409,14 +1409,18 @@ void TypeChecker2::visit(AstExprConstantBool* expr) const TypeId inferredType = lookupType(expr); NotNull scope{findInnermostScope(expr->location)}; - const SubtypingResult r = subtyping->isSubtype(bestType, inferredType, scope); + SubtypingResult r = subtyping->isSubtype(bestType, inferredType, scope); if (FFlag::LuauReportSubtypingErrors) { if (!isErrorSuppressing(expr->location, inferredType)) { if (!r.isSubtype) reportError(TypeMismatch{inferredType, bestType}, expr->location); - + if (FFlag::LuauSubtypingCheckFunctionGenericCounts) + { + for (auto& e : r.errors) + e.location = expr->location; + } reportErrors(r.errors); } } @@ -1447,14 +1451,18 @@ void TypeChecker2::visit(AstExprConstantString* expr) const TypeId inferredType = lookupType(expr); NotNull scope{findInnermostScope(expr->location)}; - const SubtypingResult r = subtyping->isSubtype(bestType, inferredType, scope); + SubtypingResult r = subtyping->isSubtype(bestType, inferredType, scope); if (FFlag::LuauReportSubtypingErrors) { if (!isErrorSuppressing(expr->location, inferredType)) { if (!r.isSubtype) reportError(TypeMismatch{inferredType, bestType}, expr->location); - + if (FFlag::LuauSubtypingCheckFunctionGenericCounts) + { + for (auto& e : r.errors) + e.location = expr->location; + } reportErrors(r.errors); } } @@ -1533,7 +1541,12 @@ void TypeChecker2::visitCall(AstExprCall* call) if (FFlag::LuauReportSubtypingErrors) { if (!isErrorSuppressing(call->location, *selectedOverloadTy)) - reportErrors(std::move(result.errors)); + if (FFlag::LuauSubtypingCheckFunctionGenericCounts) + { + for (auto& e : result.errors) + e.location = call->location; + } + reportErrors(std::move(result.errors)); } if (result.normalizationTooComplex) @@ -2359,18 +2372,9 @@ TypeId TypeChecker2::visit(AstExprBinary* expr, AstNode* overrideKey) return builtinTypes->numberType; case AstExprBinary::Op::Concat: { - if (FFlag::LuauTypeCheckerAcceptNumberConcats) - { - const TypeId numberOrString = module->internalTypes.addType(UnionType{{builtinTypes->numberType, builtinTypes->stringType}}); - testIsSubtype(leftType, numberOrString, expr->left->location); - testIsSubtype(rightType, numberOrString, expr->right->location); - } - else - { - testIsSubtype(leftType, builtinTypes->stringType, expr->left->location); - testIsSubtype(rightType, builtinTypes->stringType, expr->right->location); - } - + const TypeId numberOrString = module->internalTypes.addType(UnionType{{builtinTypes->numberType, builtinTypes->stringType}}); + testIsSubtype(leftType, numberOrString, expr->left->location); + testIsSubtype(rightType, numberOrString, expr->right->location); return builtinTypes->stringType; } case AstExprBinary::Op::CompareGe: @@ -3113,7 +3117,12 @@ bool TypeChecker2::testIsSubtype(TypeId subTy, TypeId superTy, Location location if (FFlag::LuauReportSubtypingErrors) { if (!isErrorSuppressing(location, subTy)) - reportErrors(std::move(r.errors)); + if (FFlag::LuauSubtypingCheckFunctionGenericCounts) + { + for (auto& e : r.errors) + e.location = location; + } + reportErrors(std::move(r.errors)); } if (r.normalizationTooComplex) @@ -3133,7 +3142,12 @@ bool TypeChecker2::testIsSubtype(TypePackId subTy, TypePackId superTy, Location if (FFlag::LuauReportSubtypingErrors) { if (!isErrorSuppressing(location, subTy)) - reportErrors(std::move(r.errors)); + if (FFlag::LuauSubtypingCheckFunctionGenericCounts) + { + for (auto& e : r.errors) + e.location = location; + } + reportErrors(std::move(r.errors)); } if (r.normalizationTooComplex) diff --git a/Analysis/src/TypeFunction.cpp b/Analysis/src/TypeFunction.cpp index 6a6a92a4..bbfeed9d 100644 --- a/Analysis/src/TypeFunction.cpp +++ b/Analysis/src/TypeFunction.cpp @@ -48,16 +48,14 @@ LUAU_DYNAMIC_FASTINTVARIABLE(LuauTypeFamilyApplicationCartesianProductLimit, 5'0 LUAU_DYNAMIC_FASTINTVARIABLE(LuauTypeFamilyUseGuesserDepth, -1); LUAU_FASTFLAG(DebugLuauEqSatSimplification) -LUAU_FASTFLAG(LuauEagerGeneralization2) -LUAU_FASTFLAG(LuauEagerGeneralization2) +LUAU_FASTFLAG(LuauEagerGeneralization3) +LUAU_FASTFLAG(LuauEagerGeneralization3) LUAU_FASTFLAGVARIABLE(DebugLuauLogTypeFamilies) -LUAU_FASTFLAG(LuauOptimizeFalsyAndTruthyIntersect) LUAU_FASTFLAGVARIABLE(LuauNarrowIntersectionNevers) -LUAU_FASTFLAGVARIABLE(LuauRefineWaitForBlockedTypesInTarget) -LUAU_FASTFLAGVARIABLE(LuauNoMoreInjectiveTypeFunctions) LUAU_FASTFLAGVARIABLE(LuauNotAllBinaryTypeFunsHaveDefaults) LUAU_FASTFLAG(LuauUserTypeFunctionAliases) +LUAU_FASTFLAG(LuauUpdateGetMetatableTypeSignature) namespace Luau { @@ -285,7 +283,7 @@ struct TypeFunctionReducer } else if (is(ty)) { - if (FFlag::LuauEagerGeneralization2) + if (FFlag::LuauEagerGeneralization3) return SkipTestResult::Generic; else return SkipTestResult::Irreducible; @@ -307,7 +305,7 @@ struct TypeFunctionReducer } else if (is(ty)) { - if (FFlag::LuauEagerGeneralization2) + if (FFlag::LuauEagerGeneralization3) return SkipTestResult::Generic; else return SkipTestResult::Irreducible; @@ -1261,7 +1259,7 @@ TypeFunctionReductionResult unmTypeFunction( if (isPending(operandTy, ctx->solver)) return {std::nullopt, Reduction::MaybeOk, {operandTy}, {}}; - if (FFlag::LuauEagerGeneralization2) + if (FFlag::LuauEagerGeneralization3) operandTy = follow(operandTy); std::shared_ptr normTy = ctx->normalizer->normalize(operandTy); @@ -1858,7 +1856,7 @@ TypeFunctionReductionResult orTypeFunction( return {rhsTy, Reduction::MaybeOk, {}, {}}; // check to see if both operand types are resolved enough, and wait to reduce if not - if (FFlag::LuauEagerGeneralization2) + if (FFlag::LuauEagerGeneralization3) { if (is(lhsTy)) return {std::nullopt, Reduction::MaybeOk, {lhsTy}, {}}; @@ -1905,7 +1903,7 @@ static TypeFunctionReductionResult comparisonTypeFunction( if (lhsTy == instance || rhsTy == instance) return {ctx->builtins->neverType, Reduction::MaybeOk, {}, {}}; - if (FFlag::LuauEagerGeneralization2) + if (FFlag::LuauEagerGeneralization3) { if (is(lhsTy)) return {std::nullopt, Reduction::MaybeOk, {lhsTy}, {}}; @@ -1938,16 +1936,6 @@ static TypeFunctionReductionResult comparisonTypeFunction( emplaceType(asMutable(lhsTy), ctx->builtins->numberType); else if (rhsFree && isNumber(lhsTy)) emplaceType(asMutable(rhsTy), ctx->builtins->numberType); - else if (!FFlag::LuauNoMoreInjectiveTypeFunctions && lhsFree && ctx->normalizer->isInhabited(rhsTy) != NormalizationResult::False) - { - auto c1 = ctx->pushConstraint(EqualityConstraint{lhsTy, rhsTy}); - const_cast(ctx->constraint)->dependencies.emplace_back(c1); - } - else if (!FFlag::LuauNoMoreInjectiveTypeFunctions && rhsFree && ctx->normalizer->isInhabited(lhsTy) != NormalizationResult::False) - { - auto c1 = ctx->pushConstraint(EqualityConstraint{rhsTy, lhsTy}); - const_cast(ctx->constraint)->dependencies.emplace_back(c1); - } } // The above might have caused the operand types to be rebound, we need to follow them again @@ -2279,7 +2267,7 @@ TypeFunctionReductionResult refineTypeFunction( for (size_t i = 1; i < typeParams.size(); i++) discriminantTypes.push_back(follow(typeParams.at(i))); - const bool targetIsPending = FFlag::LuauEagerGeneralization2 ? is(targetTy) + const bool targetIsPending = FFlag::LuauEagerGeneralization3 ? is(targetTy) : isPending(targetTy, ctx->solver); // check to see if both operand types are resolved enough, and wait to reduce if not @@ -2294,16 +2282,13 @@ TypeFunctionReductionResult refineTypeFunction( } } - if (FFlag::LuauRefineWaitForBlockedTypesInTarget) - { - // If we have a blocked type in the target, we *could* potentially - // refine it, but more likely we end up with some type explosion in - // normalization. - FindRefinementBlockers frb; - frb.traverse(targetTy); - if (!frb.found.empty()) - return {std::nullopt, Reduction::MaybeOk, {frb.found.begin(), frb.found.end()}, {}}; - } + // If we have a blocked type in the target, we *could* potentially + // refine it, but more likely we end up with some type explosion in + // normalization. + FindRefinementBlockers frb; + frb.traverse(targetTy); + if (!frb.found.empty()) + return {std::nullopt, Reduction::MaybeOk, {frb.found.begin(), frb.found.end()}, {}}; // Refine a target type and a discriminant one at a time. // Returns result : TypeId, toBlockOn : vector @@ -2353,59 +2338,43 @@ TypeFunctionReductionResult refineTypeFunction( } } - if (FFlag::LuauOptimizeFalsyAndTruthyIntersect) + // If the target type is a table, then simplification already implements the logic to deal with refinements properly since the + // type of the discriminant is guaranteed to only ever be an (arbitrarily-nested) table of a single property type. + // We also fire for simple discriminants such as false? and ~(false?): the falsy and truthy types respectively + // NOTE: It would be nice to be able to do a simple intersection for something like: + // + // { a: A, b: B, ... } & { x: X } + // + if (is(target) || isSimpleDiscriminant(discriminant)) { - // If the target type is a table, then simplification already implements the logic to deal with refinements properly since the - // type of the discriminant is guaranteed to only ever be an (arbitrarily-nested) table of a single property type. - // We also fire for simple discriminants such as false? and ~(false?): the falsy and truthy types respectively - // NOTE: It would be nice to be able to do a simple intersection for something like: - // - // { a: A, b: B, ... } & { x: X } - // - if (is(target) || isSimpleDiscriminant(discriminant)) + SimplifyResult result = simplifyIntersection(ctx->builtins, ctx->arena, target, discriminant); + if (FFlag::LuauEagerGeneralization3) { - SimplifyResult result = simplifyIntersection(ctx->builtins, ctx->arena, target, discriminant); - if (FFlag::LuauEagerGeneralization2) + // Simplification considers free and generic types to be + // 'blocking', but that's not suitable for refine<>. + // + // If we are only blocked on those types, we consider + // the simplification a success and reduce. + if (std::all_of( + begin(result.blockedTypes), + end(result.blockedTypes), + [](auto&& v) + { + return is(follow(v)); + } + )) { - // Simplification considers free and generic types to be - // 'blocking', but that's not suitable for refine<>. - // - // If we are only blocked on those types, we consider - // the simplification a success and reduce. - if (std::all_of( - begin(result.blockedTypes), - end(result.blockedTypes), - [](auto&& v) - { - return is(follow(v)); - } - )) - { - return {result.result, {}}; - } - else - return {nullptr, {result.blockedTypes.begin(), result.blockedTypes.end()}}; + return {result.result, {}}; } else - { - if (!result.blockedTypes.empty()) - return {nullptr, {result.blockedTypes.begin(), result.blockedTypes.end()}}; - } - return {result.result, {}}; + return {nullptr, {result.blockedTypes.begin(), result.blockedTypes.end()}}; } - } - else - { - // If the target type is a table, then simplification already implements the logic to deal with refinements properly since the - // type of the discriminant is guaranteed to only ever be an (arbitrarily-nested) table of a single property type. - if (get(target)) + else { - SimplifyResult result = simplifyIntersection(ctx->builtins, ctx->arena, target, discriminant); if (!result.blockedTypes.empty()) return {nullptr, {result.blockedTypes.begin(), result.blockedTypes.end()}}; - - return {result.result, {}}; } + return {result.result, {}}; } @@ -3333,7 +3302,7 @@ static TypeFunctionReductionResult getmetatableHelper(TypeId targetTy, c { targetTy = follow(targetTy); - std::optional metatable = std::nullopt; + std::optional result = std::nullopt; bool erroneous = true; if (auto table = get(targetTy)) @@ -3341,19 +3310,19 @@ static TypeFunctionReductionResult getmetatableHelper(TypeId targetTy, c if (auto mt = get(targetTy)) { - metatable = mt->metatable; + result = mt->metatable; erroneous = false; } if (auto clazz = get(targetTy)) { - metatable = clazz->metatable; + result = clazz->metatable; erroneous = false; } if (auto primitive = get(targetTy)) { - metatable = primitive->metatable; + result = primitive->metatable; erroneous = false; } @@ -3362,11 +3331,18 @@ static TypeFunctionReductionResult getmetatableHelper(TypeId targetTy, c if (get(singleton)) { auto primitiveString = get(ctx->builtins->stringType); - metatable = primitiveString->metatable; + result = primitiveString->metatable; } erroneous = false; } + if (FFlag::LuauUpdateGetMetatableTypeSignature && get(targetTy)) + { + // getmetatable ~ any + result = targetTy; + erroneous = false; + } + if (erroneous) return {std::nullopt, Reduction::Erroneous, {}, {}}; @@ -3379,8 +3355,8 @@ static TypeFunctionReductionResult getmetatableHelper(TypeId targetTy, c if (metatableMetamethod) return {metatableMetamethod, Reduction::MaybeOk, {}, {}}; - if (metatable) - return {metatable, Reduction::MaybeOk, {}, {}}; + if (result) + return {result, Reduction::MaybeOk, {}, {}}; return {ctx->builtins->nilType, Reduction::MaybeOk, {}, {}}; } @@ -3428,16 +3404,34 @@ TypeFunctionReductionResult getmetatableTypeFunction( std::vector parts{}; parts.reserve(it->parts.size()); + bool erroredWithUnknown = false; + for (auto part : it->parts) { TypeFunctionReductionResult result = getmetatableHelper(part, location, ctx); if (!result.result) - return result; + { + // Don't immediately error if part is unknown + if (FFlag::LuauUpdateGetMetatableTypeSignature && get(follow(part))) + { + erroredWithUnknown = true; + continue; + } + else + return result; + } parts.push_back(*result.result); } + // If all parts are unknown, return erroneous reduction + if (FFlag::LuauUpdateGetMetatableTypeSignature && erroredWithUnknown && parts.empty()) + return {std::nullopt, Reduction::Erroneous, {}, {}}; + + if (FFlag::LuauUpdateGetMetatableTypeSignature && parts.size() == 1) + return {parts.front(), Reduction::MaybeOk, {}, {}}; + return {ctx->arena->addType(IntersectionType{std::move(parts)}), Reduction::MaybeOk, {}, {}}; } @@ -3495,7 +3489,7 @@ BuiltinTypeFunctions::BuiltinTypeFunctions() , ltFunc{"lt", ltTypeFunction} , leFunc{"le", leTypeFunction} , eqFunc{"eq", eqTypeFunction} - , refineFunc{"refine", refineTypeFunction, /*canReduceGenerics*/ FFlag::LuauEagerGeneralization2} + , refineFunc{"refine", refineTypeFunction, /*canReduceGenerics*/ FFlag::LuauEagerGeneralization3} , singletonFunc{"singleton", singletonTypeFunction} , unionFunc{"union", unionTypeFunction} , intersectFunc{"intersect", intersectTypeFunction} diff --git a/Analysis/src/TypeIds.cpp b/Analysis/src/TypeIds.cpp index f4189a68..b81ecf6a 100644 --- a/Analysis/src/TypeIds.cpp +++ b/Analysis/src/TypeIds.cpp @@ -150,4 +150,11 @@ bool TypeIds::operator==(const TypeIds& there) const return true; } +std::vector TypeIds::take() +{ + hash = 0; + types.clear(); + return std::move(order); +} + } diff --git a/Analysis/src/TypeUtils.cpp b/Analysis/src/TypeUtils.cpp index e2d2b028..be3a002f 100644 --- a/Analysis/src/TypeUtils.cpp +++ b/Analysis/src/TypeUtils.cpp @@ -12,7 +12,7 @@ #include LUAU_FASTFLAG(LuauSolverV2) -LUAU_FASTFLAG(LuauEagerGeneralization2) +LUAU_FASTFLAG(LuauEagerGeneralization3) LUAU_FASTFLAGVARIABLE(LuauErrorSuppressionTypeFunctionArgs) namespace Luau @@ -306,7 +306,7 @@ TypePack extendTypePack( TypePack newPack; newPack.tail = arena.freshTypePack(ftp->scope, ftp->polarity); - if (FFlag::LuauEagerGeneralization2) + if (FFlag::LuauEagerGeneralization3) trackInteriorFreeTypePack(ftp->scope, *newPack.tail); if (FFlag::LuauSolverV2) @@ -588,7 +588,7 @@ void trackInteriorFreeType(Scope* scope, TypeId ty) void trackInteriorFreeTypePack(Scope* scope, TypePackId tp) { LUAU_ASSERT(tp); - if (!FFlag::LuauEagerGeneralization2) + if (!FFlag::LuauEagerGeneralization3) return; for (; scope; scope = scope->parent.get()) @@ -695,5 +695,14 @@ bool isRecord(const AstExprTable::Item& item) return false; } +AstExpr* unwrapGroup(AstExpr* expr) +{ + while (auto group = expr->as()) + expr = group->expr; + + return expr; +} + + } // namespace Luau diff --git a/Analysis/src/Unifier2.cpp b/Analysis/src/Unifier2.cpp index d108d7c0..5ed565fe 100644 --- a/Analysis/src/Unifier2.cpp +++ b/Analysis/src/Unifier2.cpp @@ -19,7 +19,7 @@ LUAU_FASTINT(LuauTypeInferRecursionLimit) LUAU_FASTFLAG(LuauEnableWriteOnlyProperties) -LUAU_FASTFLAG(LuauEagerGeneralization2) +LUAU_FASTFLAG(LuauEagerGeneralization3) namespace Luau { @@ -329,9 +329,9 @@ bool Unifier2::unify(TypeId subTy, const FunctionType* superFn) for (TypePackId genericPack : subFn->genericPacks) { - if (FFlag::LuauEagerGeneralization2) + if (FFlag::LuauEagerGeneralization3) { - if (FFlag::LuauEagerGeneralization2) + if (FFlag::LuauEagerGeneralization3) genericPack = follow(genericPack); // TODO: Clip this follow() with LuauEagerGeneralization2 @@ -465,7 +465,7 @@ bool Unifier2::unify(TableType* subTable, const TableType* superTable) { result &= unify(subTable->indexer->indexType, superTable->indexer->indexType); result &= unify(subTable->indexer->indexResultType, superTable->indexer->indexResultType); - if (FFlag::LuauEagerGeneralization2) + if (FFlag::LuauEagerGeneralization3) { // FIXME: We can probably do something more efficient here. result &= unify(superTable->indexer->indexType, subTable->indexer->indexType); diff --git a/Common/include/Luau/ExperimentalFlags.h b/Common/include/Luau/ExperimentalFlags.h index ade0bf40..64d52d0f 100644 --- a/Common/include/Luau/ExperimentalFlags.h +++ b/Common/include/Luau/ExperimentalFlags.h @@ -12,10 +12,11 @@ inline bool isAnalysisFlagExperimental(const char* flag) // or critical bugs that are found after the code has been submitted. This list is intended _only_ for flags that affect // Luau's type checking. Flags that may change runtime behavior (e.g.: parser or VM flags) are not appropriate for this list. static const char* const kList[] = { - "LuauInstantiateInSubtyping", // requires some fixes to lua-apps code - "LuauFixIndexerSubtypingOrdering", // requires some small fixes to lua-apps code since this fixes a false negative - "StudioReportLuauAny2", // takes telemetry data for usage of any types - "LuauTableCloneClonesType3", // requires fixes in lua-apps code, terrifyingly + "LuauInstantiateInSubtyping", // requires some fixes to lua-apps code + "LuauFixIndexerSubtypingOrdering", // requires some small fixes to lua-apps code since this fixes a false negative + "StudioReportLuauAny2", // takes telemetry data for usage of any types + "LuauTableCloneClonesType3", // requires fixes in lua-apps code, terrifyingly + "LuauNormalizationReorderFreeTypeIntersect", // requires fixes in lua-apps code, also terrifyingly "LuauSolverV2", // makes sure we always have at least one entry nullptr, diff --git a/Sources.cmake b/Sources.cmake index fe8376f3..d574e921 100644 --- a/Sources.cmake +++ b/Sources.cmake @@ -185,7 +185,6 @@ target_sources(Luau.Analysis PRIVATE Analysis/include/Luau/DataFlowGraph.h Analysis/include/Luau/DcrLogger.h Analysis/include/Luau/Def.h - Analysis/include/Luau/Differ.h Analysis/include/Luau/Documentation.h Analysis/include/Luau/Error.h Analysis/include/Luau/EqSatSimplification.h @@ -265,7 +264,6 @@ target_sources(Luau.Analysis PRIVATE Analysis/src/DataFlowGraph.cpp Analysis/src/DcrLogger.cpp Analysis/src/Def.cpp - Analysis/src/Differ.cpp Analysis/src/EmbeddedBuiltinDefinitions.cpp Analysis/src/Error.cpp Analysis/src/EqSatSimplification.cpp @@ -464,9 +462,6 @@ if(TARGET Luau.UnitTest) tests/CostModel.test.cpp tests/DataFlowGraph.test.cpp tests/DenseHash.test.cpp - tests/DiffAsserts.cpp - tests/DiffAsserts.h - tests/Differ.test.cpp tests/EqSat.language.test.cpp tests/EqSat.propositional.test.cpp tests/EqSat.slice.test.cpp diff --git a/VM/src/lgc.cpp b/VM/src/lgc.cpp index 7ff002c8..b4983035 100644 --- a/VM/src/lgc.cpp +++ b/VM/src/lgc.cpp @@ -14,6 +14,8 @@ #include +LUAU_DYNAMIC_FASTFLAGVARIABLE(LuauGcAgainstOom, false) + /* * Luau uses an incremental non-generational non-moving mark&sweep garbage collector. * @@ -446,6 +448,21 @@ static void shrinkstack(lua_State* L) condhardstacktests(luaD_reallocstack(L, s_used, 0)); } +static void shrinkstackprotected(lua_State* L) +{ + struct CallContext + { + static void run(lua_State* L, void* ud) + { + shrinkstack(L); + } + } ctx = {}; + + // the resize call can fail on exception, in which case we will continue with original size + int status = luaD_rawrunprotected(L, &CallContext::run, &ctx); + LUAU_ASSERT(status == LUA_OK || status == LUA_ERRMEM); +} + /* ** traverse one gray object, turning it to black. ** Returns `quantity' traversed. @@ -497,7 +514,12 @@ static size_t propagatemark(global_State* g) // we could shrink stack at any time but we opt to do it during initial mark to do that just once per cycle if (g->gcstate == GCSpropagate) - shrinkstack(th); + { + if (DFFlag::LuauGcAgainstOom) + shrinkstackprotected(th); + else + shrinkstack(th); + } return sizeof(lua_State) + sizeof(TValue) * th->stacksize + sizeof(CallInfo) * th->size_ci; } @@ -545,6 +567,26 @@ static int isobjcleared(GCObject* o) #define iscleared(o) (iscollectable(o) && isobjcleared(gcvalue(o))) +static void tableresizeprotected(lua_State* L, LuaTable* t, int nhsize) +{ + struct CallContext + { + LuaTable* t; + int nhsize; + + static void run(lua_State* L, void* ud) + { + CallContext* ctx = (CallContext*)ud; + + luaH_resizehash(L, ctx->t, ctx->nhsize); + } + } ctx = {t, nhsize}; + + // the resize call can fail on exception, in which case we will continue with original size + int status = luaD_rawrunprotected(L, &CallContext::run, &ctx); + LUAU_ASSERT(status == LUA_OK || status == LUA_ERRMEM); +} + /* ** clear collected entries from weaktables */ @@ -592,7 +634,12 @@ static size_t cleartable(lua_State* L, GCObject* l) { // shrink at 37.5% occupancy if (activevalues < sizenode(h) * 3 / 8) - luaH_resizehash(L, h, activevalues); + { + if (DFFlag::LuauGcAgainstOom) + tableresizeprotected(L, h, activevalues); + else + luaH_resizehash(L, h, activevalues); + } } } @@ -635,12 +682,36 @@ static void freeobj(lua_State* L, GCObject* o, lua_Page* page) } } +static void stringresizeprotected(lua_State* L, int newsize) +{ + struct CallContext + { + int newsize; + + static void run(lua_State* L, void* ud) + { + CallContext* ctx = (CallContext*)ud; + + luaS_resize(L, ctx->newsize); + } + } ctx = {newsize}; + + // the resize call can fail on exception, in which case we will continue with original size + int status = luaD_rawrunprotected(L, &CallContext::run, &ctx); + LUAU_ASSERT(status == LUA_OK || status == LUA_ERRMEM); +} + static void shrinkbuffers(lua_State* L) { global_State* g = L->global; // check size of string hash if (g->strt.nuse < cast_to(uint32_t, g->strt.size / 4) && g->strt.size > LUA_MINSTRTABSIZE * 2) - luaS_resize(L, g->strt.size / 2); // table is too big + { + if (DFFlag::LuauGcAgainstOom) + stringresizeprotected(L, g->strt.size / 2); // table is too big + else + luaS_resize(L, g->strt.size / 2); // table is too big + } } static void shrinkbuffersfull(lua_State* L) @@ -651,7 +722,12 @@ static void shrinkbuffersfull(lua_State* L) while (g->strt.nuse < cast_to(uint32_t, hashsize / 4) && hashsize > LUA_MINSTRTABSIZE * 2) hashsize /= 2; if (hashsize != g->strt.size) - luaS_resize(L, hashsize); // table is too big + { + if (DFFlag::LuauGcAgainstOom) + stringresizeprotected(L, hashsize); // table is too big + else + luaS_resize(L, hashsize); // table is too big + } } static bool deletegco(void* context, lua_Page* page, GCObject* gco) diff --git a/VM/src/lstrlib.cpp b/VM/src/lstrlib.cpp index 5c9402f9..62d71f6a 100644 --- a/VM/src/lstrlib.cpp +++ b/VM/src/lstrlib.cpp @@ -8,8 +8,6 @@ #include #include -LUAU_DYNAMIC_FASTFLAGVARIABLE(LuauStringFormatFixC, false) - // macro to `unsign' a character #define uchar(c) ((unsigned char)(c)) @@ -1001,17 +999,9 @@ static int str_format(lua_State* L) { case 'c': { - if (DFFlag::LuauStringFormatFixC) - { - int count = snprintf(buff, sizeof(buff), form, (int)luaL_checknumber(L, arg)); - luaL_addlstring(&b, buff, count); - continue; // skip the 'luaL_addlstring' at the end - } - else - { - snprintf(buff, sizeof(buff), form, (int)luaL_checknumber(L, arg)); - break; - } + int count = snprintf(buff, sizeof(buff), form, (int)luaL_checknumber(L, arg)); + luaL_addlstring(&b, buff, count); + continue; // skip the 'luaL_addlstring' at the end } case 'd': case 'i': diff --git a/tests/Autocomplete.test.cpp b/tests/Autocomplete.test.cpp index b906dce0..a955b738 100644 --- a/tests/Autocomplete.test.cpp +++ b/tests/Autocomplete.test.cpp @@ -19,7 +19,7 @@ LUAU_FASTFLAG(LuauTraceTypesInNonstrictMode2) LUAU_FASTFLAG(LuauSetMetatableDoesNotTimeTravel) LUAU_FASTINT(LuauTypeInferRecursionLimit) -LUAU_FASTFLAG(LuauEagerGeneralization2) +LUAU_FASTFLAG(LuauEagerGeneralization3) LUAU_FASTFLAG(LuauExpectedTypeVisitor) using namespace Luau; @@ -4468,7 +4468,7 @@ TEST_CASE_FIXTURE(ACExternTypeFixture, "ac_dont_overflow_on_recursive_union") auto ac = autocomplete('1'); - if (FFlag::LuauSolverV2 && FFlag::LuauEagerGeneralization2) + if (FFlag::LuauSolverV2 && FFlag::LuauEagerGeneralization3) { // This `if` statement is because `LuauEagerGeneralization2` // sets some flags diff --git a/tests/Conformance.test.cpp b/tests/Conformance.test.cpp index f658eef5..6c78885f 100644 --- a/tests/Conformance.test.cpp +++ b/tests/Conformance.test.cpp @@ -36,11 +36,11 @@ void luau_callhook(lua_State* L, lua_Hook hook, void* userdata); LUAU_FASTFLAG(DebugLuauAbortingChecks) LUAU_FASTINT(CodegenHeuristicsInstructionLimit) -LUAU_DYNAMIC_FASTFLAG(LuauStringFormatFixC) LUAU_FASTFLAG(LuauYieldableContinuations) LUAU_FASTFLAG(LuauCurrentLineBounds) LUAU_FASTFLAG(LuauLoadNoOomThrow) LUAU_FASTFLAG(LuauHeapNameDetails) +LUAU_DYNAMIC_FASTFLAG(LuauGcAgainstOom) static lua_CompileOptions defaultOptions() { @@ -713,8 +713,6 @@ TEST_CASE("Clear") TEST_CASE("Strings") { - ScopedFastFlag luauStringFormatFixC{DFFlag::LuauStringFormatFixC, true}; - runConformance("strings.luau"); } @@ -768,9 +766,48 @@ TEST_CASE("Attrib") runConformance("attrib.luau"); } +static bool blockableReallocAllowed = true; + +static void* blockableRealloc(void* ud, void* ptr, size_t osize, size_t nsize) +{ + if (nsize == 0) + { + free(ptr); + return nullptr; + } + else + { + if (!blockableReallocAllowed) + return nullptr; + + return realloc(ptr, nsize); + } +} + TEST_CASE("GC") { - runConformance("gc.luau"); + ScopedFastFlag luauGcAgainstOom{DFFlag::LuauGcAgainstOom, true}; + + runConformance( + "gc.luau", + [](lua_State* L) + { + lua_pushcclosurek( + L, + [](lua_State* L) + { + blockableReallocAllowed = !luaL_checkboolean(L, 1); + return 0; + }, + "setblockallocations", + 0, + nullptr + ); + lua_setglobal(L, "setblockallocations"); + }, + nullptr, + lua_newstate(blockableRealloc, nullptr) + ); } TEST_CASE("Bitwise") diff --git a/tests/DiffAsserts.cpp b/tests/DiffAsserts.cpp deleted file mode 100644 index f343367f..00000000 --- a/tests/DiffAsserts.cpp +++ /dev/null @@ -1,31 +0,0 @@ -// This file is part of the Luau programming language and is licensed under MIT License; see LICENSE.txt for details - -#include "DiffAsserts.h" - -#include - -namespace Luau -{ - - -std::string toString(const DifferResult& result) -{ - if (result.diffError) - return result.diffError->toString(); - else - return ""; -} - -template<> -std::string diff(TypeId l, TypeId r) -{ - return toString(diff(l, r)); -} - -template<> -std::string diff(const Type& l, const Type& r) -{ - return toString(diff(&l, &r)); -} - -} // namespace Luau diff --git a/tests/DiffAsserts.h b/tests/DiffAsserts.h deleted file mode 100644 index b80ea312..00000000 --- a/tests/DiffAsserts.h +++ /dev/null @@ -1,46 +0,0 @@ -// This file is part of the Luau programming language and is licensed under MIT License; see LICENSE.txt for details -#pragma once - -#include "Luau/Differ.h" -#include "Luau/TypeFwd.h" - -#include "doctest.h" - -#include -#include - -namespace Luau -{ - -std::string toString(const DifferResult& result); - -template -std::string diff(L, R) -{ - return ""; -} - -template<> -std::string diff(TypeId l, TypeId r); - -template<> -std::string diff(const Type& l, const Type& r); - -} // namespace Luau - -// Note: the do-while blocks in the macros below is to scope the INFO block to -// only that assertion. - -#define CHECK_EQ_DIFF(l, r) \ - do \ - { \ - INFO("Left and right values were not equal: ", diff(l, r)); \ - CHECK_EQ(l, r); \ - } while (false); - -#define REQUIRE_EQ_DIFF(l, r) \ - do \ - { \ - INFO("Left and right values were not equal: ", diff(l, r)); \ - REQUIRE_EQ(l, r); \ - } while (false); diff --git a/tests/Differ.test.cpp b/tests/Differ.test.cpp deleted file mode 100644 index bf200533..00000000 --- a/tests/Differ.test.cpp +++ /dev/null @@ -1,1759 +0,0 @@ -// This file is part of the Luau programming language and is licensed under MIT License; see LICENSE.txt for details -#include "Luau/Differ.h" -#include "Luau/Common.h" -#include "Luau/Error.h" -#include "Luau/Frontend.h" - -#include "Fixture.h" -#include "ClassFixture.h" - -#include "Luau/Symbol.h" -#include "Luau/Type.h" -#include "ScopedFlags.h" -#include "doctest.h" -#include - -using namespace Luau; - -LUAU_FASTFLAG(LuauSolverV2) - -TEST_SUITE_BEGIN("Differ"); - -TEST_CASE_FIXTURE(DifferFixture, "equal_numbers") -{ - CheckResult result = check(R"( - local foo = 5 - local almostFoo = 78 - )"); - LUAU_REQUIRE_NO_ERRORS(result); - - compareTypesEq("foo", "almostFoo"); -} - -TEST_CASE_FIXTURE(DifferFixture, "equal_strings") -{ - CheckResult result = check(R"( - local foo = "hello" - local almostFoo = "world" - )"); - LUAU_REQUIRE_NO_ERRORS(result); - - compareTypesEq("foo", "almostFoo"); -} - -TEST_CASE_FIXTURE(DifferFixture, "equal_tables") -{ - CheckResult result = check(R"( - local foo = { x = 1, y = "where" } - local almostFoo = { x = 5, y = "when" } - )"); - LUAU_REQUIRE_NO_ERRORS(result); - - compareTypesEq("foo", "almostFoo"); -} - -TEST_CASE_FIXTURE(DifferFixture, "a_table_missing_property") -{ - CheckResult result = check(R"( - local foo = { x = 1, y = 2 } - local almostFoo = { x = 1, z = 3 } - )"); - LUAU_REQUIRE_NO_ERRORS(result); - - compareTypesNe( - "foo", - "almostFoo", - "DiffError: these two types are not equal because the left type at foo.y has type number, while the right type at almostFoo is missing " - "the property y" - ); -} - -TEST_CASE_FIXTURE(DifferFixture, "left_table_missing_property") -{ - CheckResult result = check(R"( - local foo = { x = 1 } - local almostFoo = { x = 1, z = 3 } - )"); - LUAU_REQUIRE_NO_ERRORS(result); - - compareTypesNe( - "foo", - "almostFoo", - "DiffError: these two types are not equal because the left type at foo is missing the property z, while the right type at almostFoo.z " - "has type number" - ); -} - -TEST_CASE_FIXTURE(DifferFixture, "a_table_wrong_type") -{ - CheckResult result = check(R"( - local foo = { x = 1, y = 2 } - local almostFoo = { x = 1, y = "two" } - )"); - LUAU_REQUIRE_NO_ERRORS(result); - - compareTypesNe( - "foo", - "almostFoo", - "DiffError: these two types are not equal because the left type at foo.y has type number, while the right type at almostFoo.y has type " - "string" - ); -} - -TEST_CASE_FIXTURE(DifferFixture, "a_table_wrong_type") -{ - CheckResult result = check(R"( - local foo: string - local almostFoo: number - )"); - LUAU_REQUIRE_NO_ERRORS(result); - - compareTypesNe( - "foo", - "almostFoo", - "DiffError: these two types are not equal because the left type at has type string, while the right type at " - " has type number" - ); -} - -TEST_CASE_FIXTURE(DifferFixture, "a_nested_table_wrong_type") -{ - CheckResult result = check(R"( - local foo = { x = 1, inner = { table = { has = { wrong = { value = 5 } } } } } - local almostFoo = { x = 1, inner = { table = { has = { wrong = { value = "five" } } } } } - )"); - LUAU_REQUIRE_NO_ERRORS(result); - - compareTypesNe( - "foo", - "almostFoo", - "DiffError: these two types are not equal because the left type at foo.inner.table.has.wrong.value has type number, while the right " - "type at almostFoo.inner.table.has.wrong.value has type string" - ); -} - -TEST_CASE_FIXTURE(DifferFixture, "a_nested_table_wrong_match") -{ - CheckResult result = check(R"( - local foo = { x = 1, inner = { table = { has = { wrong = { variant = { because = { it = { goes = { on = "five" } } } } } } } } } - local almostFoo = { x = 1, inner = { table = { has = { wrong = { variant = "five" } } } } } - )"); - LUAU_REQUIRE_NO_ERRORS(result); - - compareTypesNe( - "foo", - "almostFoo", - "DiffError: these two types are not equal because the left type at foo.inner.table.has.wrong.variant has type { because: { it: { goes: " - "{ on: string } } } }, while the right type at almostFoo.inner.table.has.wrong.variant has type string" - ); -} - -TEST_CASE_FIXTURE(DifferFixture, "left_cyclic_table_right_table_missing_property") -{ - CheckResult result = check(R"( - local function id(x: a): a - return x - end - - -- Remove name from cyclic table - local foo = id({}) - foo.foo = foo - local almostFoo = { x = 2 } - )"); - LUAU_REQUIRE_NO_ERRORS(result); - - compareTypesNe( - "foo", - "almostFoo", - R"(DiffError: these two types are not equal because the left type at .foo has type t1 where t1 = { foo: t1 }, while the right type at almostFoo is missing the property foo)" - ); -} - -TEST_CASE_FIXTURE(DifferFixture, "left_cyclic_table_right_table_property_wrong") -{ - CheckResult result = check(R"( - local function id(x: a): a - return x - end - - -- Remove name from cyclic table - local foo = id({}) - foo.foo = foo - local almostFoo = { foo = 2 } - )"); - LUAU_REQUIRE_NO_ERRORS(result); - - compareTypesNe( - "foo", - "almostFoo", - R"(DiffError: these two types are not equal because the left type at .foo has type t1 where t1 = { foo: t1 }, while the right type at almostFoo.foo has type number)" - ); -} - -TEST_CASE_FIXTURE(DifferFixture, "right_cyclic_table_left_table_missing_property") -{ - CheckResult result = check(R"( - local function id(x: a): a - return x - end - - -- Remove name from cyclic table - local foo = id({}) - foo.foo = foo - local almostFoo = { x = 2 } - )"); - LUAU_REQUIRE_NO_ERRORS(result); - - compareTypesNe( - "almostFoo", - "foo", - R"(DiffError: these two types are not equal because the left type at almostFoo.x has type number, while the right type at is missing the property x)" - ); -} - -TEST_CASE_FIXTURE(DifferFixture, "right_cyclic_table_left_table_property_wrong") -{ - CheckResult result = check(R"( - local function id(x: a): a - return x - end - - -- Remove name from cyclic table - local foo = id({}) - foo.foo = foo - local almostFoo = { foo = 2 } - )"); - LUAU_REQUIRE_NO_ERRORS(result); - - compareTypesNe( - "almostFoo", - "foo", - R"(DiffError: these two types are not equal because the left type at almostFoo.foo has type number, while the right type at .foo has type t1 where t1 = { foo: t1 })" - ); -} - -TEST_CASE_FIXTURE(DifferFixture, "equal_table_two_cyclic_tables_are_not_different") -{ - CheckResult result = check(R"( - local function id(x: a): a - return x - end - - -- Remove name from cyclic table - local foo = id({}) - foo.foo = foo - local almostFoo = id({}) - almostFoo.foo = almostFoo - )"); - LUAU_REQUIRE_NO_ERRORS(result); - - compareTypesEq("foo", "almostFoo"); -} - -TEST_CASE_FIXTURE(DifferFixture, "equal_table_two_shifted_circles_are_not_different") -{ - CheckResult result = check(R"( - local function id(x: a): a - return x - end - - -- Remove name from cyclic table - local foo = id({}) - foo.foo = id({}) - foo.foo.foo = id({}) - foo.foo.foo.foo = id({}) - foo.foo.foo.foo.foo = foo - - local builder = id({}) - builder.foo = id({}) - builder.foo.foo = id({}) - builder.foo.foo.foo = id({}) - builder.foo.foo.foo.foo = builder - -- Shift - local almostFoo = builder.foo.foo - )"); - LUAU_REQUIRE_NO_ERRORS(result); - - compareTypesEq("foo", "almostFoo"); -} - -TEST_CASE_FIXTURE(DifferFixture, "table_left_circle_right_measuring_tape") -{ - // Left is a circle, right is a measuring tape - CheckResult result = check(R"( - local function id(x: a): a - return x - end - - -- Remove name from cyclic table - local foo = id({}) - foo.foo = id({}) - foo.foo.foo = id({}) - foo.foo.foo.foo = id({}) - foo.foo.foo.bar = id({}) -- anchor to pin shape - foo.foo.foo.foo.foo = foo - local almostFoo = id({}) - almostFoo.foo = id({}) - almostFoo.foo.foo = id({}) - almostFoo.foo.foo.foo = id({}) - almostFoo.foo.foo.bar = id({}) -- anchor to pin shape - almostFoo.foo.foo.foo.foo = almostFoo.foo - )"); - LUAU_REQUIRE_NO_ERRORS(result); - - compareTypesNe( - "foo", - "almostFoo", - R"(DiffError: these two types are not equal because the left type at .foo.foo.foo.foo.foo is missing the property bar, while the right type at .foo.foo.foo.foo.foo.bar has type { })" - ); -} - -TEST_CASE_FIXTURE(DifferFixture, "equal_table_measuring_tapes") -{ - CheckResult result = check(R"( - local function id(x: a): a - return x - end - - -- Remove name from cyclic table - local foo = id({}) - foo.foo = id({}) - foo.foo.foo = id({}) - foo.foo.foo.foo = id({}) - foo.foo.foo.foo.foo = foo.foo - local almostFoo = id({}) - almostFoo.foo = id({}) - almostFoo.foo.foo = id({}) - almostFoo.foo.foo.foo = id({}) - almostFoo.foo.foo.foo.foo = almostFoo.foo - )"); - LUAU_REQUIRE_NO_ERRORS(result); - - compareTypesEq("foo", "almostFoo"); -} - -TEST_CASE_FIXTURE(DifferFixture, "equal_table_A_B_C") -{ - CheckResult result = check(R"( - local function id(x: a): a - return x - end - - -- Remove name from cyclic table - local foo = id({}) - foo.foo = id({}) - foo.foo.foo = id({}) - foo.foo.foo.foo = id({}) - foo.foo.foo.foo.foo = foo.foo - local almostFoo = id({}) - almostFoo.foo = id({}) - almostFoo.foo.foo = id({}) - almostFoo.foo.foo.foo = id({}) - almostFoo.foo.foo.foo.foo = almostFoo.foo - )"); - LUAU_REQUIRE_NO_ERRORS(result); - - compareTypesEq("foo", "almostFoo"); -} - -TEST_CASE_FIXTURE(DifferFixture, "equal_table_kind_A") -{ - CheckResult result = check(R"( - -- Remove name from cyclic table - local function id(x: a): a - return x - end - - local foo = id({}) - foo.left = id({}) - foo.right = id({}) - foo.left.left = id({}) - foo.left.right = id({}) - foo.right.left = id({}) - foo.right.right = id({}) - foo.right.left.left = id({}) - foo.right.left.right = id({}) - - foo.right.left.left.child = foo.right - - local almostFoo = id({}) - almostFoo.left = id({}) - almostFoo.right = id({}) - almostFoo.left.left = id({}) - almostFoo.left.right = id({}) - almostFoo.right.left = id({}) - almostFoo.right.right = id({}) - almostFoo.right.left.left = id({}) - almostFoo.right.left.right = id({}) - - almostFoo.right.left.left.child = almostFoo.right - - -- Bindings for requireType - local fooLeft = foo.left - local fooRight = foo.left.right - local fooLeftLeft = foo.left.left - local fooLeftRight = foo.left.right - local fooRightLeft = foo.right.left - local fooRightRight = foo.right.right - local fooRightLeftLeft = foo.right.left.left - local fooRightLeftRight = foo.right.left.right - - local almostFooLeft = almostFoo.left - local almostFooRight = almostFoo.left.right - local almostFooLeftLeft = almostFoo.left.left - local almostFooLeftRight = almostFoo.left.right - local almostFooRightLeft = almostFoo.right.left - local almostFooRightRight = almostFoo.right.right - local almostFooRightLeftLeft = almostFoo.right.left.left - local almostFooRightLeftRight = almostFoo.right.left.right - )"); - LUAU_REQUIRE_NO_ERRORS(result); - - compareTypesEq("foo", "almostFoo"); -} - -TEST_CASE_FIXTURE(DifferFixture, "equal_table_kind_B") -{ - CheckResult result = check(R"( - -- Remove name from cyclic table - local function id(x: a): a - return x - end - - local foo = id({}) - foo.left = id({}) - foo.right = id({}) - foo.left.left = id({}) - foo.left.right = id({}) - foo.right.left = id({}) - foo.right.right = id({}) - foo.right.left.left = id({}) - foo.right.left.right = id({}) - - foo.right.left.left.child = foo.left - - local almostFoo = id({}) - almostFoo.left = id({}) - almostFoo.right = id({}) - almostFoo.left.left = id({}) - almostFoo.left.right = id({}) - almostFoo.right.left = id({}) - almostFoo.right.right = id({}) - almostFoo.right.left.left = id({}) - almostFoo.right.left.right = id({}) - - almostFoo.right.left.left.child = almostFoo.left - )"); - LUAU_REQUIRE_NO_ERRORS(result); - - compareTypesEq("foo", "almostFoo"); -} - -TEST_CASE_FIXTURE(DifferFixture, "equal_table_kind_C") -{ - CheckResult result = check(R"( - -- Remove name from cyclic table - local function id(x: a): a - return x - end - - local foo = id({}) - foo.left = id({}) - foo.right = id({}) - foo.left.left = id({}) - foo.left.right = id({}) - foo.right.left = id({}) - foo.right.right = id({}) - foo.right.left.left = id({}) - foo.right.left.right = id({}) - - foo.right.left.left.child = foo - - local almostFoo = id({}) - almostFoo.left = id({}) - almostFoo.right = id({}) - almostFoo.left.left = id({}) - almostFoo.left.right = id({}) - almostFoo.right.left = id({}) - almostFoo.right.right = id({}) - almostFoo.right.left.left = id({}) - almostFoo.right.left.right = id({}) - - almostFoo.right.left.left.child = almostFoo - )"); - LUAU_REQUIRE_NO_ERRORS(result); - - compareTypesEq("foo", "almostFoo"); -} - -TEST_CASE_FIXTURE(DifferFixture, "equal_table_kind_D") -{ - CheckResult result = check(R"( - -- Remove name from cyclic table - local function id(x: a): a - return x - end - - local foo = id({}) - foo.left = id({}) - foo.right = id({}) - foo.left.left = id({}) - foo.left.right = id({}) - foo.right.left = id({}) - foo.right.right = id({}) - foo.right.left.left = id({}) - foo.right.left.right = id({}) - - foo.right.left.left.child = foo.right.left.left - - local almostFoo = id({}) - almostFoo.left = id({}) - almostFoo.right = id({}) - almostFoo.left.left = id({}) - almostFoo.left.right = id({}) - almostFoo.right.left = id({}) - almostFoo.right.right = id({}) - almostFoo.right.left.left = id({}) - almostFoo.right.left.right = id({}) - - almostFoo.right.left.left.child = almostFoo.right.left.left - )"); - LUAU_REQUIRE_NO_ERRORS(result); - - compareTypesEq("foo", "almostFoo"); -} - -TEST_CASE_FIXTURE(DifferFixture, "equal_table_cyclic_diamonds_unraveled") -{ - CheckResult result = check(R"( - -- Remove name from cyclic table - local function id(x: a): a - return x - end - - -- Pattern 1 - local foo = id({}) - foo.child = id({}) - foo.child.left = id({}) - foo.child.right = id({}) - - foo.child.left.child = foo - foo.child.right.child = foo - - -- Pattern 2 - local almostFoo = id({}) - almostFoo.child = id({}) - almostFoo.child.left = id({}) - almostFoo.child.right = id({}) - - almostFoo.child.left.child = id({}) -- Use a new table - almostFoo.child.right.child = almostFoo.child.left.child -- Refer to the same new table - - almostFoo.child.left.child.child = id({}) - almostFoo.child.left.child.child.left = id({}) - almostFoo.child.left.child.child.right = id({}) - - almostFoo.child.left.child.child.left.child = almostFoo.child.left.child - almostFoo.child.left.child.child.right.child = almostFoo.child.left.child - - -- Pattern 3 - local anotherFoo = id({}) - anotherFoo.child = id({}) - anotherFoo.child.left = id({}) - anotherFoo.child.right = id({}) - - anotherFoo.child.left.child = id({}) -- Use a new table - anotherFoo.child.right.child = id({}) -- Use another new table - - anotherFoo.child.left.child.child = id({}) - anotherFoo.child.left.child.child.left = id({}) - anotherFoo.child.left.child.child.right = id({}) - anotherFoo.child.right.child.child = id({}) - anotherFoo.child.right.child.child.left = id({}) - anotherFoo.child.right.child.child.right = id({}) - - anotherFoo.child.left.child.child.left.child = anotherFoo.child.left.child - anotherFoo.child.left.child.child.right.child = anotherFoo.child.left.child - anotherFoo.child.right.child.child.left.child = anotherFoo.child.right.child - anotherFoo.child.right.child.child.right.child = anotherFoo.child.right.child - - -- Pattern 4 - local cleverFoo = id({}) - cleverFoo.child = id({}) - cleverFoo.child.left = id({}) - cleverFoo.child.right = id({}) - - cleverFoo.child.left.child = id({}) -- Use a new table - cleverFoo.child.right.child = id({}) -- Use another new table - - cleverFoo.child.left.child.child = id({}) - cleverFoo.child.left.child.child.left = id({}) - cleverFoo.child.left.child.child.right = id({}) - cleverFoo.child.right.child.child = id({}) - cleverFoo.child.right.child.child.left = id({}) - cleverFoo.child.right.child.child.right = id({}) - -- Same as pattern 3, but swapped here - cleverFoo.child.left.child.child.left.child = cleverFoo.child.right.child -- Swap - cleverFoo.child.left.child.child.right.child = cleverFoo.child.right.child - cleverFoo.child.right.child.child.left.child = cleverFoo.child.left.child - cleverFoo.child.right.child.child.right.child = cleverFoo.child.left.child - - -- Pattern 5 - local cheekyFoo = id({}) - cheekyFoo.child = id({}) - cheekyFoo.child.left = id({}) - cheekyFoo.child.right = id({}) - - cheekyFoo.child.left.child = foo -- Use existing pattern - cheekyFoo.child.right.child = foo -- Use existing pattern - )"); - LUAU_REQUIRE_NO_ERRORS(result); - - std::vector symbols{"foo", "almostFoo", "anotherFoo", "cleverFoo", "cheekyFoo"}; - - for (auto left : symbols) - { - for (auto right : symbols) - { - compareTypesEq(left, right); - } - } -} - -TEST_CASE_FIXTURE(DifferFixture, "equal_function_cyclic") -{ - // Old solver does not correctly infer function typepacks - ScopedFastFlag sff{FFlag::LuauSolverV2, true}; - - CheckResult result = check(R"( - function foo() - return foo - end - function almostFoo() - function bar() - return bar - end - return bar - end - )"); - LUAU_REQUIRE_NO_ERRORS(result); - - compareTypesEq("foo", "almostFoo"); -} - -TEST_CASE_FIXTURE(DifferFixture, "equal_function_table_cyclic") -{ - // Old solver does not correctly infer function typepacks - ScopedFastFlag sff{FFlag::LuauSolverV2, true}; - - CheckResult result = check(R"( - function foo() - return { - bar = foo - } - end - function almostFoo() - function bar() - return { - bar = bar - } - end - return { - bar = bar - } - end - )"); - LUAU_REQUIRE_NO_ERRORS(result); - - compareTypesEq("foo", "almostFoo"); -} - -TEST_CASE_FIXTURE(DifferFixture, "function_table_self_referential_cyclic") -{ - // Old solver does not correctly infer function typepacks - // ScopedFastFlag sff{FFlag::LuauSolverV2, true}; - - CheckResult result = check(R"( - function foo() - return { - bar = foo - } - end - function almostFoo() - function bar() - return bar - end - return { - bar = bar - } - end - )"); - LUAU_REQUIRE_NO_ERRORS(result); - - if (FFlag::LuauSolverV2) - compareTypesNe( - "foo", - "almostFoo", - R"(DiffError: these two types are not equal because the left type at .Ret[1].bar.Ret[1] has type t1 where t1 = { bar: () -> t1 }, while the right type at .Ret[1].bar.Ret[1] has type t1 where t1 = () -> t1)" - ); - else - compareTypesNe( - "foo", - "almostFoo", - R"(DiffError: these two types are not equal because the left type at .Ret[1].bar.Ret[1] has type t1 where t1 = {| bar: () -> t1 |}, while the right type at .Ret[1].bar.Ret[1] has type t1 where t1 = () -> t1)" - ); -} - -TEST_CASE_FIXTURE(DifferFixture, "equal_union_cyclic") -{ - TypeArena arena; - TypeId number = arena.addType(PrimitiveType{PrimitiveType::Number}); - TypeId string = arena.addType(PrimitiveType{PrimitiveType::String}); - - TypeId foo = arena.addType(UnionType{std::vector{number, string}}); - UnionType* unionFoo = getMutable(foo); - unionFoo->options.push_back(foo); - - TypeId almostFoo = arena.addType(UnionType{std::vector{number, string}}); - UnionType* unionAlmostFoo = getMutable(almostFoo); - unionAlmostFoo->options.push_back(almostFoo); - - compareEq(foo, almostFoo); -} - -TEST_CASE_FIXTURE(DifferFixture, "equal_intersection_cyclic") -{ - // Old solver does not correctly refine test types - ScopedFastFlag sff{FFlag::LuauSolverV2, true}; - - CheckResult result = check(R"( - function foo1(x: number) - return x - end - function foo2(x: string) - return 0 - end - function bar1(x: number) - return x - end - function bar2(x: string) - return 0 - end - )"); - LUAU_REQUIRE_NO_ERRORS(result); - TypeId foo1 = requireType("foo1"); - TypeId foo2 = requireType("foo2"); - TypeId bar1 = requireType("bar1"); - TypeId bar2 = requireType("bar2"); - - TypeArena arena; - - TypeId foo = arena.addType(IntersectionType{std::vector{foo1, foo2}}); - IntersectionType* intersectionFoo = getMutable(foo); - intersectionFoo->parts.push_back(foo); - - TypeId almostFoo = arena.addType(IntersectionType{std::vector{bar1, bar2}}); - IntersectionType* intersectionAlmostFoo = getMutable(almostFoo); - intersectionAlmostFoo->parts.push_back(almostFoo); - - compareEq(foo, almostFoo); -} - -TEST_CASE_FIXTURE(DifferFixture, "singleton") -{ - CheckResult result = check(R"( - local foo: "hello" = "hello" - local almostFoo: true = true - )"); - LUAU_REQUIRE_NO_ERRORS(result); - - compareTypesNe( - "foo", - "almostFoo", - R"(DiffError: these two types are not equal because the left type at has type "hello", while the right type at has type true)" - ); -} - -TEST_CASE_FIXTURE(DifferFixture, "equal_singleton") -{ - CheckResult result = check(R"( - local foo: "hello" = "hello" - local almostFoo: "hello" - )"); - LUAU_REQUIRE_NO_ERRORS(result); - - compareTypesEq("foo", "almostFoo"); -} - -TEST_CASE_FIXTURE(DifferFixture, "singleton_string") -{ - CheckResult result = check(R"( - local foo: "hello" = "hello" - local almostFoo: "world" = "world" - )"); - LUAU_REQUIRE_NO_ERRORS(result); - - compareTypesNe( - "foo", - "almostFoo", - R"(DiffError: these two types are not equal because the left type at has type "hello", while the right type at has type "world")" - ); -} - -TEST_CASE_FIXTURE(DifferFixtureWithBuiltins, "negation") -{ - if (!FFlag::LuauSolverV2) - return; - - CheckResult result = check(R"( - local bar: { x: { y: unknown }} - local almostBar: { x: { y: unknown }} - - local foo - local almostFoo - - if typeof(bar.x.y) ~= "string" then - foo = bar - end - - if typeof(almostBar.x.y) ~= "number" then - almostFoo = almostBar - end - )"); - LUAU_REQUIRE_NO_ERRORS(result); - - compareTypesNe( - "foo", - "almostFoo", - R"(DiffError: these two types are not equal because the left type at is a union containing type { x: { y: ~string } }, while the right type at is a union missing type { x: { y: ~string } })" - ); - - // TODO: a more desirable expected error here is as below, but `Differ` requires improvements to - // dealing with unions to get something like this (recognizing that the union is identical - // except in one component where they differ). - // - // compareTypesNe("foo", "almostFoo", - // R"(DiffError: these two types are not equal because the left type at .x.y.Negation has type string, while the right type - // at .x.y.Negation has type number)"); -} - -TEST_CASE_FIXTURE(DifferFixture, "union_missing_right") -{ - CheckResult result = check(R"( - local foo: string | number - local almostFoo: boolean | string - )"); - LUAU_REQUIRE_NO_ERRORS(result); - - compareTypesNe( - "foo", - "almostFoo", - R"(DiffError: these two types are not equal because the left type at is a union containing type number, while the right type at is a union missing type number)" - ); -} - -TEST_CASE_FIXTURE(DifferFixture, "union_missing_left") -{ - CheckResult result = check(R"( - local foo: string | number - local almostFoo: boolean | string | number - )"); - LUAU_REQUIRE_NO_ERRORS(result); - - compareTypesNe( - "foo", - "almostFoo", - R"(DiffError: these two types are not equal because the left type at is a union missing type boolean, while the right type at is a union containing type boolean)" - ); -} - -TEST_CASE_FIXTURE(DifferFixture, "union_missing") -{ - // TODO: this test case produces an error message that is not the most UX-friendly - - CheckResult result = check(R"( - local foo: { bar: number, pan: string } | { baz: boolean, rot: "singleton" } - local almostFoo: { bar: number, pan: string } | { baz: string, rot: "singleton" } - )"); - LUAU_REQUIRE_NO_ERRORS(result); - - if (FFlag::LuauSolverV2) - compareTypesNe( - "foo", - "almostFoo", - R"(DiffError: these two types are not equal because the left type at is a union containing type { baz: boolean, rot: "singleton" }, while the right type at is a union missing type { baz: boolean, rot: "singleton" })" - ); - else - compareTypesNe( - "foo", - "almostFoo", - R"(DiffError: these two types are not equal because the left type at is a union containing type {| baz: boolean, rot: "singleton" |}, while the right type at is a union missing type {| baz: boolean, rot: "singleton" |})" - ); -} - -TEST_CASE_FIXTURE(DifferFixture, "intersection_missing_right") -{ - CheckResult result = check(R"( - local foo: (number) -> () & (string) -> () - local almostFoo: (string) -> () & (boolean) -> () - )"); - LUAU_REQUIRE_NO_ERRORS(result); - - compareTypesNe( - "foo", - "almostFoo", - R"(DiffError: these two types are not equal because the left type at is an intersection containing type (number) -> (), while the right type at is an intersection missing type (number) -> ())" - ); -} - -TEST_CASE_FIXTURE(DifferFixture, "intersection_missing_left") -{ - CheckResult result = check(R"( - local foo: (number) -> () & (string) -> () - local almostFoo: (string) -> () & (boolean) -> () & (number) -> () - )"); - LUAU_REQUIRE_NO_ERRORS(result); - - compareTypesNe( - "foo", - "almostFoo", - R"(DiffError: these two types are not equal because the left type at is an intersection missing type (boolean) -> (), while the right type at is an intersection containing type (boolean) -> ())" - ); -} - -TEST_CASE_FIXTURE(DifferFixture, "intersection_tables_missing_right") -{ - CheckResult result = check(R"( - local foo: { x: number } & { y: string } - local almostFoo: { y: string } & { z: boolean } - )"); - LUAU_REQUIRE_NO_ERRORS(result); - - if (FFlag::LuauSolverV2) - compareTypesNe( - "foo", - "almostFoo", - R"(DiffError: these two types are not equal because the left type at is an intersection containing type { x: number }, while the right type at is an intersection missing type { x: number })" - ); - else - compareTypesNe( - "foo", - "almostFoo", - R"(DiffError: these two types are not equal because the left type at is an intersection containing type {| x: number |}, while the right type at is an intersection missing type {| x: number |})" - ); -} - -TEST_CASE_FIXTURE(DifferFixture, "intersection_tables_missing_left") -{ - CheckResult result = check(R"( - local foo: { x: number } & { y: string } - local almostFoo: { y: string } & { z: boolean } & { x: number } - )"); - LUAU_REQUIRE_NO_ERRORS(result); - - if (FFlag::LuauSolverV2) - compareTypesNe( - "foo", - "almostFoo", - R"(DiffError: these two types are not equal because the left type at is an intersection missing type { z: boolean }, while the right type at is an intersection containing type { z: boolean })" - ); - else - compareTypesNe( - "foo", - "almostFoo", - R"(DiffError: these two types are not equal because the left type at is an intersection missing type {| z: boolean |}, while the right type at is an intersection containing type {| z: boolean |})" - ); -} - -TEST_CASE_FIXTURE(DifferFixture, "equal_function") -{ - // Old solver does not correctly infer function typepacks - ScopedFastFlag sff{FFlag::LuauSolverV2, true}; - - CheckResult result = check(R"( - function foo(x: number) - return x - end - function almostFoo(y: number) - return y + 10 - end - )"); - LUAU_REQUIRE_NO_ERRORS(result); - - compareTypesEq("foo", "almostFoo"); -} - -TEST_CASE_FIXTURE(DifferFixture, "equal_function_inferred_ret_length") -{ - // Old solver does not correctly infer function typepacks - ScopedFastFlag sff{FFlag::LuauSolverV2, true}; - - CheckResult result = check(R"( - function bar(x: number, y: string) - return x, y - end - function almostBar(a: number, b: string) - return a, b - end - function foo(x: number, y: string, z: boolean) - return z, bar(x, y) - end - function almostFoo(a: number, b: string, c: boolean) - return c, almostBar(a, b) - end - )"); - LUAU_REQUIRE_NO_ERRORS(result); - - compareTypesEq("foo", "almostFoo"); -} - -TEST_CASE_FIXTURE(DifferFixture, "equal_function_inferred_ret_length_2") -{ - // Old solver does not correctly infer function typepacks - ScopedFastFlag sff{FFlag::LuauSolverV2, true}; - - CheckResult result = check(R"( - function bar(x: number, y: string) - return x, y - end - function foo(x: number, y: string, z: boolean) - return bar(x, y), z - end - function almostFoo(a: number, b: string, c: boolean) - return a, c - end - )"); - LUAU_REQUIRE_NO_ERRORS(result); - - compareTypesEq("foo", "almostFoo"); -} - -TEST_CASE_FIXTURE(DifferFixture, "function_arg_normal") -{ - // Old solver does not correctly infer function typepacks - ScopedFastFlag sff{FFlag::LuauSolverV2, true}; - - CheckResult result = check(R"( - function foo(x: number, y: number, z: number) - return x * y * z - end - function almostFoo(a: number, b: number, msg: string) - return a - end - )"); - LUAU_REQUIRE_NO_ERRORS(result); - - compareTypesNe( - "foo", - "almostFoo", - R"(DiffError: these two types are not equal because the left type at .Arg[3] has type number, while the right type at .Arg[3] has type string)" - ); -} - -TEST_CASE_FIXTURE(DifferFixture, "function_arg_normal_2") -{ - // Old solver does not correctly infer function typepacks - ScopedFastFlag sff{FFlag::LuauSolverV2, true}; - - CheckResult result = check(R"( - function foo(x: number, y: number, z: string) - return x * y - end - function almostFoo(a: number, y: string, msg: string) - return a - end - )"); - LUAU_REQUIRE_NO_ERRORS(result); - - compareTypesNe( - "foo", - "almostFoo", - R"(DiffError: these two types are not equal because the left type at .Arg[2] has type number, while the right type at .Arg[2] has type string)" - ); -} - -TEST_CASE_FIXTURE(DifferFixture, "function_ret_normal") -{ - // Old solver does not correctly infer function typepacks - ScopedFastFlag sff{FFlag::LuauSolverV2, true}; - - CheckResult result = check(R"( - function foo(x: number, y: number, z: string) - return x - end - function almostFoo(a: number, b: number, msg: string) - return msg - end - )"); - LUAU_REQUIRE_NO_ERRORS(result); - - compareTypesNe( - "foo", - "almostFoo", - R"(DiffError: these two types are not equal because the left type at .Ret[1] has type number, while the right type at .Ret[1] has type string)" - ); -} - -TEST_CASE_FIXTURE(DifferFixture, "function_arg_length") -{ - // Old solver does not correctly infer function typepacks - ScopedFastFlag sff{FFlag::LuauSolverV2, true}; - - CheckResult result = check(R"( - function foo(x: number, y: number) - return x - end - function almostFoo(x: number, y: number, c: number) - return x - end - )"); - LUAU_REQUIRE_NO_ERRORS(result); - - compareTypesNe( - "foo", - "almostFoo", - R"(DiffError: these two types are not equal because the left type at takes 2 or more arguments, while the right type at takes 3 or more arguments)" - ); -} - -TEST_CASE_FIXTURE(DifferFixture, "function_arg_length_2") -{ - // Old solver does not correctly infer function typepacks - ScopedFastFlag sff{FFlag::LuauSolverV2, true}; - - CheckResult result = check(R"( - function foo(x: number, y: string, z: number) - return z - end - function almostFoo(x: number, y: string) - return x - end - )"); - LUAU_REQUIRE_NO_ERRORS(result); - - compareTypesNe( - "foo", - "almostFoo", - R"(DiffError: these two types are not equal because the left type at takes 3 or more arguments, while the right type at takes 2 or more arguments)" - ); -} - -TEST_CASE_FIXTURE(DifferFixture, "function_arg_length_none") -{ - // Old solver does not correctly infer function typepacks - ScopedFastFlag sff{FFlag::LuauSolverV2, true}; - - CheckResult result = check(R"( - function foo() - return 5 - end - function almostFoo(x: number, y: string) - return x - end - )"); - LUAU_REQUIRE_NO_ERRORS(result); - - compareTypesNe( - "foo", - "almostFoo", - R"(DiffError: these two types are not equal because the left type at takes 0 or more arguments, while the right type at takes 2 or more arguments)" - ); -} - -TEST_CASE_FIXTURE(DifferFixture, "function_arg_length_none_2") -{ - // Old solver does not correctly infer function typepacks - ScopedFastFlag sff{FFlag::LuauSolverV2, true}; - - CheckResult result = check(R"( - function foo(x: number) - return x - end - function almostFoo() - return 5 - end - )"); - LUAU_REQUIRE_NO_ERRORS(result); - - compareTypesNe( - "foo", - "almostFoo", - R"(DiffError: these two types are not equal because the left type at takes 1 or more arguments, while the right type at takes 0 or more arguments)" - ); -} - -TEST_CASE_FIXTURE(DifferFixture, "function_ret_length") -{ - // Old solver does not correctly infer function typepacks - ScopedFastFlag sff{FFlag::LuauSolverV2, true}; - - CheckResult result = check(R"( - function foo(x: number, y: number) - return x - end - function almostFoo(x: number, y: number) - return x, y - end - )"); - LUAU_REQUIRE_NO_ERRORS(result); - - compareTypesNe( - "foo", - "almostFoo", - R"(DiffError: these two types are not equal because the left type at returns 1 values, while the right type at returns 2 values)" - ); -} - -TEST_CASE_FIXTURE(DifferFixture, "function_ret_length_2") -{ - // Old solver does not correctly infer function typepacks - ScopedFastFlag sff{FFlag::LuauSolverV2, true}; - - CheckResult result = check(R"( - function foo(x: number, y: string, z: number) - return y, x, z - end - function almostFoo(x: number, y: string, z: number) - return y, x - end - )"); - LUAU_REQUIRE_NO_ERRORS(result); - - compareTypesNe( - "foo", - "almostFoo", - R"(DiffError: these two types are not equal because the left type at returns 3 values, while the right type at returns 2 values)" - ); -} - -TEST_CASE_FIXTURE(DifferFixture, "function_ret_length_none") -{ - // Old solver does not correctly infer function typepacks - ScopedFastFlag sff{FFlag::LuauSolverV2, true}; - - CheckResult result = check(R"( - function foo(x: number, y: string) - return - end - function almostFoo(x: number, y: string) - return x - end - )"); - LUAU_REQUIRE_NO_ERRORS(result); - - compareTypesNe( - "foo", - "almostFoo", - R"(DiffError: these two types are not equal because the left type at returns 0 values, while the right type at returns 1 values)" - ); -} - -TEST_CASE_FIXTURE(DifferFixture, "function_ret_length_none_2") -{ - // Old solver does not correctly infer function typepacks - ScopedFastFlag sff{FFlag::LuauSolverV2, true}; - - CheckResult result = check(R"( - function foo() - return 5 - end - function almostFoo() - return - end - )"); - LUAU_REQUIRE_NO_ERRORS(result); - - compareTypesNe( - "foo", - "almostFoo", - R"(DiffError: these two types are not equal because the left type at returns 1 values, while the right type at returns 0 values)" - ); -} - -TEST_CASE_FIXTURE(DifferFixture, "function_variadic_arg_normal") -{ - // Old solver does not correctly infer function typepacks - ScopedFastFlag sff{FFlag::LuauSolverV2, true}; - - CheckResult result = check(R"( - function foo(x: number, y: string, ...: number) - return x, y - end - function almostFoo(a: number, b: string, ...: string) - return a, b - end - )"); - LUAU_REQUIRE_NO_ERRORS(result); - - compareTypesNe( - "foo", - "almostFoo", - R"(DiffError: these two types are not equal because the left type at .Arg[Variadic] has type number, while the right type at .Arg[Variadic] has type string)" - ); -} - -TEST_CASE_FIXTURE(DifferFixture, "function_variadic_arg_missing") -{ - // Old solver does not correctly infer function typepacks - ScopedFastFlag sff{FFlag::LuauSolverV2, true}; - - CheckResult result = check(R"( - function foo(x: number, y: string, ...: number) - return x, y - end - function almostFoo(a: number, b: string) - return a, b - end - )"); - LUAU_REQUIRE_NO_ERRORS(result); - - compareTypesNe( - "foo", - "almostFoo", - R"(DiffError: these two types are not equal because the left type at .Arg[Variadic] has type number, while the right type at .Arg[Variadic] has type any)" - ); -} - -TEST_CASE_FIXTURE(DifferFixture, "function_variadic_arg_missing_2") -{ - // Old solver does not correctly infer function typepacks - ScopedFastFlag sff{FFlag::LuauSolverV2, true}; - - CheckResult result = check(R"( - function foo(x: number, y: string) - return x, y - end - function almostFoo(a: number, b: string, ...: string) - return a, b - end - )"); - LUAU_REQUIRE_NO_ERRORS(result); - - compareTypesNe( - "foo", - "almostFoo", - R"(DiffError: these two types are not equal because the left type at .Arg[Variadic] has type any, while the right type at .Arg[Variadic] has type string)" - ); -} - -TEST_CASE_FIXTURE(DifferFixture, "function_variadic_oversaturation") -{ - // Old solver does not correctly infer function typepacks - ScopedFastFlag sff{FFlag::LuauSolverV2, true}; - - CheckResult result = check(R"( - -- allowed to be oversaturated - function foo(x: number, y: string) - return x, y - end - -- must not be oversaturated - local almostFoo: (number, string) -> (number, string) = foo - )"); - LUAU_REQUIRE_NO_ERRORS(result); - - compareTypesNe( - "foo", - "almostFoo", - R"(DiffError: these two types are not equal because the left type at takes 2 or more arguments, while the right type at takes 2 arguments)" - ); -} - -TEST_CASE_FIXTURE(DifferFixture, "function_variadic_oversaturation_2") -{ - // Old solver does not correctly infer function typepacks - ScopedFastFlag sff{FFlag::LuauSolverV2, true}; - - CheckResult result = check(R"( - -- must not be oversaturated - local foo: (number, string) -> (number, string) - -- allowed to be oversaturated - function almostFoo(x: number, y: string) - return x, y - end - )"); - LUAU_REQUIRE_NO_ERRORS(result); - - compareTypesNe( - "foo", - "almostFoo", - R"(DiffError: these two types are not equal because the left type at takes 2 arguments, while the right type at takes 2 or more arguments)" - ); -} - -TEST_CASE_FIXTURE(DifferFixture, "generic") -{ - // Old solver does not correctly infer function typepacks - ScopedFastFlag sff{FFlag::LuauSolverV2, true}; - - CheckResult result = check(R"( - function foo(x, y) - return x, y - end - function almostFoo(x, y) - return y, x - end - )"); - LUAU_REQUIRE_NO_ERRORS(result); - - compareTypesNe( - "foo", - "almostFoo", - R"(DiffError: these two types are not equal because the left generic at .Ret[1] cannot be the same type parameter as the right generic at .Ret[1])" - ); -} - -TEST_CASE_FIXTURE(DifferFixture, "generic_one_vs_two") -{ - // Old solver does not correctly infer function typepacks - ScopedFastFlag sff{FFlag::LuauSolverV2, true}; - - CheckResult result = check(R"( - function foo(x: X, y: X) - return - end - function almostFoo(x: T, y: U) - return - end - )"); - LUAU_REQUIRE_NO_ERRORS(result); - - compareTypesNe( - "foo", - "almostFoo", - R"(DiffError: these two types are not equal because the left generic at .Arg[2] cannot be the same type parameter as the right generic at .Arg[2])" - ); -} - -TEST_CASE_FIXTURE(DifferFixture, "generic_three_or_three") -{ - // Old solver does not correctly infer function typepacks - ScopedFastFlag sff{FFlag::LuauSolverV2, true}; - - CheckResult result = check(R"( - function foo(x: X, y: X, z: Y) - return - end - function almostFoo(x: T, y: U, z: U) - return - end - )"); - LUAU_REQUIRE_NO_ERRORS(result); - - compareTypesNe( - "foo", - "almostFoo", - R"(DiffError: these two types are not equal because the left generic at .Arg[2] cannot be the same type parameter as the right generic at .Arg[2])" - ); -} - -TEST_CASE_FIXTURE(DifferFixtureWithBuiltins, "equal_metatable") -{ - CheckResult result = check(R"( - local metaFoo = { - metaBar = 5 - } - local metaAlmostFoo = { - metaBar = 1 - } - local foo = { - bar = 3 - } - setmetatable(foo, metaFoo) - local almostFoo = { - bar = 4 - } - setmetatable(almostFoo, metaAlmostFoo) - )"); - LUAU_REQUIRE_NO_ERRORS(result); - - compareTypesEq("foo", "almostFoo"); -} - -TEST_CASE_FIXTURE(DifferFixtureWithBuiltins, "metatable_normal") -{ - CheckResult result = check(R"( - local metaFoo = { - metaBar = 5 - } - local metaAlmostFoo = { - metaBar = 1 - } - local foo = { - bar = 3 - } - setmetatable(foo, metaFoo) - local almostFoo = { - bar = "hello" - } - setmetatable(almostFoo, metaAlmostFoo) - )"); - LUAU_REQUIRE_NO_ERRORS(result); - - compareTypesNe( - "foo", - "almostFoo", - R"(DiffError: these two types are not equal because the left type at .bar has type number, while the right type at .bar has type string)" - ); -} - -TEST_CASE_FIXTURE(DifferFixtureWithBuiltins, "metatable_metanormal") -{ - CheckResult result = check(R"( - local metaFoo = { - metaBar = "world" - } - local metaAlmostFoo = { - metaBar = 1 - } - local foo = { - bar = "amazing" - } - setmetatable(foo, metaFoo) - local almostFoo = { - bar = "hello" - } - setmetatable(almostFoo, metaAlmostFoo) - )"); - LUAU_REQUIRE_NO_ERRORS(result); - - compareTypesNe( - "foo", - "almostFoo", - R"(DiffError: these two types are not equal because the left type at .__metatable.metaBar has type string, while the right type at .__metatable.metaBar has type number)" - ); -} - -TEST_CASE_FIXTURE(DifferFixtureWithBuiltins, "metatable_metamissing_left") -{ - CheckResult result = check(R"( - local metaFoo = { - metaBar = "world" - } - local metaAlmostFoo = { - metaBar = 1, - thisIsOnlyInRight = 2, - } - local foo = { - bar = "amazing" - } - setmetatable(foo, metaFoo) - local almostFoo = { - bar = "hello" - } - setmetatable(almostFoo, metaAlmostFoo) - )"); - LUAU_REQUIRE_NO_ERRORS(result); - - compareTypesNe( - "foo", - "almostFoo", - R"(DiffError: these two types are not equal because the left type at .__metatable is missing the property thisIsOnlyInRight, while the right type at .__metatable.thisIsOnlyInRight has type number)" - ); -} - -TEST_CASE_FIXTURE(DifferFixtureWithBuiltins, "metatable_metamissing_right") -{ - CheckResult result = check(R"( - local metaFoo = { - metaBar = "world", - thisIsOnlyInLeft = 2, - } - local metaAlmostFoo = { - metaBar = 1, - } - local foo = { - bar = "amazing" - } - setmetatable(foo, metaFoo) - local almostFoo = { - bar = "hello" - } - setmetatable(almostFoo, metaAlmostFoo) - )"); - LUAU_REQUIRE_NO_ERRORS(result); - - compareTypesNe( - "foo", - "almostFoo", - R"(DiffError: these two types are not equal because the left type at .__metatable.thisIsOnlyInLeft has type number, while the right type at .__metatable is missing the property thisIsOnlyInLeft)" - ); -} - -TEST_CASE_FIXTURE(DifferFixtureGeneric, "equal_class") -{ - CheckResult result = check(R"( - local foo = BaseClass - local almostFoo = BaseClass - )"); - LUAU_REQUIRE_NO_ERRORS(result); - - compareTypesEq("foo", "almostFoo"); -} - -TEST_CASE_FIXTURE(DifferFixtureGeneric, "class_normal") -{ - CheckResult result = check(R"( - local foo = BaseClass - local almostFoo = ChildClass - )"); - LUAU_REQUIRE_NO_ERRORS(result); - - compareTypesNe( - "foo", - "almostFoo", - R"(DiffError: these two types are not equal because the left type at has type BaseClass, while the right type at has type ChildClass)" - ); -} - -TEST_CASE_FIXTURE(DifferFixture, "equal_generictp") -{ - CheckResult result = check(R"( - local foo: () -> T... - local almostFoo: () -> U... - )"); - LUAU_REQUIRE_NO_ERRORS(result); - - compareTypesEq("foo", "almostFoo"); -} - -TEST_CASE_FIXTURE(DifferFixture, "generictp_ne_fn") -{ - CheckResult result = check(R"( - local foo: (...T) -> U... - local almostFoo: (U...) -> U... - )"); - LUAU_REQUIRE_NO_ERRORS(result); - - compareTypesNe( - "foo", - "almostFoo", - R"(DiffError: these two types are not equal because the left type at has type (...T) -> (U...), while the right type at has type (U...) -> (U...))" - ); -} - -TEST_CASE_FIXTURE(DifferFixture, "generictp_normal") -{ - CheckResult result = check(R"( - -- trN should be X... -> Y... - -- s should be X -> Y... - -- x should be X - -- bij should be X... -> X... - - -- Intended signature: (X... -> Y..., Z -> X..., X... -> Y..., Z, Y... -> Y...) -> () - function foo(tr, s, tr2, x, bij) - bij(bij(tr(s(x)))) - bij(bij(tr2(s(x)))) - end - -- Intended signature: (X... -> X..., Z -> X..., X... -> Y..., Z, Y... -> Y...) -> () - function almostFoo(bij, s, tr, x, bij2) - bij(bij(s(x))) - bij2(bij2(tr(s(x)))) - end - )"); - LUAU_REQUIRE_NO_ERRORS(result); - - INFO(Luau::toString(requireType("foo"))); - INFO(Luau::toString(requireType("almostFoo"))); - - compareTypesNe( - "foo", - "almostFoo", - R"(DiffError: these two types are not equal because the left generic at .Arg[1].Ret[Variadic] cannot be the same type parameter as the right generic at .Arg[1].Ret[Variadic])" - ); -} - -TEST_CASE_FIXTURE(DifferFixture, "generictp_normal_2") -{ - CheckResult result = check(R"( - -- trN should be X... -> Y... - -- s should be X -> Y... - -- x should be X - -- bij should be X... -> X... - - function foo(s, tr, tr2, x, bij) - bij(bij(tr(s(x)))) - bij(bij(tr2(s(x)))) - end - function almostFoo(s, bij, tr, x, bij2) - bij2(bij2(bij(bij(tr(s(x)))))) - end - )"); - LUAU_REQUIRE_NO_ERRORS(result); - - INFO(Luau::toString(requireType("foo"))); - INFO(Luau::toString(requireType("almostFoo"))); - - compareTypesNe( - "foo", - "almostFoo", - R"(DiffError: these two types are not equal because the left generic at .Arg[2].Arg[Variadic] cannot be the same type parameter as the right generic at .Arg[2].Arg[Variadic])" - ); -} - -TEST_CASE_FIXTURE(DifferFixture, "equal_generictp_cyclic") -{ - CheckResult result = check(R"( - function foo(f, g, s, x) - f(f(g(g(s(x))))) - return foo - end - function almostFoo(f, g, s, x) - g(g(f(f(s(x))))) - return almostFoo - end - )"); - LUAU_REQUIRE_NO_ERRORS(result); - - INFO(Luau::toString(requireType("foo"))); - INFO(Luau::toString(requireType("almostFoo"))); - - compareTypesEq("foo", "almostFoo"); -} - -TEST_CASE_FIXTURE(DifferFixture, "symbol_forward") -{ - CheckResult result = check(R"( - local foo = 5 - local almostFoo = "five" - )"); - LUAU_REQUIRE_NO_ERRORS(result); - - INFO(Luau::toString(requireType("foo"))); - INFO(Luau::toString(requireType("almostFoo"))); - - compareTypesNe( - "foo", - "almostFoo", - R"(DiffError: these two types are not equal because the left type at foo has type number, while the right type at almostFoo has type string)", - true - ); -} - -TEST_CASE_FIXTURE(DifferFixture, "newlines") -{ - CheckResult result = check(R"( - local foo = 5 - local almostFoo = "five" - )"); - LUAU_REQUIRE_NO_ERRORS(result); - - INFO(Luau::toString(requireType("foo"))); - INFO(Luau::toString(requireType("almostFoo"))); - - compareTypesNe( - "foo", - "almostFoo", - R"(DiffError: these two types are not equal because the left type at - foo -has type - number, -while the right type at - almostFoo -has type - string)", - true, - true - ); -} - -TEST_SUITE_END(); diff --git a/tests/Fixture.h b/tests/Fixture.h index cca3aec4..91d126bf 100644 --- a/tests/Fixture.h +++ b/tests/Fixture.h @@ -2,7 +2,6 @@ #pragma once #include "Luau/Config.h" -#include "Luau/Differ.h" #include "Luau/Error.h" #include "Luau/FileResolver.h" #include "Luau/Frontend.h" @@ -30,6 +29,7 @@ LUAU_FASTFLAG(DebugLuauForceAllNewSolverTests) LUAU_FASTFLAG(LuauTypeFunOptional) LUAU_FASTFLAG(LuauUpdateSetMetatableTypeSignature) +LUAU_FASTFLAG(LuauUpdateGetMetatableTypeSignature) #define DOES_NOT_PASS_NEW_SOLVER_GUARD_IMPL(line) ScopedFastFlag sff_##line{FFlag::LuauSolverV2, FFlag::DebugLuauForceAllNewSolverTests}; @@ -150,6 +150,7 @@ struct Fixture // In that case, flag can be forced to 'true' using the example below: // ScopedFastFlag sff_LuauExampleFlagDefinition{FFlag::LuauExampleFlagDefinition, true}; ScopedFastFlag sff_LuauUpdateSetMetatableTypeSignature{FFlag::LuauUpdateSetMetatableTypeSignature, true}; + ScopedFastFlag sff_LuauUpdateGetMetatableTypeSignature{FFlag::LuauUpdateGetMetatableTypeSignature, true}; // Arena freezing marks the `TypeArena`'s underlying memory as read-only, raising an access violation whenever you mutate it. // This is useful for tracking down violations of Luau's memory model. @@ -234,84 +235,6 @@ const E* findError(const CheckResult& result) return nullptr; } -template -struct DifferFixtureGeneric : BaseFixture -{ - std::string normalizeWhitespace(std::string msg) - { - std::string normalizedMsg = ""; - bool wasWhitespace = true; - for (char c : msg) - { - bool isWhitespace = c == ' ' || c == '\n'; - if (wasWhitespace && isWhitespace) - continue; - normalizedMsg += isWhitespace ? ' ' : c; - wasWhitespace = isWhitespace; - } - if (wasWhitespace) - normalizedMsg.pop_back(); - return normalizedMsg; - } - - void compareNe(TypeId left, TypeId right, const std::string& expectedMessage, bool multiLine) - { - compareNe(left, std::nullopt, right, std::nullopt, expectedMessage, multiLine); - } - - void compareNe( - TypeId left, - std::optional symbolLeft, - TypeId right, - std::optional symbolRight, - const std::string& expectedMessage, - bool multiLine - ) - { - DifferResult diffRes = diffWithSymbols(left, right, symbolLeft, symbolRight); - REQUIRE_MESSAGE(diffRes.diffError.has_value(), "Differ did not report type error, even though types are unequal"); - std::string diffMessage = diffRes.diffError->toString(multiLine); - CHECK_EQ(expectedMessage, diffMessage); - } - - void compareTypesNe( - const std::string& leftSymbol, - const std::string& rightSymbol, - const std::string& expectedMessage, - bool forwardSymbol = false, - bool multiLine = false - ) - { - if (forwardSymbol) - { - compareNe( - BaseFixture::requireType(leftSymbol), leftSymbol, BaseFixture::requireType(rightSymbol), rightSymbol, expectedMessage, multiLine - ); - } - else - { - compareNe( - BaseFixture::requireType(leftSymbol), std::nullopt, BaseFixture::requireType(rightSymbol), std::nullopt, expectedMessage, multiLine - ); - } - } - - void compareEq(TypeId left, TypeId right) - { - DifferResult diffRes = diff(left, right); - CHECK(!diffRes.diffError); - if (diffRes.diffError) - INFO(diffRes.diffError->toString()); - } - - void compareTypesEq(const std::string& leftSymbol, const std::string& rightSymbol) - { - compareEq(BaseFixture::requireType(leftSymbol), BaseFixture::requireType(rightSymbol)); - } -}; -using DifferFixture = DifferFixtureGeneric; -using DifferFixtureWithBuiltins = DifferFixtureGeneric; - } // namespace Luau #define LUAU_REQUIRE_ERRORS(result) \ diff --git a/tests/FragmentAutocomplete.test.cpp b/tests/FragmentAutocomplete.test.cpp index 20b337f7..04ff813c 100644 --- a/tests/FragmentAutocomplete.test.cpp +++ b/tests/FragmentAutocomplete.test.cpp @@ -3406,7 +3406,6 @@ local foo = 8)"); CHECK(*pos == Position{2, 0}); } -#if 0 TEST_CASE_FIXTURE(FragmentAutocompleteBuiltinsFixture, "TypeCorrectLocalReturn_assert") { const std::string source = R"()"; @@ -3452,7 +3451,6 @@ return target(bar)"; } ); } -#endif TEST_CASE_FIXTURE(FragmentAutocompleteBuiltinsFixture, "str_metata_table_finished_defining") { diff --git a/tests/Generalization.test.cpp b/tests/Generalization.test.cpp index 019e5ce0..4e2dafdf 100644 --- a/tests/Generalization.test.cpp +++ b/tests/Generalization.test.cpp @@ -15,9 +15,9 @@ using namespace Luau; LUAU_FASTFLAG(LuauSolverV2) -LUAU_FASTFLAG(LuauEagerGeneralization2) +LUAU_FASTFLAG(LuauEagerGeneralization3) LUAU_FASTFLAG(DebugLuauForbidInternalTypes) -LUAU_FASTFLAG(LuauTrackInferredFunctionTypeFromCall) +LUAU_FASTFLAG(LuauAvoidGenericsLeakingDuringFunctionCallCheck) TEST_SUITE_BEGIN("Generalization"); @@ -227,7 +227,7 @@ TEST_CASE_FIXTURE(GeneralizationFixture, "('a) -> 'a") TEST_CASE_FIXTURE(GeneralizationFixture, "(t1, (t1 <: 'b)) -> () where t1 = ('a <: (t1 <: 'b) & {number} & {number})") { - ScopedFastFlag sff{FFlag::LuauEagerGeneralization2, true}; + ScopedFastFlag sff{FFlag::LuauEagerGeneralization3, true}; TableType tt; tt.indexer = TableIndexer{builtinTypes.numberType, builtinTypes.numberType}; @@ -261,7 +261,7 @@ TEST_CASE_FIXTURE(GeneralizationFixture, "(('a <: number | string)) -> string?") TEST_CASE_FIXTURE(GeneralizationFixture, "(('a <: {'b})) -> ()") { - ScopedFastFlag sff{FFlag::LuauEagerGeneralization2, true}; + ScopedFastFlag sff{FFlag::LuauEagerGeneralization3, true}; auto [aTy, aFree] = freshType(); auto [bTy, bFree] = freshType(); @@ -342,7 +342,7 @@ end TEST_CASE_FIXTURE(BuiltinsFixture, "generalization_should_not_leak_free_type") { - ScopedFastFlag sffs[] = {{FFlag::DebugLuauForbidInternalTypes, true}, {FFlag::LuauTrackInferredFunctionTypeFromCall, true}}; + ScopedFastFlag _{FFlag::DebugLuauForbidInternalTypes, true}; // This test case should just not assert CheckResult result = check(R"( @@ -374,4 +374,93 @@ TEST_CASE_FIXTURE(BuiltinsFixture, "generalization_should_not_leak_free_type") )"); } +TEST_CASE_FIXTURE(Fixture, "generics_dont_leak_into_callback") +{ + ScopedFastFlag sffs[] = { + {FFlag::LuauSolverV2, true}, + {FFlag::LuauAvoidGenericsLeakingDuringFunctionCallCheck, true}, + }; + + LUAU_REQUIRE_NO_ERRORS(check(R"( + local func: (T, (T) -> ()) -> () = nil :: any + func({}, function(obj) + local _ = obj + end) + )")); + + // `unknown` is correct here + // - The lambda given can be generalized to `(unknown) -> ()` + // - We can substitute the `T` in `func` for either `{}` or `unknown` and + // still have a well typed program. + // We *probably* can do a better job bidirectionally inferring the types. + CHECK_EQ("unknown", toString(requireTypeAtPosition(Position{3, 23}))); +} + +TEST_CASE_FIXTURE(Fixture, "generics_dont_leak_into_callback_2") +{ + ScopedFastFlag sffs[] = { + {FFlag::LuauSolverV2, true}, + {FFlag::LuauAvoidGenericsLeakingDuringFunctionCallCheck, true}, + }; + + // FIXME: CLI-156389: this is clearly wrong, but also predates this PR. + LUAU_REQUIRE_NO_ERRORS(check(R"( + local func: (T, (T) -> ()) -> () = nil :: any + local foobar: (number) -> () = nil :: any + func({}, function(obj) + foobar(obj) + end) + )")); +} + +TEST_CASE_FIXTURE(Fixture, "generic_argument_with_singleton_oss_1808") +{ + ScopedFastFlag _{FFlag::LuauAvoidGenericsLeakingDuringFunctionCallCheck, true}; + // All we care about here is that this has no errors, and we correctly + // infer that the `false` literal should be typed as `false`. + LUAU_REQUIRE_NO_ERRORS(check(R"( + local function test(value: false | (T) -> T) + return value + end + test(false) + )")); +} + +TEST_CASE_FIXTURE(BuiltinsFixture, "avoid_cross_module_mutation_in_bidirectional_inference") +{ + ScopedFastFlag sffs[] = { + {FFlag::LuauAvoidGenericsLeakingDuringFunctionCallCheck, true}, + {FFlag::LuauEagerGeneralization3, true}, + }; + + fileResolver.source["Module/ListFns"] = R"( + local mod = {} + function mod.findWhere(list, predicate): number? + for i = 1, #list do + if predicate(list[i], i) then + return i + end + end + return nil + end + return mod + )"; + + fileResolver.source["Module/B"] = R"( + local funs = require(script.Parent.ListFns) + local accessories = funs.findWhere(getList(), function(accessory) + return accessory.AccessoryType ~= accessoryTypeEnum + end) + return {} + )"; + + CheckResult result = frontend.check("Module/ListFns"); + auto modListFns = frontend.moduleResolver.getModule("Module/ListFns"); + freeze(modListFns->interfaceTypes); + freeze(modListFns->internalTypes); + LUAU_REQUIRE_NO_ERRORS(result); + CheckResult result2 = frontend.check("Module/B"); + LUAU_REQUIRE_NO_ERRORS(result); +} + TEST_SUITE_END(); diff --git a/tests/InferPolarity.test.cpp b/tests/InferPolarity.test.cpp index d2b9cadc..d62b8b84 100644 --- a/tests/InferPolarity.test.cpp +++ b/tests/InferPolarity.test.cpp @@ -8,13 +8,13 @@ using namespace Luau; -LUAU_FASTFLAG(LuauEagerGeneralization2); +LUAU_FASTFLAG(LuauEagerGeneralization3); TEST_SUITE_BEGIN("InferPolarity"); TEST_CASE_FIXTURE(Fixture, "T where T = { m: (a) -> T }") { - ScopedFastFlag sff{FFlag::LuauEagerGeneralization2, true}; + ScopedFastFlag sff{FFlag::LuauEagerGeneralization3, true}; TypeArena arena; ScopePtr globalScope = std::make_shared(builtinTypes->anyTypePack); diff --git a/tests/Linter.test.cpp b/tests/Linter.test.cpp index b6156d03..cbb5c3cf 100644 --- a/tests/Linter.test.cpp +++ b/tests/Linter.test.cpp @@ -10,7 +10,7 @@ LUAU_FASTFLAG(LuauSolverV2); LUAU_FASTFLAG(LintRedundantNativeAttribute); LUAU_FASTFLAG(LuauDeprecatedAttribute); -LUAU_FASTFLAG(LuauEagerGeneralization2); +LUAU_FASTFLAG(LuauEagerGeneralization3); using namespace Luau; @@ -1941,9 +1941,6 @@ print(foo:bar(2.0)) TEST_CASE_FIXTURE(BuiltinsFixture, "TableOperations") { - // FIXME: For now this flag causes a stack overflow on Windows. - ScopedFastFlag _{FFlag::LuauEagerGeneralization2, false}; - LintResult result = lint(R"( local t = {} local tt = {} diff --git a/tests/Normalize.test.cpp b/tests/Normalize.test.cpp index 40f01048..a48fd28b 100644 --- a/tests/Normalize.test.cpp +++ b/tests/Normalize.test.cpp @@ -14,14 +14,9 @@ LUAU_FASTFLAG(LuauSolverV2) LUAU_FASTINT(LuauTypeInferRecursionLimit) LUAU_FASTINT(LuauNormalizeIntersectionLimit) LUAU_FASTINT(LuauNormalizeUnionLimit) -LUAU_FASTFLAG(LuauEagerGeneralization2) -LUAU_FASTFLAG(LuauRefineWaitForBlockedTypesInTarget) -LUAU_FASTFLAG(LuauSimplifyOutOfLine) -LUAU_FASTFLAG(LuauOptimizeFalsyAndTruthyIntersect) +LUAU_FASTFLAG(LuauSimplifyOutOfLine2) LUAU_FASTFLAG(LuauClipVariadicAnysFromArgsToGenericFuncs2) -LUAU_FASTFLAG(LuauSubtypeGenericsAndNegations) -LUAU_FASTFLAG(LuauNoMoreInjectiveTypeFunctions) -LUAU_FASTFLAG(LuauSubtypeGenericsAndNegations) +LUAU_FASTFLAG(LuauNormalizationReorderFreeTypeIntersect) using namespace Luau; @@ -1071,6 +1066,26 @@ TEST_CASE_FIXTURE(NormalizeFixture, "free_type_and_not_truthy") CHECK("'a & (false?)" == toString(result)); } +TEST_CASE_FIXTURE(NormalizeFixture, "free_type_intersection_ordering") +{ + ScopedFastFlag sff[] = { + {FFlag::LuauSolverV2, true}, // Affects stringification of free types. + {FFlag::LuauNormalizationReorderFreeTypeIntersect, true}, + }; + + TypeId freeTy = arena.freshType(builtinTypes, &globalScope); + TypeId orderA = arena.addType(IntersectionType{{freeTy, builtinTypes->stringType}}); + auto normA = normalizer.normalize(orderA); + REQUIRE(normA); + CHECK_EQ("'a & string", toString(normalizer.typeFromNormal(*normA))); + + TypeId orderB = arena.addType(IntersectionType{{builtinTypes->stringType, freeTy}}); + auto normB = normalizer.normalize(orderB); + REQUIRE(normB); + // Prior to LuauNormalizationReorderFreeTypeIntersect this became `never` :skull: + CHECK_EQ("'a & string", toString(normalizer.typeFromNormal(*normB))); +} + TEST_CASE_FIXTURE(BuiltinsFixture, "normalizer_should_be_able_to_detect_cyclic_tables_and_not_stack_overflow") { if (!FFlag::LuauSolverV2) @@ -1200,13 +1215,8 @@ TEST_CASE_FIXTURE(BuiltinsFixture, "fuzz_union_type_pack_cycle") { ScopedFastFlag sff[] = { {FFlag::LuauSolverV2, true}, - {FFlag::LuauRefineWaitForBlockedTypesInTarget, true}, - {FFlag::LuauSimplifyOutOfLine, true}, - {FFlag::LuauSubtypeGenericsAndNegations, true}, - {FFlag::LuauNoMoreInjectiveTypeFunctions, true}, - {FFlag::LuauOptimizeFalsyAndTruthyIntersect, true}, + {FFlag::LuauSimplifyOutOfLine2, true}, {FFlag::LuauClipVariadicAnysFromArgsToGenericFuncs2, true}, - {FFlag::LuauEagerGeneralization2, true} }; ScopedFastInt sfi{FInt::LuauTypeInferRecursionLimit, 0}; diff --git a/tests/Subtyping.test.cpp b/tests/Subtyping.test.cpp index db0cd61e..aba64a4a 100644 --- a/tests/Subtyping.test.cpp +++ b/tests/Subtyping.test.cpp @@ -16,8 +16,7 @@ #include LUAU_FASTFLAG(LuauSolverV2) -LUAU_FASTFLAG(LuauSubtypeGenericsAndNegations) -LUAU_FASTFLAG(LuauEagerGeneralization2) +LUAU_FASTFLAG(LuauEagerGeneralization3) using namespace Luau; @@ -1619,8 +1618,6 @@ TEST_CASE_FIXTURE(SubtypeFixture, "multiple_reasonings") TEST_CASE_FIXTURE(SubtypeFixture, "substitute_a_generic_for_a_negation") { - ScopedFastFlag sff{FFlag::LuauSubtypeGenericsAndNegations, true}; - // (x: A, y: B) -> (A & ~(false?)) | B // (~(false?), ~(false?)) -> (~(false?) & ~(false?)) | ~(false?) @@ -1644,7 +1641,7 @@ TEST_CASE_FIXTURE(SubtypeFixture, "substitute_a_generic_for_a_negation") TEST_CASE_FIXTURE(SubtypeFixture, "free_types_might_be_subtypes") { - ScopedFastFlag sff{FFlag::LuauEagerGeneralization2, true}; + ScopedFastFlag sff{FFlag::LuauEagerGeneralization3, true}; TypeId argTy = arena.freshType(builtinTypes, moduleScope.get()); FreeType* freeArg = getMutable(argTy); diff --git a/tests/TypeFunction.test.cpp b/tests/TypeFunction.test.cpp index e383570b..9aacb315 100644 --- a/tests/TypeFunction.test.cpp +++ b/tests/TypeFunction.test.cpp @@ -14,9 +14,8 @@ using namespace Luau; LUAU_FASTFLAG(LuauSolverV2) LUAU_DYNAMIC_FASTINT(LuauTypeFamilyApplicationCartesianProductLimit) -LUAU_FASTFLAG(LuauEagerGeneralization2) -LUAU_FASTFLAG(LuauHasPropProperBlock) -LUAU_FASTFLAG(LuauSimplifyOutOfLine) +LUAU_FASTFLAG(LuauEagerGeneralization3) +LUAU_FASTFLAG(LuauSimplifyOutOfLine2) LUAU_FASTFLAG(LuauTableLiteralSubtypeSpecificCheck) LUAU_FASTFLAG(LuauErrorSuppressionTypeFunctionArgs) @@ -159,7 +158,7 @@ TEST_CASE_FIXTURE(TypeFunctionFixture, "unsolvable_function") TEST_CASE_FIXTURE(TypeFunctionFixture, "table_internal_functions") { - ScopedFastFlag _{FFlag::LuauSimplifyOutOfLine, true}; + ScopedFastFlag _{FFlag::LuauSimplifyOutOfLine2, true}; if (!FFlag::LuauSolverV2) return; @@ -1652,7 +1651,8 @@ type foo = { a: add, b : add } TEST_CASE_FIXTURE(BuiltinsFixture, "len_typefun_on_metatable") { - ScopedFastFlag newSolver{FFlag::LuauSolverV2, true}; + if (!FFlag::LuauSolverV2) + return; CheckResult result = check(R"( local t = setmetatable({}, { __mode = "v" }) @@ -1669,7 +1669,6 @@ end TEST_CASE_FIXTURE(BuiltinsFixture, "has_prop_on_irreducible_type_function") { ScopedFastFlag newSolver{FFlag::LuauSolverV2, true}; - ScopedFastFlag luauHasPropProperBlock{FFlag::LuauHasPropProperBlock, true}; CheckResult result = check(R"( local test = "a" + "b" @@ -1725,7 +1724,7 @@ struct TFFixture TypeFunctionRuntime runtime{NotNull{&ice}, NotNull{&limits}}; const ScopedFastFlag sff[1] = { - {FFlag::LuauEagerGeneralization2, true}, + {FFlag::LuauEagerGeneralization3, true}, }; BuiltinTypeFunctions builtinTypeFunctions; diff --git a/tests/TypeFunction.user.test.cpp b/tests/TypeFunction.user.test.cpp index 366063f4..cd8ae817 100644 --- a/tests/TypeFunction.user.test.cpp +++ b/tests/TypeFunction.user.test.cpp @@ -9,7 +9,7 @@ using namespace Luau; LUAU_FASTFLAG(LuauSolverV2) LUAU_FASTFLAG(DebugLuauEqSatSimplification) -LUAU_FASTFLAG(LuauEagerGeneralization2) +LUAU_FASTFLAG(LuauEagerGeneralization3) LUAU_FASTFLAG(LuauTableLiteralSubtypeSpecificCheck) LUAU_FASTFLAG(LuauUserTypeFunctionAliases) @@ -409,7 +409,9 @@ TEST_CASE_FIXTURE(BuiltinsFixture, "udtf_optional_works_on_unions") TEST_CASE_FIXTURE(BuiltinsFixture, "udtf_union_methods_work") { - ScopedFastFlag newSolver{FFlag::LuauSolverV2, true}; + if (!FFlag::LuauSolverV2) + return; + ScopedFastFlag _{FFlag::LuauTableLiteralSubtypeSpecificCheck, true}; CheckResult result = check(R"( @@ -458,7 +460,9 @@ TEST_CASE_FIXTURE(BuiltinsFixture, "udtf_intersection_serialization_works") TEST_CASE_FIXTURE(BuiltinsFixture, "udtf_intersection_methods_work") { - ScopedFastFlag newSolver{FFlag::LuauSolverV2, true}; + if (!FFlag::LuauSolverV2) + return; + ScopedFastFlag _{FFlag::LuauTableLiteralSubtypeSpecificCheck, true}; CheckResult result = check(R"( @@ -649,7 +653,9 @@ TEST_CASE_FIXTURE(BuiltinsFixture, "udtf_function_serialization_works") TEST_CASE_FIXTURE(BuiltinsFixture, "udtf_function_methods_work") { - ScopedFastFlag newSolver{FFlag::LuauSolverV2, true}; + if (!FFlag::LuauSolverV2) + return; + ScopedFastFlag _{FFlag::LuauTableLiteralSubtypeSpecificCheck, true}; CheckResult result = check(R"( @@ -892,7 +898,8 @@ TEST_CASE_FIXTURE(BuiltinsFixture, "udtf_complex_cyclic_serialization_works") TEST_CASE_FIXTURE(BuiltinsFixture, "udtf_user_error_is_reported") { - ScopedFastFlag newSolver{FFlag::LuauSolverV2, true}; + if (!FFlag::LuauSolverV2) + return; CheckResult result = check(R"( type function errors_if_string(arg) @@ -913,7 +920,8 @@ TEST_CASE_FIXTURE(BuiltinsFixture, "udtf_user_error_is_reported") TEST_CASE_FIXTURE(BuiltinsFixture, "udtf_type_overrides_call_metamethod") { - ScopedFastFlag newSolver{FFlag::LuauSolverV2, true}; + if (!FFlag::LuauSolverV2) + return; CheckResult result = check(R"( type function hello(arg) @@ -1098,7 +1106,8 @@ TEST_CASE_FIXTURE(BuiltinsFixture, "udtf_no_shared_state") TEST_CASE_FIXTURE(BuiltinsFixture, "udtf_math_reset") { - ScopedFastFlag newSolver{FFlag::LuauSolverV2, true}; + if (!FFlag::LuauSolverV2) + return; CheckResult result = check(R"( type function foo(x) @@ -1112,7 +1121,9 @@ TEST_CASE_FIXTURE(BuiltinsFixture, "udtf_math_reset") TEST_CASE_FIXTURE(BuiltinsFixture, "udtf_optionify") { - ScopedFastFlag newSolver{FFlag::LuauSolverV2, true}; + if (!FFlag::LuauSolverV2) + return; + ScopedFastFlag _{FFlag::LuauTableLiteralSubtypeSpecificCheck, true}; CheckResult result = check(R"( @@ -1164,7 +1175,9 @@ TEST_CASE_FIXTURE(BuiltinsFixture, "udtf_calling_illegal_global") TEST_CASE_FIXTURE(BuiltinsFixture, "udtf_recursion_and_gc") { - ScopedFastFlag newSolver{FFlag::LuauSolverV2, true}; + if (!FFlag::LuauSolverV2) + return; + ScopedFastFlag _{FFlag::LuauTableLiteralSubtypeSpecificCheck, true}; CheckResult result = check(R"( @@ -1211,7 +1224,8 @@ TEST_CASE_FIXTURE(BuiltinsFixture, "udtf_recovery_no_upvalues") TEST_CASE_FIXTURE(BuiltinsFixture, "udtf_follow") { - ScopedFastFlag solverV2{FFlag::LuauSolverV2, true}; + if (!FFlag::LuauSolverV2) + return; CheckResult result = check(R"( type t0 = any @@ -1226,7 +1240,9 @@ TEST_CASE_FIXTURE(BuiltinsFixture, "udtf_follow") TEST_CASE_FIXTURE(BuiltinsFixture, "udtf_strip_indexer") { - ScopedFastFlag newSolver{FFlag::LuauSolverV2, true}; + if (!FFlag::LuauSolverV2) + return; + ScopedFastFlag _{FFlag::LuauTableLiteralSubtypeSpecificCheck, true}; CheckResult result = check(R"( @@ -1251,7 +1267,8 @@ TEST_CASE_FIXTURE(BuiltinsFixture, "udtf_strip_indexer") TEST_CASE_FIXTURE(BuiltinsFixture, "no_type_methods_on_types") { - ScopedFastFlag newSolver{FFlag::LuauSolverV2, true}; + if (!FFlag::LuauSolverV2) + return; CheckResult result = check(R"( type function test(x) @@ -1377,7 +1394,8 @@ local a: foo<> = "a" TEST_CASE_FIXTURE(BuiltinsFixture, "implicit_export") { - ScopedFastFlag newSolver{FFlag::LuauSolverV2, true}; + if (!FFlag::LuauSolverV2) + return; fileResolver.source["game/A"] = R"( type function concat(a: type, b: type) @@ -1430,7 +1448,8 @@ local a = test() TEST_CASE_FIXTURE(BuiltinsFixture, "explicit_export") { - ScopedFastFlag newSolver{FFlag::LuauSolverV2, true}; + if (!FFlag::LuauSolverV2) + return; fileResolver.source["game/A"] = R"( export type function concat(a: type, b: type) @@ -1478,7 +1497,8 @@ TEST_CASE_FIXTURE(BuiltinsFixture, "print_to_error") TEST_CASE_FIXTURE(BuiltinsFixture, "print_to_error_plus_error") { - ScopedFastFlag solverV2{FFlag::LuauSolverV2, true}; + if (!FFlag::LuauSolverV2) + return; CheckResult result = check(R"( type function t0(a) @@ -1665,7 +1685,8 @@ local function ok(idx: pass): (T, T) -> (T) return idx end TEST_CASE_FIXTURE(ExternTypeFixture, "udtf_generic_api_3") { - ScopedFastFlag newSolver{FFlag::LuauSolverV2, true}; + if (!FFlag::LuauSolverV2) + return; CheckResult result = check(R"( type function pass() @@ -1968,7 +1989,10 @@ local function ok(idx: pass): (number, ...string) -> (string, ...number) r TEST_CASE_FIXTURE(BuiltinsFixture, "udtf_eqsat_opaque") { - ScopedFastFlag sffs[] = {{FFlag::LuauSolverV2, true}, {FFlag::DebugLuauEqSatSimplification, true}}; + if (!FFlag::LuauSolverV2) + return; + + ScopedFastFlag sffs[] = {{FFlag::DebugLuauEqSatSimplification, true}}; CheckResult _ = check(R"( type function t0(a) @@ -1988,7 +2012,7 @@ TEST_CASE_FIXTURE(BuiltinsFixture, "udtf_singleton_equality_bool") { ScopedFastFlag newSolver{FFlag::LuauSolverV2, true}; - if (FFlag::LuauEagerGeneralization2) + if (FFlag::LuauEagerGeneralization3) { // FIXME: CLI-151985 // This test breaks because we can't see that eq is already fully reduced. @@ -2011,7 +2035,7 @@ TEST_CASE_FIXTURE(BuiltinsFixture, "udtf_singleton_equality_string") { ScopedFastFlag newSolver{FFlag::LuauSolverV2, true}; - if (FFlag::LuauEagerGeneralization2) + if (FFlag::LuauEagerGeneralization3) { // FIXME: CLI-151985 // This test breaks because we can't see that eq is already fully reduced. diff --git a/tests/TypeInfer.builtins.test.cpp b/tests/TypeInfer.builtins.test.cpp index 4688d122..a73ea82c 100644 --- a/tests/TypeInfer.builtins.test.cpp +++ b/tests/TypeInfer.builtins.test.cpp @@ -11,8 +11,9 @@ using namespace Luau; LUAU_FASTFLAG(LuauSolverV2) LUAU_FASTFLAG(LuauTableCloneClonesType3) -LUAU_FASTFLAG(LuauEagerGeneralization2) +LUAU_FASTFLAG(LuauEagerGeneralization3) LUAU_FASTFLAG(LuauArityMismatchOnUndersaturatedUnknownArguments) +LUAU_FASTFLAG(LuauStringFormatImprovements) TEST_SUITE_BEGIN("BuiltinTests"); @@ -459,9 +460,7 @@ TEST_CASE_FIXTURE(BuiltinsFixture, "table_pack_reduce") )"); LUAU_REQUIRE_NO_ERRORS(result); - if (FFlag::LuauSolverV2 && FFlag::LuauEagerGeneralization2) - CHECK("{ [number]: string | string | string, n: number }" == toString(requireType("t"))); - else if (FFlag::LuauSolverV2) + if (FFlag::LuauSolverV2) CHECK_EQ("{ [number]: string, n: number }", toString(requireType("t"))); else CHECK_EQ("{| [number]: string, n: number |}", toString(requireType("t"))); @@ -1665,4 +1664,57 @@ TEST_CASE_FIXTURE(BuiltinsFixture, "string_format_should_support_any") LUAU_REQUIRE_NO_ERRORS(result); } +TEST_CASE_FIXTURE(BuiltinsFixture, "string_format_should_support_any_2") +{ + ScopedFastFlag _{FFlag::LuauSolverV2, true}; + ScopedFastFlag sff{FFlag::LuauStringFormatImprovements, true}; + + CheckResult result = check(R"( + local fmt = "Hello, %s!" :: any + local x = "world" :: any + print(string.format(fmt, x)) + print(string.format(fmt, "hello")) + print(string.format(fmt, 5)) -- unchecked because the format string is `any`! + )"); + + LUAU_REQUIRE_NO_ERRORS(result); +} + +TEST_CASE_FIXTURE(BuiltinsFixture, "string_format_should_support_singleton_types") +{ + ScopedFastFlag _{FFlag::LuauSolverV2, true}; + ScopedFastFlag sff{FFlag::LuauStringFormatImprovements, true}; + + CheckResult result = check(R"( + local fmt: "Hello, %s!" = "Hello, %s!" + print(string.format(fmt, "hello")) + print(string.format(fmt, 5)) -- should still produce an error since the expected type is `string`! + )"); + + LUAU_REQUIRE_ERROR_COUNT(1, result); + TypeMismatch* tm = get(result.errors[0]); + REQUIRE(tm); + CHECK_EQ(tm->wantedType, builtinTypes->stringType); + CHECK_EQ(tm->givenType, builtinTypes->numberType); +} + +TEST_CASE_FIXTURE(BuiltinsFixture, "better_string_format_error_when_format_string_is_dynamic") +{ + ScopedFastFlag _{FFlag::LuauSolverV2, true}; + ScopedFastFlag sff{FFlag::LuauStringFormatImprovements, true}; + + CheckResult result = check(R"( + local fmt: string = "Hello, %s!" + print(string.format(fmt, "hello")) + print(string.format(fmt :: any, "hello")) -- no error + )"); + + LUAU_REQUIRE_ERROR_COUNT(1, result); + CHECK_EQ( + "We cannot statically check the type of `string.format` when called with a format string that is not statically known.\n" + "If you'd like to use an unchecked `string.format` call, you can cast the format string to `any` using `:: any`.", + toString(result.errors[0]) + ); +} + TEST_SUITE_END(); diff --git a/tests/TypeInfer.definitions.test.cpp b/tests/TypeInfer.definitions.test.cpp index 7fbee26d..d17e4369 100644 --- a/tests/TypeInfer.definitions.test.cpp +++ b/tests/TypeInfer.definitions.test.cpp @@ -10,7 +10,7 @@ using namespace Luau; LUAU_FASTINT(LuauTypeInferRecursionLimit) -LUAU_FASTFLAG(LuauSimplifyOutOfLine) +LUAU_FASTFLAG(LuauSimplifyOutOfLine2) TEST_SUITE_BEGIN("DefinitionTests"); @@ -558,7 +558,7 @@ TEST_CASE_FIXTURE(BuiltinsFixture, "cli_142285_reduce_minted_union_func") { ScopedFastFlag sffs[] = { {FFlag::LuauSolverV2, true}, - {FFlag::LuauSimplifyOutOfLine, true}, + {FFlag::LuauSimplifyOutOfLine2, true}, }; CheckResult result = check(R"( diff --git a/tests/TypeInfer.functions.test.cpp b/tests/TypeInfer.functions.test.cpp index a419e395..30962d9a 100644 --- a/tests/TypeInfer.functions.test.cpp +++ b/tests/TypeInfer.functions.test.cpp @@ -22,14 +22,13 @@ LUAU_FASTFLAG(LuauInstantiateInSubtyping) LUAU_FASTFLAG(LuauSolverV2) LUAU_FASTINT(LuauTarjanChildLimit) LUAU_FASTFLAG(DebugLuauEqSatSimplification) -LUAU_FASTFLAG(LuauEagerGeneralization2) +LUAU_FASTFLAG(LuauEagerGeneralization3) LUAU_FASTFLAG(LuauArityMismatchOnUndersaturatedUnknownArguments) -LUAU_FASTFLAG(LuauHasPropProperBlock) -LUAU_FASTFLAG(LuauOptimizeFalsyAndTruthyIntersect) LUAU_FASTFLAG(LuauFormatUseLastPosition) LUAU_FASTFLAG(LuauDoNotAddUpvalueTypesToLocalType) -LUAU_FASTFLAG(LuauSimplifyOutOfLine) +LUAU_FASTFLAG(LuauSimplifyOutOfLine2) LUAU_FASTFLAG(LuauTableLiteralSubtypeSpecificCheck) +LUAU_FASTFLAG(LuauAvoidGenericsLeakingDuringFunctionCallCheck) TEST_SUITE_BEGIN("TypeInferFunctions"); @@ -1432,6 +1431,10 @@ g12({x=1}, {x=2}, function(x, y) return {x=x.x + y.x} end) TEST_CASE_FIXTURE(BuiltinsFixture, "infer_generic_lib_function_function_argument") { + ScopedFastFlag sffs[] = { + {FFlag::LuauSolverV2, true}, + }; + CheckResult result = check(R"( local a = {{x=4}, {x=7}, {x=1}} table.sort(a, function(x, y) return x.x < y.x end) @@ -1683,7 +1686,7 @@ t.f = function(x) end )"); - if (FFlag::LuauEagerGeneralization2 && FFlag::LuauSolverV2) + if (FFlag::LuauEagerGeneralization3 && FFlag::LuauSolverV2) { // FIXME CLI-151985 LUAU_CHECK_ERROR_COUNT(3, result); @@ -1768,7 +1771,7 @@ t.f = function(x) end )"); - if (FFlag::LuauEagerGeneralization2 && FFlag::LuauSolverV2) + if (FFlag::LuauEagerGeneralization3 && FFlag::LuauSolverV2) { // FIXME CLI-151985 LUAU_CHECK_ERROR_COUNT(2, result); @@ -1945,8 +1948,6 @@ TEST_CASE_FIXTURE(Fixture, "free_is_not_bound_to_unknown") TEST_CASE_FIXTURE(Fixture, "dont_infer_parameter_types_for_functions_from_their_call_site") { - ScopedFastFlag sffs[] = {{FFlag::LuauHasPropProperBlock, true}, {FFlag::LuauOptimizeFalsyAndTruthyIntersect, true}}; - CheckResult result = check(R"( local t = {} @@ -1967,7 +1968,7 @@ TEST_CASE_FIXTURE(Fixture, "dont_infer_parameter_types_for_functions_from_their_ CHECK_EQ("(a) -> a", toString(requireType("f"))); - if (FFlag::LuauEagerGeneralization2 && FFlag::LuauSolverV2) + if (FFlag::LuauEagerGeneralization3 && FFlag::LuauSolverV2) { LUAU_CHECK_NO_ERRORS(result); CHECK("({ read p: { read q: a } }) -> (a & ~(false?))?" == toString(requireType("g"))); @@ -2910,9 +2911,8 @@ TEST_CASE_FIXTURE(Fixture, "fuzzer_missing_follow_in_ast_stat_fun") TEST_CASE_FIXTURE(Fixture, "unifier_should_not_bind_free_types") { ScopedFastFlag sffs[] = { - {FFlag::LuauSimplifyOutOfLine, true}, + {FFlag::LuauSimplifyOutOfLine2, true}, {FFlag::LuauTableLiteralSubtypeSpecificCheck, true}, - {FFlag::LuauOptimizeFalsyAndTruthyIntersect, true}, }; CheckResult result = check(R"( @@ -2932,7 +2932,7 @@ TEST_CASE_FIXTURE(Fixture, "unifier_should_not_bind_free_types") { // The new solver should ideally be able to do better here, but this is no worse than the old solver. - if (FFlag::LuauEagerGeneralization2) + if (FFlag::LuauEagerGeneralization3) { LUAU_REQUIRE_ERROR_COUNT(2, result); auto tm1 = get(result.errors[0]); @@ -3159,7 +3159,6 @@ TEST_CASE_FIXTURE(Fixture, "fuzz_unwind_mutually_recursive_union_type_func") TEST_CASE_FIXTURE(BuiltinsFixture, "string_format_pack") { - ScopedFastFlag _{FFlag::LuauFormatUseLastPosition, true}; LUAU_REQUIRE_NO_ERRORS(check(R"( local function foo(): (string, string, string) return "", "", "" @@ -3170,7 +3169,6 @@ TEST_CASE_FIXTURE(BuiltinsFixture, "string_format_pack") TEST_CASE_FIXTURE(BuiltinsFixture, "string_format_pack_variadic") { - ScopedFastFlag _{FFlag::LuauFormatUseLastPosition, true}; LUAU_REQUIRE_NO_ERRORS(check(R"( local foo : () -> (...string) = (nil :: any) print(string.format("%s %s %s", foo())) diff --git a/tests/TypeInfer.generics.test.cpp b/tests/TypeInfer.generics.test.cpp index d899201c..8df7421c 100644 --- a/tests/TypeInfer.generics.test.cpp +++ b/tests/TypeInfer.generics.test.cpp @@ -15,6 +15,10 @@ LUAU_FASTFLAG(LuauSolverV2) LUAU_FASTFLAG(LuauAddCallConstraintForIterableFunctions) LUAU_FASTFLAG(LuauClipVariadicAnysFromArgsToGenericFuncs2) LUAU_FASTFLAG(LuauTableLiteralSubtypeSpecificCheck) +LUAU_FASTFLAG(LuauIntersectNotNil) +LUAU_FASTFLAG(LuauSubtypingCheckFunctionGenericCounts) +LUAU_FASTFLAG(LuauReportSubtypingErrors) +LUAU_FASTFLAG(LuauEagerGeneralization3) using namespace Luau; @@ -780,7 +784,7 @@ TEST_CASE_FIXTURE(Fixture, "instantiated_function_argument_names_old_solver") TEST_CASE_FIXTURE(Fixture, "error_detailed_function_mismatch_generic_types") { - DOES_NOT_PASS_NEW_SOLVER_GUARD(); + ScopedFastFlag sffs[] = {{FFlag::LuauReportSubtypingErrors, true}, {FFlag::LuauSubtypingCheckFunctionGenericCounts, true}}; CheckResult result = check(R"( type C = () -> () @@ -790,14 +794,64 @@ local c: C local d: D = c )"); - LUAU_REQUIRE_ERROR_COUNT(1, result); + if (FFlag::LuauSolverV2) + { + LUAU_REQUIRE_ERROR_COUNT(2, result); + const auto genericMismatch = get(result.errors[0]); + CHECK(genericMismatch); + CHECK_EQ(genericMismatch->subTyGenericCount, 1); + CHECK_EQ(genericMismatch->superTyGenericCount, 0); - CHECK_EQ(toString(result.errors[0]), R"(Type '() -> ()' could not be converted into '() -> ()'; different number of generic type parameters)"); + auto mismatch = get(result.errors[1]); + CHECK(mismatch); + CHECK_EQ(toString(mismatch->givenType), "() -> ()"); + CHECK_EQ(toString(mismatch->wantedType), "() -> ()"); + } + else + { + LUAU_REQUIRE_ERROR_COUNT(1, result); + auto mismatch = get(result.errors[0]); + CHECK(mismatch); + CHECK_EQ(mismatch->reason, "different number of generic type parameters"); + } +} +TEST_CASE_FIXTURE(Fixture, "generic_function_mismatch_with_argument") +{ + ScopedFastFlag sffs[] = {{FFlag::LuauReportSubtypingErrors, true}, {FFlag::LuauSubtypingCheckFunctionGenericCounts, true}}; + + CheckResult result = check(R"( +type C = (number) -> () +type D = (number) -> () + +local c: C +local d: D = c + )"); + + if (FFlag::LuauSolverV2) + { + LUAU_REQUIRE_ERROR_COUNT(2, result); + const auto genericMismatch = get(result.errors[0]); + CHECK(genericMismatch); + CHECK_EQ(genericMismatch->subTyGenericCount, 1); + CHECK_EQ(genericMismatch->superTyGenericCount, 0); + + auto mismatch = get(result.errors[1]); + CHECK(mismatch); + CHECK_EQ(toString(mismatch->givenType), "(number) -> ()"); + CHECK_EQ(toString(mismatch->wantedType), "(number) -> ()"); + } + else + { + LUAU_REQUIRE_ERROR_COUNT(1, result); + auto mismatch = get(result.errors[0]); + CHECK(mismatch); + CHECK_EQ(mismatch->reason, "different number of generic type parameters"); + } } TEST_CASE_FIXTURE(Fixture, "error_detailed_function_mismatch_generic_pack") { - DOES_NOT_PASS_NEW_SOLVER_GUARD(); + ScopedFastFlag sffs[] = {{FFlag::LuauReportSubtypingErrors, true}, {FFlag::LuauSubtypingCheckFunctionGenericCounts, true}}; CheckResult result = check(R"( type C = () -> () @@ -807,12 +861,26 @@ local c: C local d: D = c )"); - LUAU_REQUIRE_ERROR_COUNT(1, result); + if (FFlag::LuauSolverV2) + { + LUAU_REQUIRE_ERROR_COUNT(2, result); + const auto genericMismatch = get(result.errors[0]); + CHECK(genericMismatch); + CHECK_EQ(genericMismatch->subTyGenericPackCount, 1); + CHECK_EQ(genericMismatch->superTyGenericPackCount, 0); - CHECK_EQ( - toString(result.errors[0]), - R"(Type '() -> ()' could not be converted into '() -> ()'; different number of generic type pack parameters)" - ); + auto mismatch = get(result.errors[1]); + CHECK(mismatch); + CHECK_EQ(toString(mismatch->givenType), "() -> ()"); + CHECK_EQ(toString(mismatch->wantedType), "() -> ()"); + } + else + { + LUAU_REQUIRE_ERROR_COUNT(1, result); + auto mismatch = get(result.errors[0]); + CHECK(mismatch); + CHECK_EQ(mismatch->reason, "different number of generic type pack parameters"); + } } TEST_CASE_FIXTURE(BuiltinsFixture, "generic_functions_dont_cache_type_parameters") @@ -966,7 +1034,7 @@ wrapper(test) TEST_CASE_FIXTURE(Fixture, "generic_argument_count_too_many") { - ScopedFastFlag sff{FFlag::LuauClipVariadicAnysFromArgsToGenericFuncs2, true}; + DOES_NOT_PASS_NEW_SOLVER_GUARD(); CheckResult result = check(R"( function test2(a: number, b: string) @@ -980,17 +1048,7 @@ wrapper(test2, 1, "", 3) )"); LUAU_REQUIRE_ERROR_COUNT(1, result); - if (FFlag::LuauSolverV2) - { - const CountMismatch* cm = get(result.errors[0]); - REQUIRE_MESSAGE(cm, "Expected CountMismatch but got " << result.errors[0]); - // TODO: CLI-152070 fix to expect 3 - CHECK_EQ(cm->expected, 1); - CHECK_EQ(cm->actual, 4); - CHECK_EQ(cm->context, CountMismatch::Arg); - } - else - CHECK_EQ(toString(result.errors[0]), R"(Argument count mismatch. Function 'wrapper' expects 3 arguments, but 4 are specified)"); + CHECK_EQ(toString(result.errors[0]), R"(Argument count mismatch. Function 'wrapper' expects 3 arguments, but 4 are specified)"); } TEST_CASE_FIXTURE(Fixture, "generic_argument_count_just_right") @@ -1364,19 +1422,13 @@ TEST_CASE_FIXTURE(Fixture, "infer_generic_function_function_argument_overloaded" LUAU_REQUIRE_NO_ERRORS(result); } -TEST_CASE_FIXTURE(BuiltinsFixture, "infer_generic_lib_function_function_argument") -{ - CheckResult result = check(R"( -local a = {{x=4}, {x=7}, {x=1}} -table.sort(a, function(x, y) return x.x < y.x end) - )"); - - LUAU_REQUIRE_NO_ERRORS(result); -} - TEST_CASE_FIXTURE(BuiltinsFixture, "do_not_infer_generic_functions") { - + ScopedFastFlag _[] = { + {FFlag::LuauReportSubtypingErrors, true}, + {FFlag::LuauSubtypingCheckFunctionGenericCounts, true}, + {FFlag::LuauEagerGeneralization3, true}, + }; CheckResult result; if (FFlag::LuauSolverV2) @@ -1392,7 +1444,8 @@ TEST_CASE_FIXTURE(BuiltinsFixture, "do_not_infer_generic_functions") local c = sumrec(function(x, y, f) return f(x, y) end) -- type binders are not inferred )"); - LUAU_REQUIRE_NO_ERRORS(result); + CHECK_EQ("(a, a, (a, a) -> a) -> a", toString(requireType("sum"))); + CHECK_EQ("(a, a, (a, a) -> a) -> a", toString(requireTypeAtPosition({7, 29}))); } else { @@ -1406,9 +1459,9 @@ TEST_CASE_FIXTURE(BuiltinsFixture, "do_not_infer_generic_functions") local b = sumrec(sum) -- ok local c = sumrec(function(x, y, f) return f(x, y) end) -- type binders are not inferred )"); - - LUAU_REQUIRE_NO_ERRORS(result); } + + LUAU_REQUIRE_NO_ERRORS(result); } diff --git a/tests/TypeInfer.loops.test.cpp b/tests/TypeInfer.loops.test.cpp index 604ec0d3..482b090b 100644 --- a/tests/TypeInfer.loops.test.cpp +++ b/tests/TypeInfer.loops.test.cpp @@ -16,7 +16,7 @@ using namespace Luau; LUAU_FASTFLAG(LuauSolverV2) LUAU_FASTFLAG(LuauAddCallConstraintForIterableFunctions) -LUAU_FASTFLAG(LuauSimplifyOutOfLine) +LUAU_FASTFLAG(LuauSimplifyOutOfLine2) LUAU_FASTFLAG(LuauDfgIfBlocksShouldRespectControlFlow) LUAU_FASTFLAG(LuauDfgAllowUpdatesInLoops) @@ -188,7 +188,7 @@ TEST_CASE_FIXTURE(BuiltinsFixture, "for_in_loop_with_next_and_multiple_elements" { ScopedFastFlag sffs[] = { {FFlag::LuauAddCallConstraintForIterableFunctions, true}, - {FFlag::LuauSimplifyOutOfLine, true}, + {FFlag::LuauSimplifyOutOfLine2, true}, {FFlag::LuauDfgAllowUpdatesInLoops, true}, }; @@ -1407,8 +1407,10 @@ TEST_CASE_FIXTURE(BuiltinsFixture, "oss_1413") TEST_CASE_FIXTURE(BuiltinsFixture, "while_loop_error_in_body") { + if (!FFlag::LuauSolverV2) + return; + ScopedFastFlag sffs[] = { - {FFlag::LuauSolverV2, true}, {FFlag::LuauDfgAllowUpdatesInLoops, true}, }; @@ -1499,8 +1501,10 @@ TEST_CASE_FIXTURE(BuiltinsFixture, "repeat_unconditionally_fires_error") TEST_CASE_FIXTURE(BuiltinsFixture, "repeat_is_linearish") { + if (!FFlag::LuauSolverV2) + return; + ScopedFastFlag sffs[] = { - {FFlag::LuauSolverV2, true}, {FFlag::LuauDfgIfBlocksShouldRespectControlFlow, true}, {FFlag::LuauDfgAllowUpdatesInLoops, true}, }; diff --git a/tests/TypeInfer.modules.test.cpp b/tests/TypeInfer.modules.test.cpp index cfad64d5..e97f0645 100644 --- a/tests/TypeInfer.modules.test.cpp +++ b/tests/TypeInfer.modules.test.cpp @@ -13,8 +13,7 @@ LUAU_FASTFLAG(LuauInstantiateInSubtyping) LUAU_FASTFLAG(LuauSolverV2) LUAU_FASTFLAG(LuauAddCallConstraintForIterableFunctions) -LUAU_FASTFLAG(LuauEagerGeneralization2) -LUAU_FASTFLAG(LuauOptimizeFalsyAndTruthyIntersect) +LUAU_FASTFLAG(LuauEagerGeneralization3) LUAU_FASTFLAG(LuauClipVariadicAnysFromArgsToGenericFuncs2) using namespace Luau; @@ -746,14 +745,7 @@ TEST_CASE_FIXTURE(BuiltinsFixture, "spooky_blocked_type_laundered_by_bound_type" local _ = require(game.A); )"); - if (FFlag::LuauAddCallConstraintForIterableFunctions && !FFlag::LuauOptimizeFalsyAndTruthyIntersect) - { - LUAU_REQUIRE_ERROR_COUNT(3, result); - } - else - { - LUAU_REQUIRE_NO_ERRORS(result); - } + LUAU_REQUIRE_NO_ERRORS(result); } TEST_CASE_FIXTURE(BuiltinsFixture, "leaky_generics") @@ -785,7 +777,7 @@ TEST_CASE_FIXTURE(BuiltinsFixture, "leaky_generics") LUAU_REQUIRE_NO_ERRORS(result); - if (FFlag::LuauEagerGeneralization2) + if (FFlag::LuauEagerGeneralization3) { CHECK_EQ("(unknown) -> unknown", toString(requireTypeAtPosition({13, 23}))); } diff --git a/tests/TypeInfer.operators.test.cpp b/tests/TypeInfer.operators.test.cpp index b8d2eb85..0938142e 100644 --- a/tests/TypeInfer.operators.test.cpp +++ b/tests/TypeInfer.operators.test.cpp @@ -17,7 +17,7 @@ using namespace Luau; LUAU_FASTFLAG(LuauSolverV2) -LUAU_FASTFLAG(LuauEagerGeneralization2) +LUAU_FASTFLAG(LuauEagerGeneralization3) TEST_SUITE_BEGIN("TypeInferOperators"); @@ -29,7 +29,7 @@ TEST_CASE_FIXTURE(Fixture, "or_joins_types") )"); LUAU_REQUIRE_NO_ERRORS(result); - if (FFlag::LuauEagerGeneralization2) + if (FFlag::LuauEagerGeneralization3) { // FIXME: Regression CHECK("(string & ~(false?)) | number" == toString(*requireType("s"))); @@ -51,7 +51,7 @@ TEST_CASE_FIXTURE(Fixture, "or_joins_types_with_no_extras") )"); LUAU_REQUIRE_NO_ERRORS(result); - if (FFlag::LuauEagerGeneralization2) + if (FFlag::LuauEagerGeneralization3) { // FIXME: Regression. CHECK("(string & ~(false?)) | number" == toString(*requireType("s"))); @@ -72,7 +72,7 @@ TEST_CASE_FIXTURE(Fixture, "or_joins_types_with_no_superfluous_union") )"); LUAU_REQUIRE_NO_ERRORS(result); - if (FFlag::LuauEagerGeneralization2) + if (FFlag::LuauEagerGeneralization3) { // FIXME: Regression CHECK("(string & ~(false?)) | string" == toString(requireType("s"))); @@ -634,7 +634,7 @@ TEST_CASE_FIXTURE(BuiltinsFixture, "typecheck_unary_minus_error") local a = -foo )"); - if (FFlag::LuauEagerGeneralization2 && FFlag::LuauSolverV2) + if (FFlag::LuauEagerGeneralization3 && FFlag::LuauSolverV2) { LUAU_REQUIRE_ERROR_COUNT(1, result); diff --git a/tests/TypeInfer.primitives.test.cpp b/tests/TypeInfer.primitives.test.cpp index 2c76f123..249341f6 100644 --- a/tests/TypeInfer.primitives.test.cpp +++ b/tests/TypeInfer.primitives.test.cpp @@ -1,14 +1,9 @@ // This file is part of the Luau programming language and is licensed under MIT License; see LICENSE.txt for details -#include "Luau/AstQuery.h" -#include "Luau/BuiltinDefinitions.h" -#include "Luau/Scope.h" -#include "Luau/TypeInfer.h" #include "Luau/Type.h" #include "Luau/VisitType.h" #include "Fixture.h" -#include "DiffAsserts.h" #include "doctest.h" @@ -32,7 +27,7 @@ TEST_CASE_FIXTURE(Fixture, "string_length") )"); LUAU_REQUIRE_NO_ERRORS(result); - CHECK_EQ_DIFF(builtinTypes->numberType, requireType("t")); + CHECK_EQ(builtinTypes->numberType, requireType("t")); } TEST_CASE_FIXTURE(Fixture, "string_index") diff --git a/tests/TypeInfer.refinements.test.cpp b/tests/TypeInfer.refinements.test.cpp index 1234591f..52d6fb6a 100644 --- a/tests/TypeInfer.refinements.test.cpp +++ b/tests/TypeInfer.refinements.test.cpp @@ -10,13 +10,13 @@ LUAU_FASTFLAG(LuauSolverV2) LUAU_FASTFLAG(DebugLuauEqSatSimplification) -LUAU_FASTFLAG(LuauEagerGeneralization2) +LUAU_FASTFLAG(LuauEagerGeneralization3) LUAU_FASTFLAG(LuauFunctionCallsAreNotNilable) -LUAU_FASTFLAG(LuauWeakNilRefinementType) LUAU_FASTFLAG(LuauAddCallConstraintForIterableFunctions) LUAU_FASTFLAG(LuauSimplificationTableExternType) LUAU_FASTFLAG(LuauBetterCannotCallFunctionPrimitive) LUAU_FASTFLAG(LuauTypeCheckerStricterIndexCheck) +LUAU_FASTFLAG(LuauNormalizationIntersectTablesPreservesExternTypes) LUAU_FASTFLAG(LuauAvoidDoubleNegation) using namespace Luau; @@ -690,8 +690,6 @@ TEST_CASE_FIXTURE(Fixture, "unknown_lvalue_is_not_synonymous_with_other_on_not_e TEST_CASE_FIXTURE(Fixture, "string_not_equal_to_string_or_nil") { - ScopedFastFlag _{FFlag::LuauWeakNilRefinementType, true}; - CheckResult result = check(R"( local t: {string} = {"hello"} @@ -748,10 +746,6 @@ TEST_CASE_FIXTURE(BuiltinsFixture, "type_narrow_to_vector") TEST_CASE_FIXTURE(BuiltinsFixture, "nonoptional_type_can_narrow_to_nil_if_sense_is_true") { - ScopedFastFlag sffs[] = { - {FFlag::LuauWeakNilRefinementType, true}, - }; - CheckResult result = check(R"( local t = {"hello"} local v = t[2] @@ -770,7 +764,7 @@ TEST_CASE_FIXTURE(BuiltinsFixture, "nonoptional_type_can_narrow_to_nil_if_sense_ LUAU_REQUIRE_NO_ERRORS(result); - if (FFlag::LuauEagerGeneralization2) + if (FFlag::LuauEagerGeneralization3) { CHECK("nil & string & unknown & unknown" == toString(requireTypeAtPosition({4, 24}))); // type(v) == "nil" CHECK("string & unknown & unknown & ~nil" == toString(requireTypeAtPosition({6, 24}))); // type(v) ~= "nil" @@ -1655,7 +1649,37 @@ TEST_CASE_FIXTURE(RefinementExternTypeFixture, "asserting_optional_properties_sh local pos = part1.Position )"); - if (FFlag::LuauSolverV2) + if (FFlag::LuauSolverV2 && !FFlag::LuauNormalizationIntersectTablesPreservesExternTypes) + { + // CLI-142467: this is a major regression that we need to address. + CHECK_EQ("never", toString(requireTypeAtPosition({3, 15}))); + CHECK_EQ("any", toString(requireTypeAtPosition({6, 29}))); + } + else + { + LUAU_REQUIRE_NO_ERRORS(result); + + CHECK_EQ("WeldConstraint", toString(requireTypeAtPosition({3, 15}))); + CHECK_EQ("Vector3", toString(requireTypeAtPosition({6, 29}))); + } +} + +TEST_CASE_FIXTURE(RefinementExternTypeFixture, "asserting_non_existent_properties_should_not_refine_extern_types_to_never") +{ + + CheckResult result = check(R"( + local weld: WeldConstraint = nil :: any + assert(weld.Part8) + print(weld) -- hover type should become `never` + assert(weld.Part8.Name == "RootPart") + local part8 = assert(weld.Part8) + local pos = part8.Position + )"); + + LUAU_REQUIRE_ERRORS(result); + CHECK_EQ(toString(result.errors[0]), "Key 'Part8' not found in class 'WeldConstraint'"); + + if (FFlag::LuauSolverV2 && !FFlag::LuauNormalizationIntersectTablesPreservesExternTypes) { // CLI-142467: this is a major regression that we need to address. CHECK_EQ("never", toString(requireTypeAtPosition({3, 15}))); @@ -1664,7 +1688,10 @@ TEST_CASE_FIXTURE(RefinementExternTypeFixture, "asserting_optional_properties_sh else { CHECK_EQ("WeldConstraint", toString(requireTypeAtPosition({3, 15}))); - CHECK_EQ("Vector3", toString(requireTypeAtPosition({6, 29}))); + if (FFlag::LuauSolverV2) + CHECK_EQ("any", toString(requireTypeAtPosition({6, 29}))); + else + CHECK_EQ("*error-type*", toString(requireTypeAtPosition({6, 29}))); } } @@ -2223,6 +2250,8 @@ TEST_CASE_FIXTURE(BuiltinsFixture, "luau_polyfill_isindexkey_refine_conjunction" TEST_CASE_FIXTURE(BuiltinsFixture, "luau_polyfill_isindexkey_refine_conjunction_variant") { + // FIXME CLI-141364: An underlying bug in normalization means the type of + // `isIndexKey` is platform dependent. CheckResult result = check(R"( local function isIndexKey(k, contiguousLength: number) return type(k) == "number" @@ -2231,7 +2260,6 @@ TEST_CASE_FIXTURE(BuiltinsFixture, "luau_polyfill_isindexkey_refine_conjunction_ and math.floor(k) == k -- no float keys end )"); - LUAU_REQUIRE_NO_ERRORS(result); } @@ -2505,7 +2533,6 @@ TEST_CASE_FIXTURE(BuiltinsFixture, "remove_recursive_upper_bound_when_generalizi { ScopedFastFlag sffs[] = { {FFlag::LuauSolverV2, true}, - {FFlag::LuauWeakNilRefinementType, true}, {FFlag::DebugLuauEqSatSimplification, true}, }; @@ -2517,7 +2544,8 @@ TEST_CASE_FIXTURE(BuiltinsFixture, "remove_recursive_upper_bound_when_generalizi end )")); - if (FFlag::LuauEagerGeneralization2) + if (FFlag::LuauEagerGeneralization3) + // FIXME CLI-114134. We need to simplify types more consistently. CHECK_EQ("nil & string & unknown", toString(requireTypeAtPosition({4, 24}))); else CHECK_EQ("nil", toString(requireTypeAtPosition({4, 24}))); @@ -2617,8 +2645,6 @@ TEST_CASE_FIXTURE(BuiltinsFixture, "oss_1528_method_calls_are_not_nillable") TEST_CASE_FIXTURE(Fixture, "oss_1687_equality_shouldnt_leak_nil") { - ScopedFastFlag _{FFlag::LuauWeakNilRefinementType, true}; - LUAU_REQUIRE_NO_ERRORS(check(R"( --!strict function returns_two(): number @@ -2639,7 +2665,7 @@ TEST_CASE_FIXTURE(Fixture, "oss_1687_equality_shouldnt_leak_nil") TEST_CASE_FIXTURE(BuiltinsFixture, "oss_1451") { - ScopedFastFlag sffs[] = {{FFlag::LuauSolverV2, true}, {FFlag::LuauWeakNilRefinementType, true}}; + ScopedFastFlag _{FFlag::LuauSolverV2, true}; LUAU_REQUIRE_NO_ERRORS(check(R"( type Part = { @@ -2740,4 +2766,37 @@ TEST_CASE_FIXTURE(BuiltinsFixture, "oss_1835") CHECK(get(result.errors[0])); } +TEST_CASE_FIXTURE(Fixture, "limit_complexity_of_arithmetic_type_functions" * doctest::timeout(0.5)) +{ + ScopedFastFlag _{FFlag::LuauSolverV2, true}; + + CheckResult result = check(R"( + local Hermite = {} + + function Hermite:__init(p0, p1, m0, m1) + self[1] = { + p0.x; + p0.y; + p0.z; + } + self[2] = { + m0.x; + m0.y; + m0.z; + } + self[3] = { + 3*(p1.x - p0.x) - 2*m0.x - m1.x; + 3*(p1.y - p0.y) - 2*m0.y - m1.y; + 3*(p1.z - p0.z) - 2*m0.z - m1.z; + } + end + + return Hermite + )"); + + // We do not care what the errors are, only that this type checks in a + // reasonable amount of time. + LUAU_REQUIRE_ERRORS(result); +} + TEST_SUITE_END(); diff --git a/tests/TypeInfer.tables.test.cpp b/tests/TypeInfer.tables.test.cpp index 1762a04e..1b755c0d 100644 --- a/tests/TypeInfer.tables.test.cpp +++ b/tests/TypeInfer.tables.test.cpp @@ -21,20 +21,16 @@ LUAU_FASTFLAG(LuauSolverV2) LUAU_FASTFLAG(LuauInstantiateInSubtyping) LUAU_FASTFLAG(LuauFixIndexerSubtypingOrdering) -LUAU_FASTFLAG(LuauEagerGeneralization2) -LUAU_FASTFLAG(LuauEagerGeneralization2) +LUAU_FASTFLAG(LuauEagerGeneralization3) LUAU_FASTFLAG(DebugLuauAssertOnForcedConstraint) -LUAU_FASTFLAG(LuauBidirectionalInferenceElideAssert) -LUAU_FASTFLAG(LuauOptimizeFalsyAndTruthyIntersect) LUAU_FASTFLAG(LuauTypeCheckerStricterIndexCheck) LUAU_FASTFLAG(LuauReportSubtypingErrors) -LUAU_FASTFLAG(LuauSimplifyOutOfLine) +LUAU_FASTFLAG(LuauSimplifyOutOfLine2) LUAU_FASTFLAG(LuauTableLiteralSubtypeSpecificCheck) LUAU_FASTFLAG(LuauEnableWriteOnlyProperties) LUAU_FASTFLAG(LuauDisablePrimitiveInferenceInLargeTables) LUAU_FASTINT(LuauPrimitiveInferenceInTableLimit) -LUAU_FASTFLAG(LuauSubtypeGenericsAndNegations) -LUAU_FASTFLAG(LuauNoMoreInjectiveTypeFunctions) +LUAU_FASTFLAG(LuauAutocompleteMissingFollows) TEST_SUITE_BEGIN("TableTests"); @@ -702,7 +698,7 @@ TEST_CASE_FIXTURE(Fixture, "indexers_get_quantified_too") LUAU_REQUIRE_NO_ERRORS(result); - if (FFlag::LuauSolverV2 && FFlag::LuauEagerGeneralization2) + if (FFlag::LuauSolverV2 && FFlag::LuauEagerGeneralization3) CHECK("({a}) -> ()" == toString(requireType("swap"))); else if (FFlag::LuauSolverV2) CHECK("({unknown}) -> ()" == toString(requireType("swap"))); @@ -762,7 +758,7 @@ TEST_CASE_FIXTURE(Fixture, "indexers_quantification_2") TEST_CASE_FIXTURE(Fixture, "infer_indexer_from_array_like_table") { - ScopedFastFlag _{FFlag::LuauSimplifyOutOfLine, true}; + ScopedFastFlag _{FFlag::LuauSimplifyOutOfLine2, true}; CheckResult result = check(R"( local t = {"one", "two", "three"} @@ -2062,7 +2058,7 @@ TEST_CASE_FIXTURE(Fixture, "explicit_nil_indexer") TEST_CASE_FIXTURE(Fixture, "ok_to_provide_a_subtype_during_construction") { - ScopedFastFlag _{FFlag::LuauSimplifyOutOfLine, true}; + ScopedFastFlag _{FFlag::LuauSimplifyOutOfLine2, true}; CheckResult result = check(R"( local a: string | number = 1 @@ -2379,7 +2375,7 @@ TEST_CASE_FIXTURE(Fixture, "invariant_table_properties_means_instantiating_table local c : string = t.m("hi") )"); - if (FFlag::LuauEagerGeneralization2 && FFlag::LuauSolverV2) + if (FFlag::LuauEagerGeneralization3 && FFlag::LuauSolverV2) { // FIXME CLI-151985 LUAU_CHECK_ERROR_COUNT(2, result); @@ -3176,9 +3172,10 @@ TEST_CASE_FIXTURE(BuiltinsFixture, "dont_quantify_table_that_belongs_to_outer_sc CHECK_EQ(follow(newRet->metatable), follow(requireType("Counter"))); } -// TODO: CLI-39624 TEST_CASE_FIXTURE(BuiltinsFixture, "instantiate_tables_at_scope_level") { + ScopedFastFlag sff1{FFlag::LuauSimplifyOutOfLine2, true}; + CheckResult result = check(R"( --!strict local Option = {} @@ -3749,9 +3746,7 @@ TEST_CASE_FIXTURE(Fixture, "scalar_is_not_a_subtype_of_a_compatible_polymorphic_ { ScopedFastFlag sff[] = { {FFlag::LuauReportSubtypingErrors, true}, - {FFlag::LuauEagerGeneralization2, true}, - {FFlag::LuauSubtypeGenericsAndNegations, true}, - {FFlag::LuauNoMoreInjectiveTypeFunctions, true} + {FFlag::LuauEagerGeneralization3, true}, }; CheckResult result = check(R"( @@ -4644,7 +4639,7 @@ TEST_CASE_FIXTURE(Fixture, "table_writes_introduce_write_properties") return; ScopedFastFlag sff[] = { - {FFlag::LuauEagerGeneralization2, true}, {FFlag::LuauSubtypeGenericsAndNegations, true}, {FFlag::LuauNoMoreInjectiveTypeFunctions, true} + {FFlag::LuauEagerGeneralization3, true}, }; CheckResult result = check(R"( @@ -4684,8 +4679,6 @@ TEST_CASE_FIXTURE(BuiltinsFixture, "tables_can_have_both_metatables_and_indexers TEST_CASE_FIXTURE(Fixture, "refined_thing_can_be_an_array") { - ScopedFastFlag _{FFlag::LuauOptimizeFalsyAndTruthyIntersect, true}; - CheckResult result = check(R"( function foo(x, y) if x then @@ -4696,7 +4689,7 @@ TEST_CASE_FIXTURE(Fixture, "refined_thing_can_be_an_array") end )"); - if (FFlag::LuauSolverV2 && !FFlag::LuauEagerGeneralization2) + if (FFlag::LuauSolverV2 && !FFlag::LuauEagerGeneralization3) { LUAU_CHECK_ERROR_COUNT(1, result); LUAU_CHECK_ERROR(result, NotATable); @@ -4744,7 +4737,7 @@ TEST_CASE_FIXTURE(Fixture, "parameter_was_set_an_indexer_and_bounded_by_another_ LUAU_REQUIRE_NO_ERRORS(result); // FIXME CLI-114134. We need to simplify types more consistently. - if (FFlag::LuauEagerGeneralization2) + if (FFlag::LuauEagerGeneralization3) CHECK("({number} & {number}, unknown) -> ()" == toString(requireType("f"))); else CHECK_EQ("(unknown & {number} & {number}, unknown) -> ()", toString(requireType("f"))); @@ -5038,7 +5031,7 @@ end TEST_CASE_FIXTURE(BuiltinsFixture, "indexing_branching_table") { - ScopedFastFlag _{FFlag::LuauSimplifyOutOfLine, true}; + ScopedFastFlag _{FFlag::LuauSimplifyOutOfLine2, true}; CheckResult result = check(R"( local test = if true then { "meow", "woof" } else { 4, 81 } @@ -5639,8 +5632,6 @@ TEST_CASE_FIXTURE(Fixture, "stop_refining_new_table_indices_for_non_primitive_ta TEST_CASE_FIXTURE(Fixture, "fuzz_match_literal_type_crash_again") { - ScopedFastFlag _{FFlag::LuauBidirectionalInferenceElideAssert, true}; - CheckResult result = check(R"( function f(_: { [string]: {unknown}} ) end f( diff --git a/tests/TypeInfer.test.cpp b/tests/TypeInfer.test.cpp index be9b60e0..576fed67 100644 --- a/tests/TypeInfer.test.cpp +++ b/tests/TypeInfer.test.cpp @@ -24,22 +24,16 @@ LUAU_FASTINT(LuauNormalizeCacheLimit) LUAU_FASTINT(LuauRecursionLimit) LUAU_FASTINT(LuauTypeInferTypePackLoopLimit) LUAU_FASTINT(LuauTypeInferRecursionLimit) -LUAU_FASTFLAG(LuauTypeCheckerAcceptNumberConcats) -LUAU_FASTFLAG(LuauPreprocessTypestatedArgument) LUAU_FASTFLAG(LuauMagicFreezeCheckBlocked2) -LUAU_FASTFLAG(LuauNoMoreInjectiveTypeFunctions) -LUAU_FASTFLAG(LuauEagerGeneralization2) -LUAU_FASTFLAG(LuauOptimizeFalsyAndTruthyIntersect) -LUAU_FASTFLAG(LuauHasPropProperBlock) -LUAU_FASTFLAG(LuauStringPartLengthLimit) -LUAU_FASTFLAG(LuauSimplificationRecheckAssumption) +LUAU_FASTFLAG(LuauEagerGeneralization3) LUAU_FASTFLAG(LuauReportSubtypingErrors) LUAU_FASTFLAG(LuauAvoidDoubleNegation) LUAU_FASTFLAG(LuauInsertErrorTypesIntoIndexerResult) -LUAU_FASTFLAG(LuauSimplifyOutOfLine) -LUAU_FASTFLAG(LuauSubtypeGenericsAndNegations) -LUAU_FASTFLAG(LuauNoMoreInjectiveTypeFunctions) +LUAU_FASTFLAG(LuauSimplifyOutOfLine2) LUAU_FASTFLAG(LuauDfgAllowUpdatesInLoops) +LUAU_FASTFLAG(LuauUpdateGetMetatableTypeSignature) +LUAU_FASTFLAG(LuauSkipLvalueForCompoundAssignment) +LUAU_FASTFLAG(LuauMissingFollowInAssignIndexConstraint) using namespace Luau; @@ -447,7 +441,7 @@ TEST_CASE_FIXTURE(Fixture, "check_expr_recursion_limit") #endif ScopedFastInt luauRecursionLimit{FInt::LuauRecursionLimit, limit + 100}; ScopedFastInt luauCheckRecursionLimit{FInt::LuauCheckRecursionLimit, limit - 100}; - ScopedFastFlag _{FFlag::LuauEagerGeneralization2, false}; + ScopedFastFlag _{FFlag::LuauEagerGeneralization3, false}; CheckResult result = check(R"(("foo"))" + rep(":lower()", limit)); @@ -1865,16 +1859,16 @@ TEST_CASE_FIXTURE(BuiltinsFixture, "getmetatable_infer_any_ret") TEST_CASE_FIXTURE(BuiltinsFixture, "getmetatable_infer_any_param") { - ScopedFastFlag _{FFlag::LuauSolverV2, true}; - auto result = check(R"( local function check(x): any return getmetatable(x) end )"); - // CLI-144695: We're leaking the `MT` generic here. - CHECK_EQ("({ @metatable MT, {+ +} }) -> any", toString(requireType("check"))); + if (FFlag::LuauSolverV2) + CHECK_EQ("(unknown) -> any", toString(requireType("check"))); + else + CHECK_EQ("({ @metatable any, {+ +} }) -> any", toString(requireType("check"))); } TEST_CASE_FIXTURE(BuiltinsFixture, "fuzzer_pack_check_missing_follow") @@ -1929,7 +1923,6 @@ TEST_CASE_FIXTURE(Fixture, "fuzzer_derived_unsound_loops") TEST_CASE_FIXTURE(Fixture, "concat_string_with_string_union") { ScopedFastFlag _{FFlag::LuauSolverV2, true}; - ScopedFastFlag fixNumberConcats{FFlag::LuauTypeCheckerAcceptNumberConcats, true}; LUAU_REQUIRE_NO_ERRORS(check(R"( local function concat_stuff(x: string, y : string | number) @@ -1940,10 +1933,7 @@ TEST_CASE_FIXTURE(Fixture, "concat_string_with_string_union") TEST_CASE_FIXTURE(BuiltinsFixture, "fuzz_local_before_declaration_ice") { - ScopedFastFlag sffs[] = { - {FFlag::LuauSolverV2, true}, - {FFlag::LuauPreprocessTypestatedArgument, true}, - }; + ScopedFastFlag _{FFlag::LuauSolverV2, true}; CheckResult result = check(R"( local _ @@ -2034,11 +2024,7 @@ TEST_CASE_FIXTURE(Fixture, "fuzz_generalize_one_remove_type_assert") { ScopedFastFlag sffs[] = { {FFlag::LuauSolverV2, true}, - {FFlag::LuauHasPropProperBlock, true}, - {FFlag::LuauEagerGeneralization2, true}, - {FFlag::LuauOptimizeFalsyAndTruthyIntersect, true}, - {FFlag::LuauSubtypeGenericsAndNegations, true}, - {FFlag::LuauNoMoreInjectiveTypeFunctions, true}, + {FFlag::LuauEagerGeneralization3, true}, }; auto result = check(R"( @@ -2073,10 +2059,7 @@ TEST_CASE_FIXTURE(Fixture, "fuzz_generalize_one_remove_type_assert_2") { ScopedFastFlag sffs[] = { {FFlag::LuauSolverV2, true}, - {FFlag::LuauEagerGeneralization2, true}, - {FFlag::LuauOptimizeFalsyAndTruthyIntersect, true}, - {FFlag::LuauSubtypeGenericsAndNegations, true}, - {FFlag::LuauNoMoreInjectiveTypeFunctions, true}, + {FFlag::LuauEagerGeneralization3, true}, }; CheckResult result = check(R"( @@ -2109,13 +2092,7 @@ TEST_CASE_FIXTURE(BuiltinsFixture, "fuzz_simplify_combinatorial_explosion") { ScopedFastFlag sffs[] = { {FFlag::LuauSolverV2, true}, - {FFlag::LuauHasPropProperBlock, true}, {FFlag::LuauEagerGeneralization2, true}, - {FFlag::LuauOptimizeFalsyAndTruthyIntersect, true}, - {FFlag::LuauStringPartLengthLimit, true}, - {FFlag::LuauSimplificationRecheckAssumption, true}, - {FFlag::LuauSubtypeGenericsAndNegations, true}, - {FFlag::LuauNoMoreInjectiveTypeFunctions, true}, }; LUAU_REQUIRE_ERRORS(check(R"( @@ -2154,7 +2131,6 @@ TEST_CASE_FIXTURE(BuiltinsFixture, "fuzzer_avoid_double_negation" * doctest::tim { ScopedFastFlag sffs[] = { {FFlag::LuauSolverV2, true}, - {FFlag::LuauOptimizeFalsyAndTruthyIntersect, true}, {FFlag::LuauAvoidDoubleNegation, true}, }; // We don't care about errors, only that we don't OOM during typechecking. @@ -2207,7 +2183,7 @@ TEST_CASE_FIXTURE(BuiltinsFixture, "fuzzer_has_indexer_can_create_cyclic_union") TEST_CASE_FIXTURE(Fixture, "fuzzer_simplify_table_indexer" * doctest::timeout(0.5)) { - ScopedFastFlag _{FFlag::LuauSimplifyOutOfLine, true}; + ScopedFastFlag _{FFlag::LuauSimplifyOutOfLine2, true}; LUAU_REQUIRE_ERRORS(check(R"( _[_] += true @@ -2290,4 +2266,152 @@ end )")); } +TEST_CASE_FIXTURE(Fixture, "self_bound_due_to_compound_assign") +{ + ScopedFastFlag _{FFlag::LuauSkipLvalueForCompoundAssignment, true}; + + loadDefinition(R"( + declare class Camera + CameraType: string + CFrame: number + end + )"); + + CheckResult result = check(R"( + --!strict + function MT_UPDATE(CAMERA: Camera, Enum: any, totalOffsets: number, focusToCFrame: number, magnitude: number) + if CAMERA.CameraType ~= Enum.CameraType.Custom then + return + end + + local goalCFrame = (CAMERA.CFrame) * totalOffsets + if goalCFrame ~= CAMERA.CFrame then + goalCFrame -= (focusToCFrame * magnitude) -- Offset the goalCFrame the raycast direction based on the cutoff distance. + end + end + + return {} + )"); + + LUAU_REQUIRE_NO_ERRORS(result); +} + +TEST_CASE_FIXTURE(BuiltinsFixture, "config_reader_example") +{ + // If this flag isn't set, then we do not do the requisite setup when the + // test suite starts, which will cause an assert if we try to eagerly + // generalize _after_ the test is set up. Additionally, this code block + // crashes under the new solver without flags. + if (!FFlag::LuauEagerGeneralization3) + return; + + fileResolver.source["game/ConfigReader"] = R"( + --!strict + local ConfigReader = {} + ConfigReader.Defaults = {} + + local Defaults = ConfigReader.Defaults + local Config = ConfigReader.Defaults + + function ConfigReader:read(config_name: string) + if Config[config_name] ~= nil then + return Config[config_name] + elseif Defaults[config_name] ~= nil then + return Defaults[config_name] + else + error(config_name .. " must be defined in Config") + end + end + + + function ConfigReader:getFullConfigWithDefaults() + local config = {} + for key, val in pairs(ConfigReader.Defaults) do + config[key] = val + end + for key, val in pairs(Config) do + config[key] = val + end + return config + end + + return ConfigReader + )"; + + fileResolver.source["game/Util"] = R"( + --!strict + local ConfigReader = require(script.Parent.ConfigReader) + local _ = ConfigReader:read("foobar")() + )"; + + LUAU_REQUIRE_ERRORS(frontend.check("game/Util")); +} + +TEST_CASE_FIXTURE(BuiltinsFixture, "is_safe_integer_example") +{ + if (!FFlag::LuauEagerGeneralization3) + return; + + fileResolver.source["game/isInteger"] = R"( + --!strict + return function(value) + return type(value) == "number" and value ~= math.huge and value == math.floor(value) + end + )"; + + fileResolver.source["game/MAX_SAFE_INTEGER"] = R"( + --!strict + return 42 + )"; + + fileResolver.source["game/Util"] = R"( + --!strict + local isInteger = require(script.Parent.isInteger) + local MAX_SAFE_INTEGER = require(script.Parent.MAX_SAFE_INTEGER) + return function(value) + return isInteger(value) and math.abs(value) <= MAX_SAFE_INTEGER + end + )"; + + LUAU_REQUIRE_NO_ERRORS(frontend.check("game/Util")); +} + +TEST_CASE_FIXTURE(BuiltinsFixture, "type_remover_heap_use_after_free") +{ + LUAU_REQUIRE_ERRORS(check(R"( + _ = if l0.n0.n0 then {n4(...,setmetatable(setmetatable(_),_)),_ == _,} elseif _.ceil._ then _ elseif _ then not _ + )")); + + LUAU_REQUIRE_ERRORS(check(R"( + do + _ = if _[_] then {[_(``)]="y",} elseif _ then _ elseif _[_] then "" elseif _ then _ elseif _[_] then {} elseif _[_] then false else "" + end + )")); + + LUAU_REQUIRE_ERRORS(check(R"( + local l249 = require(module0) + _,_ = {[`{_}`]=_,[_._G._]=(_)(),[_["" + _]._G]={_=_,_=_,[_._G[_]._]=_G,},},_,(_)() + )")); + +} + +TEST_CASE_FIXTURE(Fixture, "fuzzer_missing_follow_in_assign_index_constraint") +{ + ScopedFastFlag _{FFlag::LuauMissingFollowInAssignIndexConstraint, true}; + + LUAU_REQUIRE_ERRORS(check(R"( + _._G = nil + for _ in ... do + break + end + for _ in function(l0) + _,_._,l0 = l0,_,_._ + local _ = l0,{[_]=_,} + _[{nil=_,}](_) + end,{[_]=_,} do + end + _ -= _ + )")); +} + TEST_SUITE_END(); diff --git a/tests/TypeInfer.typePacks.test.cpp b/tests/TypeInfer.typePacks.test.cpp index 482e2b8d..1c4c8100 100644 --- a/tests/TypeInfer.typePacks.test.cpp +++ b/tests/TypeInfer.typePacks.test.cpp @@ -12,9 +12,8 @@ using namespace Luau; LUAU_FASTFLAG(LuauSolverV2) LUAU_FASTFLAG(LuauInstantiateInSubtyping) -LUAU_FASTFLAG(LuauEagerGeneralization2) +LUAU_FASTFLAG(LuauEagerGeneralization3) LUAU_FASTFLAG(LuauReportSubtypingErrors) -LUAU_FASTFLAG(LuauTrackInferredFunctionTypeFromCall) LUAU_FASTFLAG(LuauTableLiteralSubtypeSpecificCheck) LUAU_FASTFLAG(LuauFixEmptyTypePackStringification) @@ -100,7 +99,7 @@ TEST_CASE_FIXTURE(Fixture, "higher_order_function") LUAU_REQUIRE_NO_ERRORS(result); - if (FFlag::LuauEagerGeneralization2) + if (FFlag::LuauEagerGeneralization3) CHECK_EQ("((c...) -> (b...), (a) -> (c...), a) -> (b...)", toString(requireType("apply"))); else CHECK_EQ("((b...) -> (c...), (a) -> (b...), a) -> (c...)", toString(requireType("apply"))); @@ -1064,7 +1063,7 @@ TEST_CASE_FIXTURE(BuiltinsFixture, "detect_cyclic_typepacks") TEST_CASE_FIXTURE(BuiltinsFixture, "detect_cyclic_typepacks2") { - ScopedFastFlag sffs[] = {{FFlag::LuauReportSubtypingErrors, true}, {FFlag::LuauTrackInferredFunctionTypeFromCall, true}}; + ScopedFastFlag _{FFlag::LuauReportSubtypingErrors, true}; CheckResult result = check(R"( function _(l0:((typeof((pcall)))|((((t0)->())|(typeof(-67108864)))|(any)))|(any),...):(((typeof(0))|(any))|(any),typeof(-67108864),any) diff --git a/tests/TypeInfer.typestates.test.cpp b/tests/TypeInfer.typestates.test.cpp index 5f64ac00..00a6d919 100644 --- a/tests/TypeInfer.typestates.test.cpp +++ b/tests/TypeInfer.typestates.test.cpp @@ -4,16 +4,12 @@ #include "doctest.h" LUAU_FASTFLAG(LuauSolverV2) -LUAU_FASTFLAG(LuauRefineWaitForBlockedTypesInTarget) LUAU_FASTFLAG(LuauDoNotAddUpvalueTypesToLocalType) LUAU_FASTFLAG(LuauDfgIfBlocksShouldRespectControlFlow) LUAU_FASTFLAG(LuauReportSubtypingErrors) -LUAU_FASTFLAG(LuauEagerGeneralization2) -LUAU_FASTFLAG(LuauPreprocessTypestatedArgument) +LUAU_FASTFLAG(LuauEagerGeneralization3) LUAU_FASTFLAG(LuauTableLiteralSubtypeSpecificCheck) LUAU_FASTFLAG(LuauDfgAllowUpdatesInLoops) -LUAU_FASTFLAG(LuauSubtypeGenericsAndNegations) -LUAU_FASTFLAG(LuauNoMoreInjectiveTypeFunctions) using namespace Luau; @@ -418,9 +414,7 @@ TEST_CASE_FIXTURE(BuiltinsFixture, "prototyped_recursive_functions_but_has_futur ScopedFastFlag sffs[] = { {FFlag::LuauSolverV2, true}, {FFlag::LuauReportSubtypingErrors, true}, - {FFlag::LuauEagerGeneralization2, true}, - {FFlag::LuauSubtypeGenericsAndNegations, true}, - {FFlag::LuauNoMoreInjectiveTypeFunctions, true}, + {FFlag::LuauEagerGeneralization3, true}, }; CheckResult result = check(R"( @@ -553,7 +547,6 @@ TEST_CASE_FIXTURE(Fixture, "typestate_unknown_global") TEST_CASE_FIXTURE(BuiltinsFixture, "fuzzer_normalized_type_variables_are_bad" * doctest::timeout(0.5)) { - ScopedFastFlag _{FFlag::LuauRefineWaitForBlockedTypesInTarget, true}; // We do not care about the errors here, only that this finishes typing // in a sensible amount of time. LUAU_REQUIRE_ERRORS(check(R"( diff --git a/tests/TypeInfer.unionTypes.test.cpp b/tests/TypeInfer.unionTypes.test.cpp index d007513e..994b0a3d 100644 --- a/tests/TypeInfer.unionTypes.test.cpp +++ b/tests/TypeInfer.unionTypes.test.cpp @@ -10,7 +10,7 @@ using namespace Luau; LUAU_FASTFLAG(LuauSolverV2) -LUAU_FASTFLAG(LuauEagerGeneralization2) +LUAU_FASTFLAG(LuauEagerGeneralization3) LUAU_FASTFLAG(LuauTableLiteralSubtypeSpecificCheck) TEST_SUITE_BEGIN("UnionTypes"); @@ -895,7 +895,7 @@ TEST_CASE_FIXTURE(Fixture, "less_greedy_unification_with_union_types") LUAU_REQUIRE_NO_ERRORS(result); - if (FFlag::LuauEagerGeneralization2) + if (FFlag::LuauEagerGeneralization3) CHECK_EQ( "(({ read x: a } & { x: number }) | ({ read x: a } & { x: string })) -> { x: number } | { x: string }", toString(requireType("f")) ); diff --git a/tests/TypeInfer.unknownnever.test.cpp b/tests/TypeInfer.unknownnever.test.cpp index c26797a9..4f5feb01 100644 --- a/tests/TypeInfer.unknownnever.test.cpp +++ b/tests/TypeInfer.unknownnever.test.cpp @@ -7,7 +7,6 @@ using namespace Luau; LUAU_FASTFLAG(LuauSolverV2); -LUAU_FASTFLAG(LuauNoMoreInjectiveTypeFunctions); TEST_SUITE_BEGIN("TypeInferUnknownNever"); @@ -336,13 +335,11 @@ TEST_CASE_FIXTURE(Fixture, "dont_unify_operands_if_one_of_the_operand_is_never_i LUAU_REQUIRE_NO_ERRORS(result); - if (FFlag::LuauSolverV2 && FFlag::LuauNoMoreInjectiveTypeFunctions) + if (FFlag::LuauSolverV2) { // FIXME: CLI-152325 CHECK_EQ("(nil, nil & ~nil) -> boolean", toString(requireType("ord"))); } - else if (FFlag::LuauSolverV2) - CHECK_EQ("(nil, unknown) -> boolean", toString(requireType("ord"))); else CHECK_EQ("(nil, a) -> boolean", toString(requireType("ord"))); } diff --git a/tests/conformance/gc.luau b/tests/conformance/gc.luau index c49dbe77..60d58a3a 100644 --- a/tests/conformance/gc.luau +++ b/tests/conformance/gc.luau @@ -186,6 +186,49 @@ for i=1,lim do a[{}] = i end collectgarbage() assert(next(a) == nil) +-- shrinking a table should be robust against an OOM +do + local count = 100000 + local extra = 1000 + a = {} + setmetatable(a, {__mode = 'ks'}) + + -- items to be collected + for i = 1,count do a[{}] = i end + + -- items that will still be reachable + for i = 1,extra do a[tostring(i)] = {} end + + setblockallocations(true) + collectgarbage() + setblockallocations(false) + + for k,v in a do assert(type(k) == "string") end +end + +-- shrinking strings should be robust against an OOM +do + local count = 100000 + a = {} + + for i = 1,count do a[tostring(count)] = i end + table.clear(a); + + setblockallocations(true) + collectgarbage() + setblockallocations(false) +end + +-- shrinking thread stacks should be robust against an OOM +do + function recurse(n, ...) return n <= 1 and (1 + #{...}) or recurse(n-1, table.unpack(table.create(4000, 1))) + 1 end + recurse(100) + + setblockallocations(true) + collectgarbage() + setblockallocations(false) +end + -- testing userdata collectgarbage("stop") -- stop collection local u = newproxy(true)