diff --git a/Analysis/include/Luau/BuiltinTypeFunctions.h b/Analysis/include/Luau/BuiltinTypeFunctions.h index 8c36c00c5..02b7d8175 100644 --- a/Analysis/include/Luau/BuiltinTypeFunctions.h +++ b/Analysis/include/Luau/BuiltinTypeFunctions.h @@ -33,7 +33,6 @@ struct BuiltinTypeFunctions TypeFunction ltFunc; TypeFunction leFunc; - TypeFunction eqFunc; TypeFunction refineFunc; TypeFunction singletonFunc; diff --git a/Analysis/include/Luau/ConstraintGenerator.h b/Analysis/include/Luau/ConstraintGenerator.h index d110652ad..2368dd675 100644 --- a/Analysis/include/Luau/ConstraintGenerator.h +++ b/Analysis/include/Luau/ConstraintGenerator.h @@ -58,6 +58,11 @@ struct InferencePack } }; +struct Checkpoint +{ + size_t offset = 0; +}; + struct ConstraintGenerator { // A list of all the scopes in the module. This vector holds ownership of the @@ -290,6 +295,13 @@ struct ConstraintGenerator ); InferencePack checkPack(const ScopePtr& scope, AstExprCall* call); + InferencePack checkExprCall( + const ScopePtr& scope, + AstExprCall* call, + TypeId fnType, + Checkpoint funcBeginCheckpoint, + Checkpoint funcEndCheckpoint + ); /** * Checks an expression that is expected to evaluate to one type. diff --git a/Analysis/include/Luau/FileResolver.h b/Analysis/include/Luau/FileResolver.h index 2985490a8..0f8de3bc6 100644 --- a/Analysis/include/Luau/FileResolver.h +++ b/Analysis/include/Luau/FileResolver.h @@ -36,6 +36,11 @@ struct ModuleInfo struct RequireAlias { + explicit RequireAlias(std::string alias) + : alias(std::move(alias)) + { + } + std::string alias; // Unprefixed alias name (no leading `@`). std::vector tags = {}; }; diff --git a/Analysis/include/Luau/IterativeTypeVisitor.h b/Analysis/include/Luau/IterativeTypeVisitor.h new file mode 100644 index 000000000..dabe007a3 --- /dev/null +++ b/Analysis/include/Luau/IterativeTypeVisitor.h @@ -0,0 +1,116 @@ +// This file is part of the Luau programming language and is licensed under MIT License; see LICENSE.txt for details +#pragma once + +#include "Luau/Type.h" +#include "Luau/TypePack.h" +#include "Luau/Set.h" + +#include + +namespace Luau +{ + +struct IterativeTypeVisitor +{ + using SeenSet = Set; + + // We avoid Luau::Variant here because we can move the tag bit and make this struct 64 bits shorter. + struct WorkItem + { + WorkItem(TypeId ty, int32_t parent); + WorkItem(TypePackId tp, int32_t parent); + + const TypeId* asType() const; + const TypePackId* asTypePack() const; + + bool operator==(TypeId ty) const; + bool operator==(TypePackId tp) const; + + private: + // TypeId if isType, else TypePackId + const void* t; + bool isType; + + public: + // -1 indicates no parent + int32_t parent; + }; + + explicit IterativeTypeVisitor(std::string visitorName, bool skipBoundTypes); + explicit IterativeTypeVisitor(std::string visitorName, bool visitOnce, bool skipBoundTypes); + explicit IterativeTypeVisitor(std::string visitorName, SeenSet seen, bool visitOnce, bool skipBoundTypes); + + IterativeTypeVisitor(const IterativeTypeVisitor&) = delete; + IterativeTypeVisitor& operator=(const IterativeTypeVisitor&) = delete; + + virtual ~IterativeTypeVisitor() = default; + + virtual void cycle(TypeId); + virtual void cycle(TypePackId); + + virtual bool visit(TypeId ty); + virtual bool visit(TypeId ty, const BoundType& btv); + virtual bool visit(TypeId ty, const FreeType& ftv); + virtual bool visit(TypeId ty, const GenericType& gtv); + virtual bool visit(TypeId ty, const ErrorType& etv); + virtual bool visit(TypeId ty, const PrimitiveType& ptv); + virtual bool visit(TypeId ty, const FunctionType& ftv); + virtual bool visit(TypeId ty, const TableType& ttv); + virtual bool visit(TypeId ty, const MetatableType& mtv); + virtual bool visit(TypeId ty, const ExternType& etv); + virtual bool visit(TypeId ty, const AnyType& atv); + virtual bool visit(TypeId ty, const NoRefineType& nrt); + virtual bool visit(TypeId ty, const UnknownType& utv); + virtual bool visit(TypeId ty, const NeverType& ntv); + virtual bool visit(TypeId ty, const UnionType& utv); + virtual bool visit(TypeId ty, const IntersectionType& itv); + virtual bool visit(TypeId ty, const BlockedType& btv); + virtual bool visit(TypeId ty, const PendingExpansionType& petv); + virtual bool visit(TypeId ty, const SingletonType& stv); + virtual bool visit(TypeId ty, const NegationType& ntv); + virtual bool visit(TypeId ty, const TypeFunctionInstanceType& tfit); + + virtual bool visit(TypePackId tp); + virtual bool visit(TypePackId tp, const BoundTypePack& btp); + virtual bool visit(TypePackId tp, const FreeTypePack& ftp); + virtual bool visit(TypePackId tp, const GenericTypePack& gtp); + virtual bool visit(TypePackId tp, const ErrorTypePack& etp); + virtual bool visit(TypePackId tp, const TypePack& pack); + virtual bool visit(TypePackId tp, const VariadicTypePack& vtp); + virtual bool visit(TypePackId tp, const BlockedTypePack& btp); + virtual bool visit(TypePackId tp, const TypeFunctionInstanceTypePack& tfitp); + + void run(TypeId ty); + void run(TypePackId tp); + +protected: + /// Add this type (or pack) to the queue of things to traverse. Does not + /// immediately process the thing! You cannot use this to effect in-order + /// traversal. + void traverse(TypeId ty); + void traverse(TypePackId tp); + +private: + void process(TypeId ty); + void process(TypePackId tp); + + bool hasSeen(const void* tv); + void unsee(const void* tv); + + template + bool isCyclic(TID ty) const; + + void processWorkQueue(); + + SeenSet seen{nullptr}; + + std::vector workQueue; + int32_t parentCursor = -1; + uint32_t workCursor = 0; + + const std::string visitorName; + bool skipBoundTypes = false; + bool visitOnce = true; +}; + +} // namespace Luau diff --git a/Analysis/include/Luau/Normalize.h b/Analysis/include/Luau/Normalize.h index a28363ccd..391be4878 100644 --- a/Analysis/include/Luau/Normalize.h +++ b/Analysis/include/Luau/Normalize.h @@ -355,10 +355,9 @@ class Normalizer std::optional intersectionOfTypePacks(TypePackId here, TypePackId there); - // Move to private with LuauNormalizerStepwiseFuel +private: std::optional intersectionOfTypePacks_INTERNAL(TypePackId here, TypePackId there); -private: // ------- Cached TypeIds TypeId unionType(TypeId here, TypeId there); TypeId intersectionType(TypeId here, TypeId there); diff --git a/Analysis/include/Luau/OverloadResolution.h b/Analysis/include/Luau/OverloadResolution.h index 67f8f5e2d..eeb3856fd 100644 --- a/Analysis/include/Luau/OverloadResolution.h +++ b/Analysis/include/Luau/OverloadResolution.h @@ -42,7 +42,7 @@ using IncompatibilityReason = Variant; */ struct SelectedOverload { - /** + /** * An unambiguous overload, if one can be selected. This is _not_ necessarily * an overload that is valid for the argument pack provided. For example: * @@ -302,7 +302,8 @@ struct SolveResult // Helper utility, presently used for binary operator type functions. // // Given a function and a set of arguments, select a suitable overload. -SolveResult solveFunctionCall( +// Clip with FFlag::LuauBuiltinTypeFunctionsUseNewOverloadResolution +SolveResult solveFunctionCall_DEPRECATED( NotNull arena, NotNull builtinTypes, NotNull normalizer, diff --git a/Analysis/include/Luau/Set.h b/Analysis/include/Luau/Set.h index 613e5aa5e..e114e98d4 100644 --- a/Analysis/include/Luau/Set.h +++ b/Analysis/include/Luau/Set.h @@ -26,7 +26,7 @@ class Set class const_iterator; using iterator = const_iterator; - Set(const T& empty_key) + explicit Set(const T& empty_key) : mapping{empty_key} { } diff --git a/Analysis/include/Luau/Subtyping.h b/Analysis/include/Luau/Subtyping.h index 81c95076a..50dbb64f6 100644 --- a/Analysis/include/Luau/Subtyping.h +++ b/Analysis/include/Luau/Subtyping.h @@ -298,7 +298,6 @@ struct Subtyping const SingletonType* superSingleton, NotNull scope ); - SubtypingResult isCovariantWith(SubtypingEnvironment& env, const TableType* subTable, const TableType* superTable, NotNull scope); SubtypingResult isCovariantWith( SubtypingEnvironment& env, const TableType* subTable, diff --git a/Analysis/include/Luau/TypeChecker2.h b/Analysis/include/Luau/TypeChecker2.h index 425eb1139..eae215458 100644 --- a/Analysis/include/Luau/TypeChecker2.h +++ b/Analysis/include/Luau/TypeChecker2.h @@ -9,9 +9,10 @@ #include "Luau/Subtyping.h" #include "Luau/Type.h" #include "Luau/TypeFwd.h" -#include "Luau/TypeOrPack.h" #include "Luau/TypeUtils.h" +LUAU_FASTFLAG(LuauBetterTypeMismatchErrors) + namespace Luau { @@ -44,7 +45,7 @@ struct Reasonings // sort the reasons here to achieve a stable error // stringification. std::sort(reasons.begin(), reasons.end()); - std::string allReasons = "\nthis is because "; + std::string allReasons = (FFlag::LuauBetterTypeMismatchErrors && reasons.size() < 2) ? "\n" : "\nthis is because "; for (const std::string& reason : reasons) { if (reasons.size() > 1) diff --git a/Analysis/src/AstJsonEncoder.cpp b/Analysis/src/AstJsonEncoder.cpp index f38c36e10..2c7e112ef 100644 --- a/Analysis/src/AstJsonEncoder.cpp +++ b/Analysis/src/AstJsonEncoder.cpp @@ -8,8 +8,6 @@ #include -LUAU_FASTFLAG(LuauAutocompleteAttributes) - namespace Luau { @@ -1167,10 +1165,7 @@ struct AstJsonEncoder : public AstVisitor "AstAttr", [&]() { - if (FFlag::LuauAutocompleteAttributes) - write("name", node->name); - else - write("name", node->type); + write("name", node->name); } ); } diff --git a/Analysis/src/AutocompleteCore.cpp b/Analysis/src/AutocompleteCore.cpp index 143d3ab4e..6718919ae 100644 --- a/Analysis/src/AutocompleteCore.cpp +++ b/Analysis/src/AutocompleteCore.cpp @@ -28,7 +28,6 @@ LUAU_FASTINT(LuauTypeInferIterationLimit) LUAU_FASTINT(LuauTypeInferRecursionLimit) LUAU_FASTFLAGVARIABLE(DebugLuauMagicVariableNames) LUAU_FASTFLAGVARIABLE(LuauDoNotSuggestGenericsInAnonFuncs) -LUAU_FASTFLAG(LuauAutocompleteAttributes) LUAU_FASTFLAGVARIABLE(LuauAutocompleteSingletonsInIndexer) static constexpr std::array kStatementStartingKeywords = @@ -2216,17 +2215,14 @@ AutocompleteResult autocomplete_( } else if (AstExprFunction* func = node->as()) { - if (FFlag::LuauAutocompleteAttributes) + for (AstAttr* attr : func->attributes) { - for (AstAttr* attr : func->attributes) + if (attr->location.begin <= position && position <= attr->location.end && attr->type == AstAttr::Type::Unknown) { - if (attr->location.begin <= position && position <= attr->location.end && attr->type == AstAttr::Type::Unknown) - { - AutocompleteEntryMap ret; - for (const auto& attr : kKnownAttributes) - ret[attr.c_str()] = {AutocompleteEntryKind::Keyword}; - return {std::move(ret), std::move(ancestry), AutocompleteContext::Keyword}; - } + AutocompleteEntryMap ret; + for (const auto& attr : kKnownAttributes) + ret[attr.c_str()] = {AutocompleteEntryKind::Keyword}; + return {std::move(ret), std::move(ancestry), AutocompleteContext::Keyword}; } } } diff --git a/Analysis/src/BuiltinTypeFunctions.cpp b/Analysis/src/BuiltinTypeFunctions.cpp index 9922dc40d..8114ce510 100644 --- a/Analysis/src/BuiltinTypeFunctions.cpp +++ b/Analysis/src/BuiltinTypeFunctions.cpp @@ -12,6 +12,7 @@ #include "Luau/Type.h" #include "Luau/TypeArena.h" #include "Luau/TypeUtils.h" +#include "Luau/Instantiation2.h" #include "Luau/Unifier2.h" #include "Luau/UserDefinedTypeFunction.h" #include "Luau/VisitType.h" @@ -21,10 +22,12 @@ LUAU_DYNAMIC_FASTINTVARIABLE(LuauStepRefineRecursionLimit, 64) LUAU_FASTFLAGVARIABLE(LuauRefineNoRefineAlways2) LUAU_FASTFLAGVARIABLE(LuauRefineDistributesOverUnions) -LUAU_FASTFLAG(LuauEGFixGenericsList) LUAU_FASTFLAG(LuauNoMoreComparisonTypeFunctions) +LUAU_FASTFLAG(LuauInstantiationUsesGenericPolarity2) +LUAU_FASTFLAGVARIABLE(LuauBuiltinTypeFunctionsUseNewOverloadResolution) LUAU_FASTFLAGVARIABLE(LuauBuiltinTypeFunctionsArentGlobal) LUAU_FASTFLAGVARIABLE(LuauGetmetatableError) +LUAU_FASTFLAGVARIABLE(LuauSetmetatableWaitForPendingTypes) namespace Luau { @@ -318,13 +321,6 @@ TypeFunctionReductionResult unmTypeFunction( if (UnifyResult::Ok != u2.unify(inferredArgPack, instantiatedMmFtv->argTypes)) return {std::nullopt, Reduction::Erroneous, {}, {}}; // occurs check failed - if (!FFlag::LuauEGFixGenericsList) - { - Subtyping subtyping{ctx->builtins, ctx->arena, ctx->normalizer, ctx->typeFunctionRuntime, ctx->ice}; - if (!subtyping.isSubtype(inferredArgPack, instantiatedMmFtv->argTypes, ctx->scope, {}).isSubtype) - return {std::nullopt, Reduction::Erroneous, {}, {}}; - } - if (std::optional ret = first(instantiatedMmFtv->retTypes)) return {ret, Reduction::MaybeOk, {}, {}}; else @@ -357,6 +353,62 @@ NotNull TypeFunctionContext::pushConstraint(ConstraintV&& c) const return newConstraint; } +static std::optional solveFunctionCall(NotNull ctx, const Location& location, TypeId fnTy, TypePackId argsPack) +{ + auto resolver = std::make_unique( + ctx->builtins, ctx->arena, ctx->normalizer, ctx->typeFunctionRuntime, ctx->scope, ctx->ice, ctx->limits, location + ); + + DenseHashSet uniqueTypes{nullptr}; + OverloadResolution resolution = resolver->resolveOverload(fnTy, argsPack, location, NotNull{&uniqueTypes}, /* useFreeTypeBounds */ false); + if (resolution.ok.empty() && resolution.potentialOverloads.empty()) + return std::nullopt; + + SelectedOverload selected = resolution.getUnambiguousOverload(); + + if (!selected.overload.has_value()) + return std::nullopt; + + TypePackId retPack = ctx->arena->freshTypePack(ctx->scope); + TypeId prospectiveFunction = ctx->arena->addType(FunctionType{argsPack, retPack}); + + // FIXME: It's too bad that we have to bust out the Unifier here. We should + // be able to know the set of implied constraints and generic substitutions + // that are implied by that overload. + // + // Given that, we should be able to compute the return pack directly. + + Unifier2 unifier{ctx->arena, ctx->builtins, ctx->scope, ctx->ice}; + + const UnifyResult unifyResult = unifier.unify(*selected.overload, prospectiveFunction); + + switch (unifyResult) + { + case Luau::UnifyResult::Ok: + break; + case Luau::UnifyResult::OccursCheckFailed: + return std::nullopt; + case Luau::UnifyResult::TooComplex: + return std::nullopt; + } + + LUAU_ASSERT(FFlag::LuauInstantiationUsesGenericPolarity2); + + if (!unifier.genericSubstitutions.empty() || !unifier.genericPackSubstitutions.empty()) + { + Subtyping subtyping{ctx->builtins, ctx->arena, ctx->normalizer, ctx->typeFunctionRuntime, ctx->ice}; + std::optional subst = instantiate2( + ctx->arena, std::move(unifier.genericSubstitutions), std::move(unifier.genericPackSubstitutions), NotNull{&subtyping}, ctx->scope, retPack + ); + if (!subst) + return std::nullopt; + else + retPack = *subst; + } + + return retPack; +} + TypeFunctionReductionResult numericBinopTypeFunction( TypeId instance, const std::vector& typeParams, @@ -429,47 +481,69 @@ TypeFunctionReductionResult numericBinopTypeFunction( return {std::nullopt, Reduction::MaybeOk, {*mmType}, {}}; TypePackId argPack = ctx->arena->addTypePack({lhsTy, rhsTy}); - SolveResult solveResult; - if (!reversed) - solveResult = solveFunctionCall( - ctx->arena, - ctx->builtins, - ctx->normalizer, - ctx->typeFunctionRuntime, - ctx->ice, - ctx->limits, - ctx->scope, - location, - *mmType, - argPack - ); - else + if (FFlag::LuauBuiltinTypeFunctionsUseNewOverloadResolution) { - TypePack* p = getMutable(argPack); - std::swap(p->head.front(), p->head.back()); - solveResult = solveFunctionCall( - ctx->arena, - ctx->builtins, - ctx->normalizer, - ctx->typeFunctionRuntime, - ctx->ice, - ctx->limits, - ctx->scope, - location, - *mmType, - argPack - ); + if (reversed) + { + TypePack* p = getMutable(argPack); + std::swap(p->head.front(), p->head.back()); + } + + std::optional retPack = solveFunctionCall(ctx, location, *mmType, argPack); + if (!retPack.has_value()) + return {std::nullopt, Reduction::Erroneous, {}, {}}; + + TypePack extracted = extendTypePack(*ctx->arena, ctx->builtins, *retPack, 1); + if (extracted.head.empty()) + return {std::nullopt, Reduction::Erroneous, {}, {}}; + + return {extracted.head.front(), Reduction::MaybeOk, {}, {}}; } + else + { + SolveResult solveResult; + + if (!reversed) + solveResult = solveFunctionCall_DEPRECATED( + ctx->arena, + ctx->builtins, + ctx->normalizer, + ctx->typeFunctionRuntime, + ctx->ice, + ctx->limits, + ctx->scope, + location, + *mmType, + argPack + ); + else + { + TypePack* p = getMutable(argPack); + std::swap(p->head.front(), p->head.back()); + solveResult = solveFunctionCall_DEPRECATED( + ctx->arena, + ctx->builtins, + ctx->normalizer, + ctx->typeFunctionRuntime, + ctx->ice, + ctx->limits, + ctx->scope, + location, + *mmType, + argPack + ); + } - if (!solveResult.typePackId.has_value()) - return {std::nullopt, Reduction::Erroneous, {}, {}}; + if (!solveResult.typePackId.has_value()) + return {std::nullopt, Reduction::Erroneous, {}, {}}; - TypePack extracted = extendTypePack(*ctx->arena, ctx->builtins, *solveResult.typePackId, 1); - if (extracted.head.empty()) - return {std::nullopt, Reduction::Erroneous, {}, {}}; + TypePack extracted = extendTypePack(*ctx->arena, ctx->builtins, *solveResult.typePackId, 1); + if (extracted.head.empty()) + return {std::nullopt, Reduction::Erroneous, {}, {}}; - return {extracted.head.front(), Reduction::MaybeOk, {}, {}}; + return {extracted.head.front(), Reduction::MaybeOk, {}, {}}; + } } TypeFunctionReductionResult addTypeFunction( @@ -2093,28 +2167,46 @@ bool tblIndexInto( if (get(indexee)) { TypePackId argPack = ctx->arena->addTypePack({indexer}); - SolveResult solveResult = solveFunctionCall( - ctx->arena, - ctx->builtins, - ctx->normalizer, - ctx->typeFunctionRuntime, - ctx->ice, - ctx->limits, - ctx->scope, - ctx->scope->location, - indexee, - argPack - ); - if (!solveResult.typePackId.has_value()) - return false; + if (FFlag::LuauBuiltinTypeFunctionsUseNewOverloadResolution) + { + std::optional retPack = solveFunctionCall(ctx, ctx->scope->location, indexee, argPack); - TypePack extracted = extendTypePack(*ctx->arena, ctx->builtins, *solveResult.typePackId, 1); - if (extracted.head.empty()) - return false; + if (!retPack.has_value()) + return false; - result.insert(follow(extracted.head.front())); - return true; + TypePack extracted = extendTypePack(*ctx->arena, ctx->builtins, *retPack, 1); + if (extracted.head.empty()) + return false; + + result.insert(follow(extracted.head.front())); + return true; + } + else + { + SolveResult solveResult = solveFunctionCall_DEPRECATED( + ctx->arena, + ctx->builtins, + ctx->normalizer, + ctx->typeFunctionRuntime, + ctx->ice, + ctx->limits, + ctx->scope, + ctx->scope->location, + indexee, + argPack + ); + + if (!solveResult.typePackId.has_value()) + return false; + + TypePack extracted = extendTypePack(*ctx->arena, ctx->builtins, *solveResult.typePackId, 1); + if (extracted.head.empty()) + return false; + + result.insert(follow(extracted.head.front())); + return true; + } } // we have a table type to try indexing @@ -2342,6 +2434,12 @@ TypeFunctionReductionResult setmetatableTypeFunction( TypeId targetTy = follow(typeParams.at(0)); TypeId metatableTy = follow(typeParams.at(1)); + if (FFlag::LuauSetmetatableWaitForPendingTypes) + { + if (isPending(targetTy, ctx->solver)) + return {std::nullopt, Reduction::MaybeOk, {targetTy}, {}}; + } + std::shared_ptr targetNorm = ctx->normalizer->normalize(targetTy); // if the operand failed to normalize, we can't reduce, but know nothing about inhabitance. @@ -2358,6 +2456,12 @@ TypeFunctionReductionResult setmetatableTypeFunction( targetNorm->hasExternTypes()) return {std::nullopt, Reduction::Erroneous, {}, {}}; + if (FFlag::LuauSetmetatableWaitForPendingTypes) + { + if (isPending(metatableTy, ctx->solver)) + return {std::nullopt, Reduction::MaybeOk, {metatableTy}, {}}; + } + // if the supposed metatable is not a table, we will fail to reduce. if (!get(metatableTy) && !get(metatableTy)) return {std::nullopt, Reduction::Erroneous, {}, {}}; @@ -2619,7 +2723,6 @@ BuiltinTypeFunctions::BuiltinTypeFunctions() , orFunc{"or", orTypeFunction, /*canReduceGenerics*/ true} , ltFunc{"lt", ltTypeFunction} , leFunc{"le", leTypeFunction} - , eqFunc{"eq", eqTypeFunction} , refineFunc{"refine", refineTypeFunction, /*canReduceGenerics*/ true} , singletonFunc{"singleton", singletonTypeFunction} , unionFunc{"union", unionTypeFunction} @@ -2681,9 +2784,6 @@ void BuiltinTypeFunctions::addToScope(NotNull arena, NotNull s scope->exportedTypeBindings[ltFunc.name] = mkBinaryTypeFunctionWithDefault(<Func); scope->exportedTypeBindings[leFunc.name] = mkBinaryTypeFunctionWithDefault(&leFunc); - if (!FFlag::LuauNoMoreComparisonTypeFunctions) - scope->exportedTypeBindings[eqFunc.name] = mkBinaryTypeFunctionWithDefault(&eqFunc); - scope->exportedTypeBindings[keyofFunc.name] = mkUnaryTypeFunction(&keyofFunc); scope->exportedTypeBindings[rawkeyofFunc.name] = mkUnaryTypeFunction(&rawkeyofFunc); diff --git a/Analysis/src/ConstraintGenerator.cpp b/Analysis/src/ConstraintGenerator.cpp index 55f519465..74ae18057 100644 --- a/Analysis/src/ConstraintGenerator.cpp +++ b/Analysis/src/ConstraintGenerator.cpp @@ -11,6 +11,7 @@ #include "Luau/Def.h" #include "Luau/DenseHash.h" #include "Luau/InferPolarity.h" +#include "Luau/IterativeTypeVisitor.h" #include "Luau/ModuleResolver.h" #include "Luau/Normalize.h" #include "Luau/NotNull.h" @@ -40,17 +41,18 @@ LUAU_FASTFLAG(DebugLuauMagicTypes) LUAU_FASTINTVARIABLE(LuauPrimitiveInferenceInTableLimit, 500) LUAU_FASTFLAG(LuauExplicitTypeExpressionInstantiation) LUAU_FASTFLAG(DebugLuauStringSingletonBasedOnQuotes) -LUAU_FASTFLAGVARIABLE(LuauPushTypeConstraint2) -LUAU_FASTFLAGVARIABLE(LuauEGFixGenericsList) LUAU_FASTFLAGVARIABLE(LuauNumericUnaryOpsDontProduceNegationRefinements) LUAU_FASTFLAGVARIABLE(LuauInitializeDefaultGenericParamsAtProgramPoint) LUAU_FASTFLAGVARIABLE(LuauCacheDuplicateHasPropConstraints) -LUAU_FASTFLAGVARIABLE(LuauNoMoreComparisonTypeFunctions) +LUAU_FASTFLAGVARIABLE(LuauTypeFunctions) LUAU_FASTFLAG(LuauBuiltinTypeFunctionsArentGlobal) LUAU_FASTFLAGVARIABLE(LuauMetatableAvoidSingletonUnion) LUAU_FASTFLAGVARIABLE(LuauAddRefinementToAssertions) LUAU_FASTFLAG(LuauPushTypeConstraintLambdas2) LUAU_FASTFLAGVARIABLE(LuauIncludeExplicitGenericPacks) +LUAU_FASTFLAGVARIABLE(LuauAvoidMintingMultipleBlockedTypesForGlobals) +LUAU_FASTFLAGVARIABLE(LuauUseIterativeTypeVisitor) +LUAU_FASTFLAGVARIABLE(LuauPropagateTypeAnnotationsInForInLoops) namespace Luau { @@ -118,11 +120,6 @@ static std::optional matchTypeGuard(const AstExprBinary::Op op, AstEx namespace { -struct Checkpoint -{ - size_t offset; -}; - Checkpoint checkpoint(const ConstraintGenerator* cg) { return Checkpoint{cg->constraints.size()}; @@ -601,12 +598,13 @@ namespace * FindSimplificationBlockers to recognize these typeArguments and defer the * simplification until constraint solution. */ -struct FindSimplificationBlockers : TypeOnceVisitor +template +struct FindSimplificationBlockers : BaseVisitor { bool found = false; FindSimplificationBlockers() - : TypeOnceVisitor("FindSimplificationBlockers", /* skipBoundTypes */ true) + : BaseVisitor("FindSimplificationBlockers", /* skipBoundTypes */ true) { } @@ -648,9 +646,18 @@ struct FindSimplificationBlockers : TypeOnceVisitor bool mustDeferIntersection(TypeId ty) { - FindSimplificationBlockers bts; - bts.traverse(ty); - return bts.found; + if (FFlag::LuauUseIterativeTypeVisitor) + { + FindSimplificationBlockers bts; + bts.run(ty); + return bts.found; + } + else + { + FindSimplificationBlockers bts; + bts.traverse(ty); + return bts.found; + } } } // namespace @@ -1336,17 +1343,37 @@ ControlFlow ConstraintGenerator::visit(const ScopePtr& scope, AstStatForIn* forI TypeId loopVar = arena->addType(BlockedType{}); variableTypes.push_back(loopVar); - if (var->annotation) + if (FFlag::LuauPropagateTypeAnnotationsInForInLoops) { - TypeId annotationTy = resolveType(loopScope, var->annotation, /*inTypeArguments*/ false); - loopScope->bindings[var] = Binding{annotationTy, var->location}; - addConstraint(scope, var->location, SubtypeConstraint{loopVar, annotationTy}); + DefId def = dfg->getDef(var); + + if (var->annotation) + { + TypeId annotationTy = resolveType(loopScope, var->annotation, /*inTypeArguments*/ false); + loopScope->bindings[var] = Binding{annotationTy, var->location}; + addConstraint(scope, var->location, SubtypeConstraint{loopVar, annotationTy}); + loopScope->lvalueTypes[def] = annotationTy; + } + else + { + loopScope->bindings[var] = Binding{loopVar, var->location}; + loopScope->lvalueTypes[def] = loopVar; + } } else - loopScope->bindings[var] = Binding{loopVar, var->location}; + { + if (var->annotation) + { + TypeId annotationTy = resolveType(loopScope, var->annotation, /*inTypeArguments*/ false); + loopScope->bindings[var] = Binding{annotationTy, var->location}; + addConstraint(scope, var->location, SubtypeConstraint{loopVar, annotationTy}); + } + else + loopScope->bindings[var] = Binding{loopVar, var->location}; - DefId def = dfg->getDef(var); - loopScope->lvalueTypes[def] = loopVar; + DefId def = dfg->getDef(var); + loopScope->lvalueTypes[def] = loopVar; + } } auto iterable = addConstraint( @@ -2290,6 +2317,27 @@ InferencePack ConstraintGenerator::checkPack( } InferencePack ConstraintGenerator::checkPack(const ScopePtr& scope, AstExprCall* call) +{ + Checkpoint funcBeginCheckpoint = checkpoint(this); + + TypeId fnType = nullptr; + { + InConditionalContext icc2{&typeContext, TypeContext::Default}; + fnType = check(scope, call->func).ty; + } + + Checkpoint funcEndCheckpoint = checkpoint(this); + + return checkExprCall(scope, call, fnType, funcBeginCheckpoint, funcEndCheckpoint); +} + +InferencePack ConstraintGenerator::checkExprCall( + const ScopePtr& scope, + AstExprCall* call, + TypeId fnType, + Checkpoint funcBeginCheckpoint, + Checkpoint funcEndCheckpoint +) { std::vector exprArgs; @@ -2328,16 +2376,6 @@ InferencePack ConstraintGenerator::checkPack(const ScopePtr& scope, AstExprCall* discriminantTypes.emplace_back(std::nullopt); } - Checkpoint funcBeginCheckpoint = checkpoint(this); - - TypeId fnType = nullptr; - { - InConditionalContext icc2{&typeContext, TypeContext::Default}; - fnType = check(scope, call->func).ty; - } - - Checkpoint funcEndCheckpoint = checkpoint(this); - std::vector> expectedTypesForCall = getExpectedCallTypesForFunctionOverloads(fnType); module->astOriginalCallTypes[call->func] = fnType; @@ -3143,35 +3181,7 @@ Inference ConstraintGenerator::checkAstExprBinary( } case AstExprBinary::Op::CompareEq: case AstExprBinary::Op::CompareNe: - { - if (FFlag::LuauNoMoreComparisonTypeFunctions) - return Inference{builtinTypes->booleanType, std::move(refinement)}; - else - { - DefId leftDef = dfg->getDef(left); - DefId rightDef = dfg->getDef(right); - bool leftSubscripted = containsSubscriptedDefinition(leftDef); - bool rightSubscripted = containsSubscriptedDefinition(rightDef); - - if (leftSubscripted && rightSubscripted) - { - // we cannot add nil in this case because then we will blindly accept comparisons that we should not. - } - else if (leftSubscripted) - leftType = makeUnion(scope, location, leftType, builtinTypes->nilType); - else if (rightSubscripted) - rightType = makeUnion(scope, location, rightType, builtinTypes->nilType); - - TypeId resultType = createTypeFunctionInstance( - FFlag::LuauBuiltinTypeFunctionsArentGlobal ? builtinTypes->typeFunctions->eqFunc : builtinTypeFunctions_DEPRECATED().eqFunc, - {leftType, rightType}, - {}, - scope, - location - ); - return Inference{resultType, std::move(refinement)}; - } - } + return Inference{builtinTypes->booleanType, std::move(refinement)}; case AstExprBinary::Op::Op__Count: ice->ice("Op__Count should never be generated in an AST."); default: // msvc can't prove that this is exhaustive. @@ -3551,7 +3561,7 @@ Inference ConstraintGenerator::check(const ScopePtr& scope, AstExprTable* expr, TypeIds valuesLowerBound; std::optional start{std::nullopt}; - if (FFlag::LuauPushTypeConstraint2 && FFlag::LuauPushTypeConstraintLambdas2) + if (FFlag::LuauPushTypeConstraintLambdas2) start = checkpoint(this); for (const AstExprTable::Item& item : expr->items) @@ -3573,7 +3583,7 @@ Inference ConstraintGenerator::check(const ScopePtr& scope, AstExprTable* expr, item.value, /* expectedType */ std::nullopt, /* forceSingleton */ false, - /* generalize */ !(FFlag::LuauPushTypeConstraint2 && FFlag::LuauPushTypeConstraintLambdas2) + /* generalize */ !FFlag::LuauPushTypeConstraintLambdas2 ).ty; if (item.key) @@ -3636,7 +3646,7 @@ Inference ConstraintGenerator::check(const ScopePtr& scope, AstExprTable* expr, ttv->indexer = TableIndexer{indexKey, indexValue}; } - if (FFlag::LuauPushTypeConstraint2 && expectedType) + if (expectedType) { auto ptc = addConstraint( scope, @@ -3816,50 +3826,34 @@ ConstraintGenerator::FunctionSignature ConstraintGenerator::checkFunctionSignatu LUAU_ASSERT(nullptr != varargPack); - if (FFlag::LuauEGFixGenericsList) - { - // Some of the unannotated parameters in argTypes will eventually be - // generics, and some will not. The ones that are not generic will be - // pruned when GeneralizationConstraint dispatches. + // Some of the unannotated parameters in argTypes will eventually be + // generics, and some will not. The ones that are not generic will be + // pruned when GeneralizationConstraint dispatches. - // The self parameter never has an annotation and so could always become generic. - if (fn->self) - genericTypes.push_back(argTypes[0]); + // The self parameter never has an annotation and so could always become generic. + if (fn->self) + genericTypes.push_back(argTypes[0]); - size_t typeIndex = fn->self ? 1 : 0; - for (auto astArg : fn->args) - { - TypeId argTy = argTypes.at(typeIndex); - if (!astArg->annotation) - genericTypes.push_back(argTy); + size_t typeIndex = fn->self ? 1 : 0; + for (auto astArg : fn->args) + { + TypeId argTy = argTypes.at(typeIndex); + if (!astArg->annotation) + genericTypes.push_back(argTy); - ++typeIndex; - } + ++typeIndex; + } - varargPack = follow(varargPack); - returnType = follow(returnType); - if (FFlag::LuauIncludeExplicitGenericPacks) - { - genericTypePacks.push_back(varargPack); - if (varargPack != returnType) - genericTypePacks.push_back(returnType); - } - else - { - if (varargPack == returnType) - genericTypePacks = {varargPack}; - else - genericTypePacks = {varargPack, returnType}; - } + varargPack = follow(varargPack); + returnType = follow(returnType); + if (FFlag::LuauIncludeExplicitGenericPacks) + { + genericTypePacks.push_back(varargPack); + if (varargPack != returnType) + genericTypePacks.push_back(returnType); } else { - // Some of the typeArguments in argTypes will eventually be generics, and some - // will not. The ones that are not generic will be pruned when - // GeneralizationConstraint dispatches. - genericTypes.insert(genericTypes.begin(), argTypes.begin(), argTypes.end()); - varargPack = follow(varargPack); - returnType = follow(returnType); if (varargPack == returnType) genericTypePacks = {varargPack}; else @@ -4511,8 +4505,11 @@ struct GlobalPrepopulator : AstVisitor if (!globalScope->lookup(g->name)) globalScope->globalsToWarn.insert(g->name.value); - TypeId bt = arena->addType(BlockedType{}); - globalScope->bindings[g->name] = Binding{bt, g->location}; + if (!FFlag::LuauAvoidMintingMultipleBlockedTypesForGlobals || globalScope->bindings.find(g->name) == globalScope->bindings.end()) + { + TypeId bt = arena->addType(BlockedType{}); + globalScope->bindings[g->name] = Binding{bt, g->location}; + } } } diff --git a/Analysis/src/ConstraintSolver.cpp b/Analysis/src/ConstraintSolver.cpp index b909e405e..e4a4b13b1 100644 --- a/Analysis/src/ConstraintSolver.cpp +++ b/Analysis/src/ConstraintSolver.cpp @@ -4,12 +4,15 @@ #include "Luau/Anyification.h" #include "Luau/ApplyTypeFunction.h" #include "Luau/AstUtils.h" +#include "Luau/Clone.h" #include "Luau/Common.h" #include "Luau/DcrLogger.h" +#include "Luau/DenseHash.h" #include "Luau/Generalization.h" #include "Luau/HashUtil.h" #include "Luau/Instantiation.h" #include "Luau/Instantiation2.h" +#include "Luau/IterativeTypeVisitor.h" #include "Luau/Location.h" #include "Luau/ModuleResolver.h" #include "Luau/OverloadResolution.h" @@ -37,22 +40,19 @@ LUAU_FASTFLAGVARIABLE(DebugLuauAssertOnForcedConstraint) LUAU_FASTFLAGVARIABLE(DebugLuauLogSolver) LUAU_FASTFLAGVARIABLE(DebugLuauLogSolverIncludeDependencies) LUAU_FASTFLAGVARIABLE(DebugLuauLogBindings) -LUAU_FASTFLAG(LuauTrackUniqueness) -LUAU_FASTFLAG(LuauLimitUnification) LUAU_FASTFLAGVARIABLE(LuauCollapseShouldNotCrash) LUAU_FASTFLAGVARIABLE(LuauDontDynamicallyCreateRedundantSubtypeConstraints) -LUAU_FASTFLAGVARIABLE(LuauExtendSealedTableUpperBounds) LUAU_FASTFLAG(DebugLuauStringSingletonBasedOnQuotes) LUAU_FASTFLAG(LuauExplicitTypeExpressionInstantiation) -LUAU_FASTFLAG(LuauPushTypeConstraint2) -LUAU_FASTFLAGVARIABLE(LuauAvoidOverloadSelectionForFunctionType) LUAU_FASTFLAG(LuauSimplifyIntersectionNoTreeSet) -LUAU_FASTFLAG(LuauInstantiationUsesGenericPolarity) +LUAU_FASTFLAG(LuauInstantiationUsesGenericPolarity2) LUAU_FASTFLAG(LuauPushTypeConstraintLambdas2) LUAU_FASTFLAGVARIABLE(LuauPushTypeConstriantAlwaysCompletes) LUAU_FASTFLAG(LuauNewOverloadResolver2) LUAU_FASTFLAG(LuauMarkUnscopedGenericsAsSolved) LUAU_FASTFLAGVARIABLE(LuauUseFastSubtypeForIndexerWithName) +LUAU_FASTFLAG(LuauUseIterativeTypeVisitor) +LUAU_FASTFLAGVARIABLE(LuauDoNotUseApplyTypeFunctionToClone) namespace Luau { @@ -334,7 +334,8 @@ struct InstantiationQueuer : TypeOnceVisitor } }; -struct InfiniteTypeFinder : TypeOnceVisitor +template +struct InfiniteTypeFinder : BaseVisitor { NotNull solver; const InstantiationSignature& signature; @@ -342,7 +343,7 @@ struct InfiniteTypeFinder : TypeOnceVisitor bool foundInfiniteType = false; explicit InfiniteTypeFinder(ConstraintSolver* solver, const InstantiationSignature& signature, NotNull scope) - : TypeOnceVisitor("InfiniteTypeFinder", /* skipBoundTypes */ true) + : BaseVisitor("InfiniteTypeFinder", /* skipBoundTypes */ true) , solver(solver) , signature(signature) , scope(scope) @@ -1142,15 +1143,31 @@ bool ConstraintSolver::tryDispatch(const NameConstraint& c, NotNullscope}; - itf.traverse(target); + if (FFlag::LuauUseIterativeTypeVisitor) + { + InfiniteTypeFinder itf{this, signature, constraint->scope}; + itf.run(target); - if (itf.foundInfiniteType) + if (itf.foundInfiniteType) + { + constraint->scope->invalidTypeAliasNames.insert(c.name); + shiftReferences(target, builtinTypes->errorType); + emplaceType(asMutable(target), builtinTypes->errorType); + return true; + } + } + else { - constraint->scope->invalidTypeAliasNames.insert(c.name); - shiftReferences(target, builtinTypes->errorType); - emplaceType(asMutable(target), builtinTypes->errorType); - return true; + InfiniteTypeFinder itf{this, signature, constraint->scope}; + itf.traverse(target); + + if (itf.foundInfiniteType) + { + constraint->scope->invalidTypeAliasNames.insert(c.name); + shiftReferences(target, builtinTypes->errorType); + emplaceType(asMutable(target), builtinTypes->errorType); + return true; + } } } @@ -1288,17 +1305,37 @@ bool ConstraintSolver::tryDispatch(const TypeAliasExpansionConstraint& c, NotNul // https://github.com/luau-lang/luau/pull/68 for the RFC responsible for // this. This is a little nicer than using a recursion limit because we can // catch the infinite expansion before actually trying to expand it. - InfiniteTypeFinder itf{this, signature, constraint->scope}; - itf.traverse(tf->type); + if (FFlag::LuauUseIterativeTypeVisitor) + { + InfiniteTypeFinder itf{this, signature, constraint->scope}; + itf.run(tf->type); - if (itf.foundInfiniteType) + if (itf.foundInfiniteType) + { + // TODO (CLI-56761): Report an error. + bindResult(builtinTypes->errorType); + reportError(GenericError{"Recursive type being used with different parameters"}, constraint->location); + return true; + } + } + else { - // TODO (CLI-56761): Report an error. - bindResult(builtinTypes->errorType); - reportError(GenericError{"Recursive type being used with different parameters"}, constraint->location); - return true; + InfiniteTypeFinder itf{this, signature, constraint->scope}; + itf.traverse(tf->type); + + if (itf.foundInfiniteType) + { + // TODO (CLI-56761): Report an error. + bindResult(builtinTypes->errorType); + reportError(GenericError{"Recursive type being used with different parameters"}, constraint->location); + return true; + } } + // FIXME: this does not actually implement instantiation properly, it puts + // the values into the _binders_ as well, which is a mess. + // e.g. `(T...) -> T...` instantiated with `any` for `T...` becomes + // `(any) -> any`, where none of these things are _generics_. ApplyTypeFunction applyTypeFunction{arena}; for (size_t i = 0; i < typeArguments.size(); ++i) { @@ -1358,25 +1395,47 @@ bool ConstraintSolver::tryDispatch(const TypeAliasExpansionConstraint& c, NotNul { if (needsClone) { - // Substitution::clone is a shallow clone. If this is a - // metatable type, we want to mutate its table, so we need to - // explicitly clone that table as well. If we don't, we will - // mutate another module's type surface and cause a - // use-after-free. - if (get(target)) + if (FFlag::LuauDoNotUseApplyTypeFunctionToClone) { - instantiated = applyTypeFunction.clone(target); - MetatableType* mtv = getMutable(instantiated); - mtv->table = applyTypeFunction.clone(mtv->table); - ttv = getMutable(mtv->table); + if (get(target)) + { + CloneState cloneState{builtinTypes}; + instantiated = shallowClone(target, *arena.get(), cloneState, true); + MetatableType* mtv = getMutable(instantiated); + mtv->table = shallowClone(mtv->table, *arena.get(), cloneState, true); + ttv = getMutable(mtv->table); + } + else if (get(target)) + { + CloneState cloneState{builtinTypes}; + instantiated = shallowClone(target, *arena.get(), cloneState, true); + ttv = getMutable(instantiated); + } + + target = follow(instantiated); } - else if (get(target)) + else { - instantiated = applyTypeFunction.clone(target); - ttv = getMutable(instantiated); - } + // Substitution::clone is a shallow clone. If this is a + // metatable type, we want to mutate its table, so we need to + // explicitly clone that table as well. If we don't, we will + // mutate another module's type surface and cause a + // use-after-free. + if (get(target)) + { + instantiated = applyTypeFunction.clone(target); + MetatableType* mtv = getMutable(instantiated); + mtv->table = applyTypeFunction.clone(mtv->table); + ttv = getMutable(mtv->table); + } + else if (get(target)) + { + instantiated = applyTypeFunction.clone(target); + ttv = getMutable(instantiated); + } - target = follow(instantiated); + target = follow(instantiated); + } } // This is a new type - redefine the location. @@ -1549,13 +1608,14 @@ bool ConstraintSolver::tryDispatch(const FunctionCallConstraint& c, NotNull uniqueTypes{nullptr}; - if (FFlag::LuauTrackUniqueness && c.callSite) + if (c.callSite) findUniqueTypes(NotNull{&uniqueTypes}, c.callSite->args, NotNull{&module->astTypes}); TypeId overloadToUse = fn; - // NOTE: This probably ends up capturing union types as well, but - // that should be fairly uncommon. - if (!FFlag::LuauAvoidOverloadSelectionForFunctionType || !is(fn)) + + // If the function we have is already a function type, then there's no + // point in trying to do overload selection: we're just wasting CPU. + if (!is(fn)) { if (FFlag::LuauNewOverloadResolver2) { @@ -1614,7 +1674,7 @@ bool ConstraintSolver::tryDispatch(const FunctionCallConstraint& c, NotNull subst = instantiate2( @@ -1656,22 +1716,17 @@ bool ConstraintSolver::tryDispatch(const FunctionCallConstraint& c, NotNulllocation); - break; - case UnifyResult::OccursCheckFailed: - reportError(OccursCheckFailed{}, constraint->location); - break; - } - } - else + case UnifyResult::Ok: + break; + case UnifyResult::TooComplex: + reportError(UnificationTooComplex{}, constraint->location); + break; + case UnifyResult::OccursCheckFailed: reportError(OccursCheckFailed{}, constraint->location); + break; + } } InstantiationQueuer queuer{constraint->scope, constraint->location, this}; @@ -1689,12 +1744,13 @@ bool ConstraintSolver::tryDispatch(const FunctionCallConstraint& c, NotNull +struct ContainsGenerics : public BaseVisitor { NotNull> generics; explicit ContainsGenerics(NotNull> generics) - : TypeOnceVisitor("ContainsGenerics", /* skipBoundTypes */ true) + : BaseVisitor("ContainsGenerics", /* skipBoundTypes */ true) , generics{generics} { } @@ -1722,14 +1778,23 @@ struct ContainsGenerics : public TypeOnceVisitor found |= generics->contains(tp); return !found; } +}; - static bool hasGeneric(TypeId ty, NotNull> generics) +bool containsGeneric(TypeId ty, NotNull> generics) +{ + if (FFlag::LuauUseIterativeTypeVisitor) + { + ContainsGenerics cg{generics}; + cg.run(ty); + return cg.found; + } + else { - ContainsGenerics cg{generics}; + ContainsGenerics cg{generics}; cg.traverse(ty); return cg.found; } -}; +} } // namespace @@ -1805,7 +1870,7 @@ bool ConstraintSolver::tryDispatch(const FunctionCheckConstraint& c, NotNullself ? 1 : 0; - if (FFlag::LuauPushTypeConstraintLambdas2 && FFlag::LuauPushTypeConstraint2) + if (FFlag::LuauPushTypeConstraintLambdas2) { Subtyping subtyping{builtinTypes, arena, normalizer, typeFunctionRuntime, NotNull{&iceReporter}}; @@ -1866,7 +1931,7 @@ bool ConstraintSolver::tryDispatch(const FunctionCheckConstraint& c, NotNull expectedLambdaArgTys = flatten(expectedLambdaTy->argTypes).first; @@ -1884,7 +1949,7 @@ bool ConstraintSolver::tryDispatch(const FunctionCheckConstraint& c, NotNullis() || expr->is() || expr->is() || expr->is() || expr->is()) { - if (ContainsGenerics::hasGeneric(expectedArgTy, NotNull{&genericTypesAndPacks})) + if (containsGeneric(expectedArgTy, NotNull{&genericTypesAndPacks})) { replacer.resetState(TxnLog::empty(), arena); if (auto res = replacer.substitute(expectedArgTy)) @@ -1894,47 +1959,39 @@ bool ConstraintSolver::tryDispatch(const FunctionCheckConstraint& c, NotNull + // + // local function move(dirs: { Direction }) --[[...]] end + // + // move({ "Left", "Right", "Left", "Right" }) + // + // We need `keyof` to reduce prior to inferring that the + // arguments to `move` must generalize to their lower bounds. This + // is how we ensure that ordering. + if (!force && !result.incompleteTypes.empty()) { - Subtyping subtyping{builtinTypes, arena, normalizer, typeFunctionRuntime, NotNull{&iceReporter}}; - PushTypeResult result = pushTypeInto( - c.astTypes, c.astExpectedTypes, NotNull{this}, constraint, NotNull{&u2}, NotNull{&subtyping}, expectedArgTy, expr - ); - // Consider: - // - // local Direction = { Left = 1, Right = 2 } - // type Direction = keyof - // - // local function move(dirs: { Direction }) --[[...]] end - // - // move({ "Left", "Right", "Left", "Right" }) - // - // We need `keyof` to reduce prior to inferring that the - // arguments to `move` must generalize to their lower bounds. This - // is how we ensure that ordering. - if (!force && !result.incompleteTypes.empty()) + for (const auto& [newExpectedTy, newTargetTy, newExpr] : result.incompleteTypes) { - for (const auto& [newExpectedTy, newTargetTy, newExpr] : result.incompleteTypes) - { - auto addition = pushConstraint( - constraint->scope, - constraint->location, - PushTypeConstraint{ - newExpectedTy, - newTargetTy, - /* astTypes */ c.astTypes, - /* astExpectedTypes */ c.astExpectedTypes, - /* expr */ NotNull{newExpr}, - } - ); - inheritBlocks(constraint, addition); - } + auto addition = pushConstraint( + constraint->scope, + constraint->location, + PushTypeConstraint{ + newExpectedTy, + newTargetTy, + /* astTypes */ c.astTypes, + /* astExpectedTypes */ c.astExpectedTypes, + /* expr */ NotNull{newExpr}, + } + ); + inheritBlocks(constraint, addition); } } - else - { - u2.unify(actualArgTy, expectedArgTy); - } } } } @@ -2978,13 +3035,13 @@ TypeId ConstraintSolver::instantiateFunctionType( replacementPacks[*typePackParametersIter++] = typePackArgument; } + // FIXME: replacer is _not_ actually instantiation, but we don't have a real instantiation implementation at this moment Replacer replacer{arena, std::move(replacements), std::move(replacementPacks)}; return replacer.substitute(functionTypeId).value_or(builtinTypes->errorType); } bool ConstraintSolver::tryDispatch(const PushTypeConstraint& c, NotNull constraint, bool force) { - LUAU_ASSERT(FFlag::LuauPushTypeConstraint2); Unifier2 u2{arena, builtinTypes, constraint->scope, NotNull{&iceReporter}, &uninhabitedTypeFunctions}; Subtyping subtyping{builtinTypes, arena, normalizer, typeFunctionRuntime, NotNull{&iceReporter}}; @@ -3404,21 +3461,13 @@ TablePropLookupResult ConstraintSolver::lookupTableProp( { const TypeId upperBound = follow(ft->upperBound); - if (FFlag::LuauExtendSealedTableUpperBounds) + if (get(upperBound) || get(upperBound)) { - if (get(upperBound) || get(upperBound)) - { - TablePropLookupResult res = lookupTableProp(constraint, upperBound, propName, context, inConditional, suppressSimplification, seen); - // Here, res.propType is empty if res is a sealed table or a primitive that lacks the property. - // When this happens, we still want to add to the upper bound of the type. - if (res.propType) - return res; - } - } - else - { - if (get(upperBound) || get(upperBound)) - return lookupTableProp(constraint, upperBound, propName, context, inConditional, suppressSimplification, seen); + TablePropLookupResult res = lookupTableProp(constraint, upperBound, propName, context, inConditional, suppressSimplification, seen); + // Here, res.propType is empty if res is a sealed table or a primitive that lacks the property. + // When this happens, we still want to add to the upper bound of the type. + if (res.propType) + return res; } NotNull scope{ft->scope}; @@ -3547,22 +3596,17 @@ bool ConstraintSolver::unify(NotNull constraint, TID subTy, TI } else { - if (FFlag::LuauLimitUnification) + switch (unifyResult) { - switch (unifyResult) - { - case Luau::UnifyResult::Ok: - break; - case Luau::UnifyResult::OccursCheckFailed: - reportError(OccursCheckFailed{}, constraint->location); - break; - case Luau::UnifyResult::TooComplex: - reportError(UnificationTooComplex{}, constraint->location); - break; - } - } - else + case Luau::UnifyResult::Ok: + break; + case Luau::UnifyResult::OccursCheckFailed: reportError(OccursCheckFailed{}, constraint->location); + break; + case Luau::UnifyResult::TooComplex: + reportError(UnificationTooComplex{}, constraint->location); + break; + } return false; } diff --git a/Analysis/src/EmbeddedBuiltinDefinitions.cpp b/Analysis/src/EmbeddedBuiltinDefinitions.cpp index a4c568df7..98052ea66 100644 --- a/Analysis/src/EmbeddedBuiltinDefinitions.cpp +++ b/Analysis/src/EmbeddedBuiltinDefinitions.cpp @@ -1,7 +1,6 @@ // This file is part of the Luau programming language and is licensed under MIT License; see LICENSE.txt for details #include "Luau/BuiltinDefinitions.h" -LUAU_FASTFLAGVARIABLE(LuauTypeCheckerVectorLerp2) LUAU_FASTFLAGVARIABLE(LuauTypeCheckerMathIsNanInfFinite) LUAU_FASTFLAGVARIABLE(LuauUseTopTableForTableClearAndIsFrozen) @@ -376,36 +375,6 @@ declare vector: { )BUILTIN_SRC"; -static const char* const kBuiltinDefinitionVectorSrc_DEPRECATED = R"BUILTIN_SRC( - --- While vector would have been better represented as a built-in primitive type, type solver extern type handling covers most of the properties -declare extern type vector with - x: number - y: number - z: number -end - -declare vector: { - create: @checked (x: number, y: number, z: number?) -> vector, - magnitude: @checked (vec: vector) -> number, - normalize: @checked (vec: vector) -> vector, - cross: @checked (vec1: vector, vec2: vector) -> vector, - dot: @checked (vec1: vector, vec2: vector) -> number, - angle: @checked (vec1: vector, vec2: vector, axis: vector?) -> number, - floor: @checked (vec: vector) -> vector, - ceil: @checked (vec: vector) -> vector, - abs: @checked (vec: vector) -> vector, - sign: @checked (vec: vector) -> vector, - clamp: @checked (vec: vector, min: vector, max: vector) -> vector, - max: @checked (vector, ...vector) -> vector, - min: @checked (vector, ...vector) -> vector, - - zero: vector, - one: vector, -} - -)BUILTIN_SRC"; - std::string getBuiltinDefinitionSource() { std::string result = kBuiltinDefinitionBaseSrc; @@ -432,14 +401,7 @@ std::string getBuiltinDefinitionSource() result += kBuiltinDefinitionDebugSrc; result += kBuiltinDefinitionUtf8Src; result += kBuiltinDefinitionBufferSrc; - if (FFlag::LuauTypeCheckerVectorLerp2) - { - result += kBuiltinDefinitionVectorSrc; - } - else - { - result += kBuiltinDefinitionVectorSrc_DEPRECATED; - } + result += kBuiltinDefinitionVectorSrc; return result; } diff --git a/Analysis/src/Error.cpp b/Analysis/src/Error.cpp index 9f412ba8a..3d372e3c7 100644 --- a/Analysis/src/Error.cpp +++ b/Analysis/src/Error.cpp @@ -21,6 +21,7 @@ LUAU_FASTINTVARIABLE(LuauIndentTypeMismatchMaxTypeLength, 10) LUAU_FASTFLAGVARIABLE(LuauNewNonStrictReportsOneIndexedErrors) LUAU_FASTFLAG(LuauUnknownGlobalFixSuggestion) LUAU_FASTFLAGVARIABLE(LuauNewNonStrictBetterCheckedFunctionErrorMessage) +LUAU_FASTFLAGVARIABLE(LuauBetterTypeMismatchErrors) static std::string wrongNumberOfArgsString( size_t expectedCount, @@ -117,9 +118,32 @@ struct ErrorConverter std::string given = givenModule ? quote(givenType) + " from " + quote(*givenModule) : quote(givenType); std::string wanted = wantedModule ? quote(wantedType) + " from " + quote(*wantedModule) : quote(wantedType); size_t luauIndentTypeMismatchMaxTypeLength = size_t(FInt::LuauIndentTypeMismatchMaxTypeLength); - if (givenType.length() <= luauIndentTypeMismatchMaxTypeLength || wantedType.length() <= luauIndentTypeMismatchMaxTypeLength) - return "Type " + given + " could not be converted into " + wanted; - return "Type\n\t" + given + "\ncould not be converted into\n\t" + wanted; + if (FFlag::LuauBetterTypeMismatchErrors) + { + if (get(follow(tm.wantedType))) + { + if (givenType.length() <= luauIndentTypeMismatchMaxTypeLength) + return "Expected this to be unreachable, but got " + given; + return "Expected this to be unreachable, but got\n\t" + given; + } + + if (tm.context == TypeMismatch::InvariantContext) + { + if (givenType.length() <= luauIndentTypeMismatchMaxTypeLength || wantedType.length() <= luauIndentTypeMismatchMaxTypeLength) + return "Expected this to be exactly " + wanted + ", but got " + given; + return "Expected this to be exactly\n\t" + wanted + "\nbut got\n\t" + given; + } + + if (givenType.length() <= luauIndentTypeMismatchMaxTypeLength || wantedType.length() <= luauIndentTypeMismatchMaxTypeLength) + return "Expected this to be " + wanted + ", but got " + given; + return "Expected this to be\n\t" + wanted + "\nbut got\n\t" + given; + } + else + { + if (givenType.length() <= luauIndentTypeMismatchMaxTypeLength || wantedType.length() <= luauIndentTypeMismatchMaxTypeLength) + return "Type " + given + " could not be converted into " + wanted; + return "Type\n\t" + given + "\ncould not be converted into\n\t" + wanted; + } }; if (givenTypeName == wantedTypeName) @@ -159,7 +183,7 @@ struct ErrorConverter { result += "; " + tm.reason; } - else if (tm.context == TypeMismatch::InvariantContext) + else if (!FFlag::LuauBetterTypeMismatchErrors && tm.context == TypeMismatch::InvariantContext) { result += " in an invariant context"; } @@ -235,7 +259,6 @@ struct ErrorConverter std::string operator()(const Luau::CountMismatch& e) const { const std::string expectedS = e.expected == 1 ? "" : "s"; - const std::string actualS = e.actual == 1 ? "" : "s"; const std::string actualVerb = e.actual == 1 ? "is" : "are"; switch (e.context) @@ -597,7 +620,9 @@ struct ErrorConverter std::string operator()(const TypePackMismatch& e) const { - std::string ss = "Type pack '" + toString(e.givenTp) + "' could not be converted into '" + toString(e.wantedTp) + "'"; + std::string ss = FFlag::LuauBetterTypeMismatchErrors + ? "Expected this to be '" + toString(e.wantedTp) + "', but got '" + toString(e.givenTp) + "'" + : "Type pack '" + toString(e.givenTp) + "' could not be converted into '" + toString(e.wantedTp) + "'"; if (!e.reason.empty()) ss += "; " + e.reason; diff --git a/Analysis/src/Instantiation2.cpp b/Analysis/src/Instantiation2.cpp index 5cc8a3d34..2d8e797c2 100644 --- a/Analysis/src/Instantiation2.cpp +++ b/Analysis/src/Instantiation2.cpp @@ -4,7 +4,7 @@ #include "Luau/Scope.h" #include "Luau/Instantiation2.h" -LUAU_FASTFLAGVARIABLE(LuauInstantiationUsesGenericPolarity) +LUAU_FASTFLAGVARIABLE(LuauInstantiationUsesGenericPolarity2) namespace Luau { @@ -44,7 +44,7 @@ bool Instantiation2::isDirty(TypePackId tp) TypeId Instantiation2::clean(TypeId ty) { - if (FFlag::LuauInstantiationUsesGenericPolarity) + if (FFlag::LuauInstantiationUsesGenericPolarity2) { LUAU_ASSERT(subtyping && scope); auto generic = get(ty); @@ -56,54 +56,39 @@ TypeId Instantiation2::clean(TypeId ty) LUAU_ASSERT(ft); TypeId res; - switch (generic->polarity) + if (is(ft->lowerBound)) { - case Polarity::Positive: + // If the lower bound is never, assume that we can pick the + // upper bound, and that this will provide a reasonable type. + // + // If we have a mixed generic who's free type is totally + // unbound (the upper bound is `unknown` and the lower + // bound is `never`), then we instantiate it to `unknown`. + // This seems ... fine. res = ft->upperBound; - break; - case Polarity::Negative: + } + else if (is(ft->upperBound)) + { + // If the upper bound is unknown, assume we can pick the + // lower bound, and that this will provide a reasonable + // type. res = ft->lowerBound; - break; - case Polarity::None: - case Polarity::Mixed: - case Polarity::Unknown: - default: { - if (is(ft->lowerBound)) - { - // If the lower bound is never, assume that we can pick the - // upper bound, and that this will provide a reasonable type. - // - // If we have a mixed generic who's free type is totally - // unbound (the upper bound is `unknown` and the lower - // bound is `never`), then we instantiate it to `unknown`. - // This seems ... fine. - res = ft->upperBound; - } - else if (is(ft->upperBound)) - { - // If the upper bound is unknown, assume we can pick the - // lower bound, and that this will provide a reasonable - // type. - res = ft->lowerBound; - } - else - { - // Imagine that we have some set of bounds on a free type: - // - // Q <: 'a <: Z - // - // If we have a mixed generic, then the upper and lower bounds - // should inform what type to instantiate. In fact, we should - // pick the intersection between the two. If our bounds are - // coherent, then Q <: Z, meaning that Q & Z == Q. - // - // If `Q isSubtype(ft->lowerBound, ft->upperBound, NotNull{scope}); - res = r.isSubtype ? ft->lowerBound : ft->upperBound; - } - break; } + else + { + // Imagine that we have some set of bounds on a free type: + // + // Q <: 'a <: Z + // + // If we have a mixed generic, then the upper and lower bounds + // should inform what type to instantiate. In fact, we should + // pick the intersection between the two. If our bounds are + // coherent, then Q <: Z, meaning that Q & Z == Q. + // + // If `Q isSubtype(ft->lowerBound, ft->upperBound, NotNull{scope}); + res = r.isSubtype ? ft->lowerBound : ft->upperBound; } // Instantiation should not traverse into the type that we are substituting for. diff --git a/Analysis/src/IterativeTypeVisitor.cpp b/Analysis/src/IterativeTypeVisitor.cpp new file mode 100644 index 000000000..49e7fa041 --- /dev/null +++ b/Analysis/src/IterativeTypeVisitor.cpp @@ -0,0 +1,596 @@ +// This file is part of the Luau programming language and is licensed under MIT License; see LICENSE.txt for details +#include "Luau/IterativeTypeVisitor.h" + +LUAU_FASTINT(LuauVisitRecursionLimit) + +namespace Luau +{ + +IterativeTypeVisitor::WorkItem::WorkItem(TypeId ty, int32_t parent) + : t(ty) + , isType(true) + , parent(parent) +{ +} + +IterativeTypeVisitor::WorkItem::WorkItem(TypePackId tp, int32_t parent) + : t(tp) + , isType(false) + , parent(parent) +{ +} + +const TypeId* IterativeTypeVisitor::WorkItem::asType() const +{ + if (isType) + return reinterpret_cast(&t); + else + return nullptr; +} + +const TypePackId* IterativeTypeVisitor::WorkItem::asTypePack() const +{ + if (isType) + return nullptr; + else + return reinterpret_cast(&t); +} + +bool IterativeTypeVisitor::WorkItem::operator==(TypeId ty) const +{ + auto asTy = asType(); + return asTy != nullptr && *asTy == ty; +} + +bool IterativeTypeVisitor::WorkItem::operator==(TypePackId tp) const +{ + auto asTp = asTypePack(); + return asTp != nullptr && *asTp == tp; +} + +IterativeTypeVisitor::IterativeTypeVisitor(std::string visitorName, bool skipBoundTypes) + : IterativeTypeVisitor(std::move(visitorName), SeenSet{nullptr}, /*visitOnce*/ true, skipBoundTypes) +{ +} + +IterativeTypeVisitor::IterativeTypeVisitor(std::string visitorName, bool visitOnce, bool skipBoundTypes) + : IterativeTypeVisitor(std::move(visitorName), SeenSet{nullptr}, visitOnce, skipBoundTypes) +{ +} + +IterativeTypeVisitor::IterativeTypeVisitor(std::string visitorName, SeenSet seen, bool visitOnce, bool skipBoundTypes) + : seen(std::move(seen)) + , visitorName(std::move(visitorName)) + , skipBoundTypes(skipBoundTypes) + , visitOnce(visitOnce) +{ + // Skip the first few doublings. Almost all visits require less than 32 steps. + workQueue.reserve(32); +} + +void IterativeTypeVisitor::cycle(TypeId) {} +void IterativeTypeVisitor::cycle(TypePackId) {} + +bool IterativeTypeVisitor::visit(TypeId ty) +{ + return true; +} + +bool IterativeTypeVisitor::visit(TypeId ty, const BoundType& btv) +{ + return visit(ty); +} + +bool IterativeTypeVisitor::visit(TypeId ty, const FreeType& ftv) +{ + return visit(ty); +} + +bool IterativeTypeVisitor::visit(TypeId ty, const GenericType& gtv) +{ + return visit(ty); +} + +bool IterativeTypeVisitor::visit(TypeId ty, const ErrorType& etv) +{ + return visit(ty); +} + +bool IterativeTypeVisitor::visit(TypeId ty, const PrimitiveType& ptv) +{ + return visit(ty); +} + +bool IterativeTypeVisitor::visit(TypeId ty, const FunctionType& ftv) +{ + return visit(ty); +} + +bool IterativeTypeVisitor::visit(TypeId ty, const TableType& ttv) +{ + return visit(ty); +} + +bool IterativeTypeVisitor::visit(TypeId ty, const MetatableType& mtv) +{ + return visit(ty); +} + +bool IterativeTypeVisitor::visit(TypeId ty, const ExternType& etv) +{ + return visit(ty); +} + +bool IterativeTypeVisitor::visit(TypeId ty, const AnyType& atv) +{ + return visit(ty); +} + +bool IterativeTypeVisitor::visit(TypeId ty, const NoRefineType& nrt) +{ + return visit(ty); +} + +bool IterativeTypeVisitor::visit(TypeId ty, const UnknownType& utv) +{ + return visit(ty); +} + +bool IterativeTypeVisitor::visit(TypeId ty, const NeverType& ntv) +{ + return visit(ty); +} + +bool IterativeTypeVisitor::visit(TypeId ty, const UnionType& utv) +{ + return visit(ty); +} + +bool IterativeTypeVisitor::visit(TypeId ty, const IntersectionType& itv) +{ + return visit(ty); +} + +bool IterativeTypeVisitor::visit(TypeId ty, const BlockedType& btv) +{ + return visit(ty); +} + +bool IterativeTypeVisitor::visit(TypeId ty, const PendingExpansionType& petv) +{ + return visit(ty); +} + +bool IterativeTypeVisitor::visit(TypeId ty, const SingletonType& stv) +{ + return visit(ty); +} + +bool IterativeTypeVisitor::visit(TypeId ty, const NegationType& ntv) +{ + return visit(ty); +} + +bool IterativeTypeVisitor::visit(TypeId ty, const TypeFunctionInstanceType& tfit) +{ + return visit(ty); +} + + +bool IterativeTypeVisitor::visit(TypePackId tp) +{ + return true; +} + +bool IterativeTypeVisitor::visit(TypePackId tp, const BoundTypePack& btp) +{ + return visit(tp); +} + +bool IterativeTypeVisitor::visit(TypePackId tp, const FreeTypePack& ftp) +{ + return visit(tp); +} + +bool IterativeTypeVisitor::visit(TypePackId tp, const GenericTypePack& gtp) +{ + return visit(tp); +} + +bool IterativeTypeVisitor::visit(TypePackId tp, const ErrorTypePack& etp) +{ + return visit(tp); +} + +bool IterativeTypeVisitor::visit(TypePackId tp, const TypePack& pack) +{ + return visit(tp); +} + +bool IterativeTypeVisitor::visit(TypePackId tp, const VariadicTypePack& vtp) +{ + return visit(tp); +} + +bool IterativeTypeVisitor::visit(TypePackId tp, const BlockedTypePack& btp) +{ + return visit(tp); +} + +bool IterativeTypeVisitor::visit(TypePackId tp, const TypeFunctionInstanceTypePack& tfitp) +{ + return visit(tp); +} + +void IterativeTypeVisitor::run(TypeId rootTy) +{ + parentCursor = -1; + workCursor = 0; + workQueue.clear(); + traverse(rootTy); + processWorkQueue(); +} + +void IterativeTypeVisitor::run(TypePackId rootTp) +{ + parentCursor = -1; + workCursor = 0; + workQueue.clear(); + traverse(rootTp); + processWorkQueue(); +} + +void IterativeTypeVisitor::traverse(TypeId ty) +{ + workQueue.emplace_back(ty, parentCursor); +} + +void IterativeTypeVisitor::traverse(TypePackId tp) +{ + workQueue.emplace_back(tp, parentCursor); +} + +void IterativeTypeVisitor::process(TypeId ty) +{ + // Morally, if `skipBoundTypes` is set, then whenever we encounter a bound + // type we should "skip" ahead to the first non-bound type. + // + // We do this check here so that we treat all bound types as if they're + // direct pointers to some final non-bound type. If we do the check later, + // then we might get slightly different behavior depending on the exact + // entry point for cyclic types. + if (skipBoundTypes) + { + if (is(ty)) + ty = follow(ty); + else if (const TableType* tt = get(ty); tt && tt->boundTo) + ty = follow(ty); + } + + if (hasSeen(ty)) + return; + + if (auto btv = get(ty)) + { + // At this point, we know that `skipBoundTypes` is false, as + // otherwise we would have hit the above branch. + LUAU_ASSERT(!skipBoundTypes); + if (visit(ty, *btv)) + traverse(btv->boundTo); + } + else if (auto ftv = get(ty)) + { + if (visit(ty, *ftv)) + { + LUAU_ASSERT(ftv->lowerBound); + LUAU_ASSERT(ftv->upperBound); + + traverse(ftv->lowerBound); + traverse(ftv->upperBound); + } + } + else if (auto gtv = get(ty)) + visit(ty, *gtv); + else if (auto etv = get(ty)) + visit(ty, *etv); + else if (auto ptv = get(ty)) + visit(ty, *ptv); + else if (auto ftv = get(ty)) + { + if (visit(ty, *ftv)) + { + traverse(ftv->argTypes); + traverse(ftv->retTypes); + } + } + else if (auto ttv = get(ty)) + { + // Some visitors want to see bound tables, that's why we traverse the original type + LUAU_ASSERT(!skipBoundTypes || !ttv->boundTo); + if (skipBoundTypes && ttv->boundTo) + { + traverse(*ttv->boundTo); + } + else if (visit(ty, *ttv)) + { + if (ttv->boundTo) + { + traverse(*ttv->boundTo); + } + else + { + for (auto& [_name, prop] : ttv->props) + { + if (auto ty = prop.readTy) + traverse(*ty); + + // In the case that the readType and the writeType + // are the same pointer, just traverse once. + // Traversing each property twice has pretty + // significant performance consequences. + if (auto ty = prop.writeTy; ty && !prop.isShared()) + traverse(*ty); + } + + if (ttv->indexer) + { + traverse(ttv->indexer->indexType); + traverse(ttv->indexer->indexResultType); + } + } + } + } + else if (auto mtv = get(ty)) + { + if (visit(ty, *mtv)) + { + traverse(mtv->table); + traverse(mtv->metatable); + } + } + else if (auto etv = get(ty)) + { + if (visit(ty, *etv)) + { + for (const auto& [name, prop] : etv->props) + { + if (auto ty = prop.readTy) + traverse(*ty); + + // In the case that the readType and the writeType are + // the same pointer, just traverse once. Traversing each + // property twice would have pretty significant + // performance consequences. + if (auto ty = prop.writeTy; ty && !prop.isShared()) + traverse(*ty); + } + + if (etv->parent) + traverse(*etv->parent); + + if (etv->metatable) + traverse(*etv->metatable); + + if (etv->indexer) + { + traverse(etv->indexer->indexType); + traverse(etv->indexer->indexResultType); + } + } + } + else if (auto atv = get(ty)) + visit(ty, *atv); + else if (auto nrt = get(ty)) + visit(ty, *nrt); + else if (auto utv = get(ty)) + { + if (visit(ty, *utv)) + { + bool unionChanged = false; + for (TypeId optTy : utv->options) + { + traverse(optTy); + if (!get(follow(ty))) + { + unionChanged = true; + break; + } + } + + if (unionChanged) + traverse(ty); + } + } + else if (auto itv = get(ty)) + { + if (visit(ty, *itv)) + { + bool intersectionChanged = false; + for (TypeId partTy : itv->parts) + { + traverse(partTy); + if (!get(follow(ty))) + { + intersectionChanged = true; + break; + } + } + + if (intersectionChanged) + traverse(ty); + } + } + else if (auto ltv = get(ty)) + { + if (TypeId unwrapped = ltv->unwrapped) + traverse(unwrapped); + + // Visiting into LazyType that hasn't been unwrapped may necessarily + // cause infinite expansion, so we don't do that on purpose. Asserting + // also makes no sense, because the type _will_ happen here, most likely + // as a property of some ExternType that doesn't need to be expanded. + } + else if (auto stv = get(ty)) + visit(ty, *stv); + else if (auto btv = get(ty)) + visit(ty, *btv); + else if (auto utv = get(ty)) + visit(ty, *utv); + else if (auto ntv = get(ty)) + visit(ty, *ntv); + else if (auto petv = get(ty)) + { + if (visit(ty, *petv)) + { + for (TypeId a : petv->typeArguments) + traverse(a); + + for (TypePackId a : petv->packArguments) + traverse(a); + } + } + else if (auto ntv = get(ty)) + { + if (visit(ty, *ntv)) + traverse(ntv->ty); + } + else if (auto tfit = get(ty)) + { + if (visit(ty, *tfit)) + { + for (TypeId p : tfit->typeArguments) + traverse(p); + + for (TypePackId p : tfit->packArguments) + traverse(p); + } + } + else + LUAU_ASSERT(!"GenericTypeVisitor::traverse(TypeId) is not exhaustive!"); + + unsee(ty); +} + +void IterativeTypeVisitor::process(TypePackId tp) +{ + if (hasSeen(tp)) + return; + + if (auto btv = get(tp)) + { + if (visit(tp, *btv)) + traverse(btv->boundTo); + } + + else if (auto ftv = get(tp)) + visit(tp, *ftv); + + else if (auto gtv = get(tp)) + visit(tp, *gtv); + + else if (auto etv = get(tp)) + visit(tp, *etv); + + else if (auto pack = get(tp)) + { + bool res = visit(tp, *pack); + if (res) + { + for (TypeId ty : pack->head) + traverse(ty); + + if (pack->tail) + traverse(*pack->tail); + } + } + else if (auto pack = get(tp)) + { + bool res = visit(tp, *pack); + if (res) + traverse(pack->ty); + } + else if (auto btp = get(tp)) + visit(tp, *btp); + else if (auto tfitp = get(tp)) + { + if (visit(tp, *tfitp)) + { + for (TypeId t : tfitp->typeArguments) + traverse(t); + + for (TypePackId t : tfitp->packArguments) + traverse(t); + } + } + else + LUAU_ASSERT(!"GenericTypeVisitor::traverse(TypePackId) is not exhaustive!"); + + unsee(tp); +} + +bool IterativeTypeVisitor::hasSeen(const void* tv) +{ + if (!visitOnce) + return false; + + bool isFresh = seen.insert(tv); + return !isFresh; +} + +void IterativeTypeVisitor::unsee(const void* tv) +{ + if (!visitOnce) + seen.erase(tv); +} + +template +bool IterativeTypeVisitor::isCyclic(TID ty) const +{ + const IterativeTypeVisitor::WorkItem* item = &workQueue[workCursor]; + int32_t cursor = workCursor; + + while (item->parent >= 0) + { + LUAU_ASSERT(item->parent < cursor); + cursor = item->parent; + item = &workQueue[cursor]; + + if (*item == ty) + return true; + } + + return false; +} + +void IterativeTypeVisitor::processWorkQueue() +{ + while (workCursor < workQueue.size()) + { + const WorkItem& item = workQueue[workCursor]; + parentCursor = workCursor; + + if (const TypeId* ty = item.asType()) + { + if (isCyclic(*ty)) + cycle(*ty); + else + process(*ty); + } + else if (const TypePackId* tp = item.asTypePack()) + { + if (isCyclic(*tp)) + cycle(*tp); + else + process(*tp); + } + else + { + LUAU_UNREACHABLE(); + LUAU_ASSERT(!"Unreachable"); + } + + ++workCursor; + } +} + +} // namespace Luau diff --git a/Analysis/src/NonStrictTypeChecker.cpp b/Analysis/src/NonStrictTypeChecker.cpp index efe45caba..26d44054c 100644 --- a/Analysis/src/NonStrictTypeChecker.cpp +++ b/Analysis/src/NonStrictTypeChecker.cpp @@ -23,7 +23,6 @@ LUAU_FASTFLAG(DebugLuauMagicTypes) LUAU_FASTINTVARIABLE(LuauNonStrictTypeCheckerRecursionLimit, 300) LUAU_FASTFLAGVARIABLE(LuauUnreducedTypeFunctionsDontTriggerWarnings) -LUAU_FASTFLAGVARIABLE(LuauNonStrictFetchScopeOnce) LUAU_FASTFLAGVARIABLE(LuauAddRecursionCounterToNonStrictTypeChecker) namespace Luau @@ -709,42 +708,21 @@ struct NonStrictTypeChecker } // Populate the context and now iterate through each of the arguments to the call to find out if we satisfy the types - if (FFlag::LuauNonStrictFetchScopeOnce) - { - NotNull scope{findInnermostScope(call->location)}; - for (size_t i = 0; i < arguments.size(); i++) - { - AstExpr* arg = arguments[i]; - if (auto runTimeFailureType = willRunTimeError(arg, fresh, scope)) - { - if (FFlag::LuauUnreducedTypeFunctionsDontTriggerWarnings) - reportError(CheckedFunctionCallError{argTypes[i], *runTimeFailureType, functionName, i}, arg->location); - else - { - if (!get(follow(*runTimeFailureType))) - reportError(CheckedFunctionCallError{argTypes[i], *runTimeFailureType, functionName, i}, arg->location); - } - } - } - } - else + NotNull scope{findInnermostScope(call->location)}; + for (size_t i = 0; i < arguments.size(); i++) { - for (size_t i = 0; i < arguments.size(); i++) + AstExpr* arg = arguments[i]; + if (auto runTimeFailureType = willRunTimeError(arg, fresh, scope)) { - AstExpr* arg = arguments[i]; - if (auto runTimeFailureType = willRunTimeError_DEPRECATED(arg, fresh)) + if (FFlag::LuauUnreducedTypeFunctionsDontTriggerWarnings) + reportError(CheckedFunctionCallError{argTypes[i], *runTimeFailureType, functionName, i}, arg->location); + else { - if (FFlag::LuauUnreducedTypeFunctionsDontTriggerWarnings) + if (!get(follow(*runTimeFailureType))) reportError(CheckedFunctionCallError{argTypes[i], *runTimeFailureType, functionName, i}, arg->location); - else - { - if (!get(follow(*runTimeFailureType))) - reportError(CheckedFunctionCallError{argTypes[i], *runTimeFailureType, functionName, i}, arg->location); - } } } } - if (arguments.size() < argTypes.size()) { // We are passing fewer arguments than we expect @@ -782,34 +760,17 @@ struct NonStrictTypeChecker // TODO: should a function being used as an expression generate a context without the arguments? auto pusher = pushStack(exprFn); NonStrictContext remainder = visit(exprFn->body); - if (FFlag::LuauNonStrictFetchScopeOnce) + auto scope = pusher ? pusher->scope : NotNull{module->getModuleScope().get()}; + for (AstLocal* local : exprFn->args) { - auto scope = pusher ? pusher->scope : NotNull{module->getModuleScope().get()}; - for (AstLocal* local : exprFn->args) + if (std::optional ty = willRunTimeErrorFunctionDefinition(local, scope, remainder)) { - if (std::optional ty = willRunTimeErrorFunctionDefinition(local, scope, remainder)) - { - const char* debugname = exprFn->debugname.value; - reportError(NonStrictFunctionDefinitionError{debugname ? debugname : "", local->name.value, *ty}, local->location); - } - remainder.remove(dfg->getDef(local)); - - visit(local->annotation); + const char* debugname = exprFn->debugname.value; + reportError(NonStrictFunctionDefinitionError{debugname ? debugname : "", local->name.value, *ty}, local->location); } - } - else - { - for (AstLocal* local : exprFn->args) - { - if (std::optional ty = willRunTimeErrorFunctionDefinition_DEPRECATED(local, remainder)) - { - const char* debugname = exprFn->debugname.value; - reportError(NonStrictFunctionDefinitionError{debugname ? debugname : "", local->name.value, *ty}, local->location); - } - remainder.remove(dfg->getDef(local)); + remainder.remove(dfg->getDef(local)); - visit(local->annotation); - } + visit(local->annotation); } visitGenerics(exprFn->generics, exprFn->genericPacks); @@ -1254,33 +1215,6 @@ struct NonStrictTypeChecker return {}; } - // If this fragment of the ast will run time error, return the type that causes this - // Clip with LuauNonStrictFetchScopeOnce - std::optional willRunTimeError_DEPRECATED(AstExpr* fragment, const NonStrictContext& context) - { - NotNull scope{Luau::findScopeAtPosition(*module, fragment->location.end).get()}; - DefId def = dfg->getDef(fragment); - std::vector defs; - collectOperands(def, &defs); - for (DefId def : defs) - { - if (std::optional contextTy = context.find(def)) - { - - TypeId actualType = lookupType(fragment); - if (FFlag::LuauUnreducedTypeFunctionsDontTriggerWarnings && shouldSkipRuntimeErrorTesting(actualType)) - continue; - SubtypingResult r = subtyping.isSubtype(actualType, *contextTy, scope); - if (r.normalizationTooComplex) - reportError(NormalizationTooComplex{}, fragment->location); - if (r.isSubtype) - return {actualType}; - } - } - - return {}; - } - std::optional willRunTimeErrorFunctionDefinition(AstLocal* fragment, NotNull scope, const NonStrictContext& context) { DefId def = dfg->getDef(fragment); @@ -1302,29 +1236,6 @@ struct NonStrictTypeChecker return {}; } - // Clip with LuauNonStrictFetchScopeOnce - std::optional willRunTimeErrorFunctionDefinition_DEPRECATED(AstLocal* fragment, const NonStrictContext& context) - { - NotNull scope{Luau::findScopeAtPosition(*module, fragment->location.end).get()}; - DefId def = dfg->getDef(fragment); - std::vector defs; - collectOperands(def, &defs); - for (DefId def : defs) - { - if (std::optional contextTy = context.find(def)) - { - SubtypingResult r1 = subtyping.isSubtype(builtinTypes->unknownType, *contextTy, scope); - SubtypingResult r2 = subtyping.isSubtype(*contextTy, builtinTypes->unknownType, scope); - if (r1.normalizationTooComplex || r2.normalizationTooComplex) - reportError(NormalizationTooComplex{}, fragment->location); - bool isUnknown = r1.isSubtype && r2.isSubtype; - if (isUnknown) - return {builtinTypes->unknownType}; - } - } - return {}; - } - private: int nonStrictRecursionCount = 0; diff --git a/Analysis/src/Normalize.cpp b/Analysis/src/Normalize.cpp index c82f15f59..801ce1232 100644 --- a/Analysis/src/Normalize.cpp +++ b/Analysis/src/Normalize.cpp @@ -18,14 +18,10 @@ LUAU_FASTFLAGVARIABLE(DebugLuauCheckNormalizeInvariant) LUAU_FASTINTVARIABLE(LuauNormalizeCacheLimit, 100000) -LUAU_FASTINTVARIABLE(LuauNormalizeIntersectionLimit, 200) -LUAU_FASTINTVARIABLE(LuauNormalizeUnionLimit, 100) LUAU_FASTFLAG(LuauSolverV2) LUAU_FASTFLAG(LuauUseWorkspacePropToChooseSolver) LUAU_FASTFLAGVARIABLE(LuauImproveNormalizeExternTypeCheck) -LUAU_FASTFLAGVARIABLE(LuauNormalizerUnionTyvarsTakeMaxSize) LUAU_FASTFLAGVARIABLE(LuauNormalizationPreservesAny) -LUAU_FASTFLAGVARIABLE(LuauNormalizerStepwiseFuel) LUAU_FASTINTVARIABLE(LuauNormalizerInitialFuel, 3000) namespace Luau @@ -367,22 +363,14 @@ static bool isShallowInhabited(const NormalizedType& norm) NormalizationResult Normalizer::isInhabited(const NormalizedType* norm) { Set seen{nullptr}; - - if (FFlag::LuauNormalizerStepwiseFuel) + try { - try - { - FuelInitializer fi{NotNull{this}}; - return isInhabited(norm, seen); - } - catch (const NormalizerHitLimits&) - { - return NormalizationResult::HitLimits; - } + FuelInitializer fi{NotNull{this}}; + return isInhabited(norm, seen); } - else + catch (const NormalizerHitLimits&) { - return isInhabited(norm, seen); + return NormalizationResult::HitLimits; } } @@ -392,8 +380,7 @@ NormalizationResult Normalizer::isInhabited(const NormalizedType* norm, Set(norm->tops) || !get(norm->booleans) || !get(norm->errors) || !get(norm->nils) || !get(norm->numbers) || !get(norm->threads) || !get(norm->buffers) || !norm->externTypes.isNever() || @@ -426,29 +413,9 @@ NormalizationResult Normalizer::isInhabited(TypeId ty) } Set seen{nullptr}; - - if (FFlag::LuauNormalizerStepwiseFuel) - { - try - { - FuelInitializer fi{NotNull{this}}; - NormalizationResult result = isInhabited(ty, seen); - - if (cacheInhabitance && result == NormalizationResult::True) - cachedIsInhabited[ty] = true; - else if (cacheInhabitance && result == NormalizationResult::False) - cachedIsInhabited[ty] = false; - - return result; - } - catch (const NormalizerHitLimits&) - { - return NormalizationResult::HitLimits; - } - } - else + try { - + FuelInitializer fi{NotNull{this}}; NormalizationResult result = isInhabited(ty, seen); if (cacheInhabitance && result == NormalizationResult::True) @@ -458,6 +425,10 @@ NormalizationResult Normalizer::isInhabited(TypeId ty) return result; } + catch (const NormalizerHitLimits&) + { + return NormalizationResult::HitLimits; + } } NormalizationResult Normalizer::isInhabited(TypeId ty, Set& seen) @@ -466,8 +437,7 @@ NormalizationResult Normalizer::isInhabited(TypeId ty, Set& seen) if (!withinResourceLimits()) return NormalizationResult::HitLimits; - if (FFlag::LuauNormalizerStepwiseFuel) - consumeFuel(); + consumeFuel(); // TODO: use log.follow(ty), CLI-64291 ty = follow(ty); @@ -523,28 +493,20 @@ NormalizationResult Normalizer::isIntersectionInhabited(TypeId left, TypeId righ { Set seen{nullptr}; SeenTablePropPairs seenTablePropPairs{{nullptr, nullptr}}; - if (FFlag::LuauNormalizerStepwiseFuel) + try { - try - { - FuelInitializer fi{NotNull{this}}; - return isIntersectionInhabited(left, right, seenTablePropPairs, seen); - } - catch (const NormalizerHitLimits&) - { - return NormalizationResult::HitLimits; - } + FuelInitializer fi{NotNull{this}}; + return isIntersectionInhabited(left, right, seenTablePropPairs, seen); } - else + catch (const NormalizerHitLimits&) { - return isIntersectionInhabited(left, right, seenTablePropPairs, seen); + return NormalizationResult::HitLimits; } } NormalizationResult Normalizer::isIntersectionInhabited(TypeId left, TypeId right, SeenTablePropPairs& seenTablePropPairs, Set& seenSet) { - if (FFlag::LuauNormalizerStepwiseFuel) - consumeFuel(); + consumeFuel(); left = follow(left); right = follow(right); @@ -917,26 +879,17 @@ std::shared_ptr Normalizer::normalize(TypeId ty) Set seenSetTypes{nullptr}; SeenTablePropPairs seenTablePropPairs{{nullptr, nullptr}}; - if (FFlag::LuauNormalizerStepwiseFuel) - { - try - { - FuelInitializer fi{NotNull{this}}; - NormalizationResult res = unionNormalWithTy(norm, ty, seenTablePropPairs, seenSetTypes); - if (res != NormalizationResult::True) - return nullptr; - } - catch (const NormalizerHitLimits&) - { - return nullptr; - } - } - else + try { + FuelInitializer fi{NotNull{this}}; NormalizationResult res = unionNormalWithTy(norm, ty, seenTablePropPairs, seenSetTypes); if (res != NormalizationResult::True) return nullptr; } + catch (const NormalizerHitLimits&) + { + return nullptr; + } if (norm.isUnknown()) { @@ -962,8 +915,7 @@ NormalizationResult Normalizer::normalizeIntersections( if (!arena) sharedState->iceHandler->ice("Normalizing types outside a module"); - if (FFlag::LuauNormalizerStepwiseFuel) - consumeFuel(); + consumeFuel(); NormalizedType norm{builtinTypes}; norm.tops = FFlag::LuauNormalizationPreservesAny ? builtinTypes->unknownType : builtinTypes->anyType; @@ -1013,8 +965,7 @@ const TypeIds* Normalizer::cacheTypeIds(TypeIds tys) TypeId Normalizer::unionType(TypeId here, TypeId there) { - if (FFlag::LuauNormalizerStepwiseFuel) - consumeFuel(); + consumeFuel(); here = follow(here); there = follow(there); @@ -1062,8 +1013,7 @@ TypeId Normalizer::unionType(TypeId here, TypeId there) TypeId Normalizer::intersectionType(TypeId here, TypeId there) { - if (FFlag::LuauNormalizerStepwiseFuel) - consumeFuel(); + consumeFuel(); here = follow(here); there = follow(there); @@ -1123,8 +1073,7 @@ void Normalizer::clearCaches() // ------- Normalizing unions TypeId Normalizer::unionOfTops(TypeId here, TypeId there) { - if (FFlag::LuauNormalizerStepwiseFuel) - consumeFuel(); + consumeFuel(); if (get(here) || get(there)) return there; @@ -1134,8 +1083,7 @@ TypeId Normalizer::unionOfTops(TypeId here, TypeId there) TypeId Normalizer::unionOfBools(TypeId here, TypeId there) { - if (FFlag::LuauNormalizerStepwiseFuel) - consumeFuel(); + consumeFuel(); if (get(here)) return there; @@ -1150,8 +1098,7 @@ TypeId Normalizer::unionOfBools(TypeId here, TypeId there) void Normalizer::unionExternTypesWithExternType(TypeIds& heres, TypeId there) { - if (FFlag::LuauNormalizerStepwiseFuel) - consumeFuel(); + consumeFuel(); if (heres.count(there)) return; @@ -1175,8 +1122,7 @@ void Normalizer::unionExternTypesWithExternType(TypeIds& heres, TypeId there) void Normalizer::unionExternTypes(TypeIds& heres, const TypeIds& theres) { - if (FFlag::LuauNormalizerStepwiseFuel) - consumeFuel(); + consumeFuel(); for (TypeId there : theres) unionExternTypesWithExternType(heres, there); @@ -1195,8 +1141,7 @@ static bool isSubclass(TypeId test, TypeId parent) void Normalizer::unionExternTypesWithExternType(NormalizedExternType& heres, TypeId there) { - if (FFlag::LuauNormalizerStepwiseFuel) - consumeFuel(); + consumeFuel(); for (auto it = heres.ordering.begin(); it != heres.ordering.end();) { @@ -1276,8 +1221,7 @@ void Normalizer::unionExternTypes(NormalizedExternType& heres, const NormalizedE // have negations to worry about combining. The two aspects combine to make // the tasks this method must perform different enough to warrant a separate // implementation. - if (FFlag::LuauNormalizerStepwiseFuel) - consumeFuel(); + consumeFuel(); for (const TypeId thereTy : theres.ordering) { @@ -1367,8 +1311,7 @@ void Normalizer::unionExternTypes(NormalizedExternType& heres, const NormalizedE void Normalizer::unionStrings(NormalizedStringType& here, const NormalizedStringType& there) { - if (FFlag::LuauNormalizerStepwiseFuel) - consumeFuel(); + consumeFuel(); if (there.isString()) here.resetToString(); @@ -1414,8 +1357,7 @@ void Normalizer::unionStrings(NormalizedStringType& here, const NormalizedString std::optional Normalizer::unionOfTypePacks(TypePackId here, TypePackId there) { - if (FFlag::LuauNormalizerStepwiseFuel) - consumeFuel(); + consumeFuel(); if (here == there) return here; @@ -1544,8 +1486,7 @@ std::optional Normalizer::unionOfTypePacks(TypePackId here, TypePack std::optional Normalizer::unionOfFunctions(TypeId here, TypeId there) { - if (FFlag::LuauNormalizerStepwiseFuel) - consumeFuel(); + consumeFuel(); if (get(here)) return here; @@ -1584,8 +1525,7 @@ std::optional Normalizer::unionOfFunctions(TypeId here, TypeId there) void Normalizer::unionFunctions(NormalizedFunctionType& heres, const NormalizedFunctionType& theres) { - if (FFlag::LuauNormalizerStepwiseFuel) - consumeFuel(); + consumeFuel(); if (heres.isTop) return; @@ -1618,8 +1558,7 @@ void Normalizer::unionFunctions(NormalizedFunctionType& heres, const NormalizedF void Normalizer::unionFunctionsWithFunction(NormalizedFunctionType& heres, TypeId there) { - if (FFlag::LuauNormalizerStepwiseFuel) - consumeFuel(); + consumeFuel(); if (heres.isNever()) { @@ -1653,8 +1592,7 @@ void Normalizer::unionTablesWithTable(TypeIds& heres, TypeId there) void Normalizer::unionTables(TypeIds& heres, const TypeIds& theres) { - if (FFlag::LuauNormalizerStepwiseFuel) - consumeFuel(); + consumeFuel(); for (TypeId there : theres) { @@ -1692,8 +1630,7 @@ void Normalizer::unionTables(TypeIds& heres, const TypeIds& theres) // That's what you get for having a type system with generics, intersection and union types. NormalizationResult Normalizer::unionNormals(NormalizedType& here, const NormalizedType& there, int ignoreSmallerTyvars) { - if (FFlag::LuauNormalizerStepwiseFuel) - consumeFuel(); + consumeFuel(); here.isCacheable &= there.isCacheable; @@ -1707,21 +1644,6 @@ NormalizationResult Normalizer::unionNormals(NormalizedType& here, const Normali return NormalizationResult::True; } - if (!FFlag::LuauNormalizerStepwiseFuel) - { - if (FFlag::LuauNormalizerUnionTyvarsTakeMaxSize) - { - auto maxSize = std::max(here.tyvars.size(), there.tyvars.size()); - if (maxSize * maxSize >= size_t(FInt::LuauNormalizeUnionLimit)) - return NormalizationResult::HitLimits; - } - else - { - if (here.tyvars.size() * there.tyvars.size() >= size_t(FInt::LuauNormalizeUnionLimit)) - return NormalizationResult::HitLimits; - } - } - for (auto it = there.tyvars.begin(); it != there.tyvars.end(); it++) { TypeId tyvar = it->first; @@ -1742,13 +1664,6 @@ NormalizationResult Normalizer::unionNormals(NormalizedType& here, const Normali return res; } - if (!FFlag::LuauNormalizerStepwiseFuel) - { - // Limit based on worst-case expansion of the function unions - if (here.functions.parts.size() * there.functions.parts.size() >= size_t(FInt::LuauNormalizeUnionLimit)) - return NormalizationResult::HitLimits; - } - here.booleans = unionOfBools(here.booleans, there.booleans); unionExternTypes(here.externTypes, there.externTypes); @@ -1793,8 +1708,7 @@ bool Normalizer::useNewLuauSolver() const NormalizationResult Normalizer::intersectNormalWithNegationTy(TypeId toNegate, NormalizedType& intersect) { - if (FFlag::LuauNormalizerStepwiseFuel) - consumeFuel(); + consumeFuel(); std::optional negated; @@ -1820,8 +1734,7 @@ NormalizationResult Normalizer::unionNormalWithTy( if (!withinResourceLimits()) return NormalizationResult::HitLimits; - if (FFlag::LuauNormalizerStepwiseFuel) - consumeFuel(); + consumeFuel(); there = follow(there); @@ -1984,8 +1897,7 @@ NormalizationResult Normalizer::unionNormalWithTy( std::optional Normalizer::negateNormal(const NormalizedType& here) { - if (FFlag::LuauNormalizerStepwiseFuel) - consumeFuel(); + consumeFuel(); NormalizedType result{builtinTypes}; result.isCacheable = here.isCacheable; @@ -2083,8 +1995,7 @@ std::optional Normalizer::negateNormal(const NormalizedType& her TypeIds Normalizer::negateAll(const TypeIds& theres) { - if (FFlag::LuauNormalizerStepwiseFuel) - consumeFuel(); + consumeFuel(); TypeIds tys; for (TypeId there : theres) @@ -2094,8 +2005,7 @@ TypeIds Normalizer::negateAll(const TypeIds& theres) TypeId Normalizer::negate(TypeId there) { - if (FFlag::LuauNormalizerStepwiseFuel) - consumeFuel(); + consumeFuel(); there = follow(there); if (get(there)) @@ -2126,8 +2036,7 @@ TypeId Normalizer::negate(TypeId there) void Normalizer::subtractPrimitive(NormalizedType& here, TypeId ty) { - if (FFlag::LuauNormalizerStepwiseFuel) - consumeFuel(); + consumeFuel(); const PrimitiveType* ptv = get(follow(ty)); LUAU_ASSERT(ptv); @@ -2162,8 +2071,7 @@ void Normalizer::subtractPrimitive(NormalizedType& here, TypeId ty) void Normalizer::subtractSingleton(NormalizedType& here, TypeId ty) { - if (FFlag::LuauNormalizerStepwiseFuel) - consumeFuel(); + consumeFuel(); const SingletonType* stv = get(ty); LUAU_ASSERT(stv); @@ -2208,8 +2116,7 @@ void Normalizer::subtractSingleton(NormalizedType& here, TypeId ty) // ------- Normalizing intersections TypeId Normalizer::intersectionOfTops(TypeId here, TypeId there) { - if (FFlag::LuauNormalizerStepwiseFuel) - consumeFuel(); + consumeFuel(); if (FFlag::LuauNormalizationPreservesAny) { @@ -2239,8 +2146,7 @@ TypeId Normalizer::intersectionOfTops(TypeId here, TypeId there) TypeId Normalizer::intersectionOfBools(TypeId here, TypeId there) { - if (FFlag::LuauNormalizerStepwiseFuel) - consumeFuel(); + consumeFuel(); if (get(here)) return here; @@ -2257,8 +2163,7 @@ TypeId Normalizer::intersectionOfBools(TypeId here, TypeId there) void Normalizer::intersectExternTypes(NormalizedExternType& heres, const NormalizedExternType& theres) { - if (FFlag::LuauNormalizerStepwiseFuel) - consumeFuel(); + consumeFuel(); if (theres.isNever()) { @@ -2385,8 +2290,7 @@ void Normalizer::intersectExternTypes(NormalizedExternType& heres, const Normali void Normalizer::intersectExternTypesWithExternType(NormalizedExternType& heres, TypeId there) { - if (FFlag::LuauNormalizerStepwiseFuel) - consumeFuel(); + consumeFuel(); for (auto it = heres.ordering.begin(); it != heres.ordering.end();) { @@ -2459,8 +2363,7 @@ void Normalizer::intersectExternTypesWithExternType(NormalizedExternType& heres, void Normalizer::intersectStrings(NormalizedStringType& here, const NormalizedStringType& there) { - if (FFlag::LuauNormalizerStepwiseFuel) - consumeFuel(); + consumeFuel(); /* There are 9 cases to worry about here Normalized Left | Normalized Right @@ -2528,8 +2431,6 @@ void Normalizer::intersectStrings(NormalizedStringType& here, const NormalizedSt std::optional Normalizer::intersectionOfTypePacks(TypePackId here, TypePackId there) { - LUAU_ASSERT(FFlag::LuauNormalizerStepwiseFuel); - FuelInitializer fi{NotNull{this}}; try @@ -2544,8 +2445,7 @@ std::optional Normalizer::intersectionOfTypePacks(TypePackId here, T std::optional Normalizer::intersectionOfTypePacks_INTERNAL(TypePackId here, TypePackId there) { - if (FFlag::LuauNormalizerStepwiseFuel) - consumeFuel(); + consumeFuel(); if (here == there) return here; @@ -2668,8 +2568,7 @@ std::optional Normalizer::intersectionOfTypePacks_INTERNAL(TypePackI std::optional Normalizer::intersectionOfTables(TypeId here, TypeId there, SeenTablePropPairs& seenTablePropPairs, Set& seenSet) { - if (FFlag::LuauNormalizerStepwiseFuel) - consumeFuel(); + consumeFuel(); if (here == there) return here; @@ -2901,8 +2800,7 @@ std::optional Normalizer::intersectionOfTables(TypeId here, TypeId there void Normalizer::intersectTablesWithTable(TypeIds& heres, TypeId there, SeenTablePropPairs& seenTablePropPairs, Set& seenSetTypes) { - if (FFlag::LuauNormalizerStepwiseFuel) - consumeFuel(); + consumeFuel(); TypeIds tmp; for (TypeId here : heres) @@ -2916,8 +2814,7 @@ void Normalizer::intersectTablesWithTable(TypeIds& heres, TypeId there, SeenTabl void Normalizer::intersectTables(TypeIds& heres, const TypeIds& theres) { - if (FFlag::LuauNormalizerStepwiseFuel) - consumeFuel(); + consumeFuel(); TypeIds tmp; for (TypeId here : heres) @@ -2937,8 +2834,7 @@ void Normalizer::intersectTables(TypeIds& heres, const TypeIds& theres) std::optional Normalizer::intersectionOfFunctions(TypeId here, TypeId there) { - if (FFlag::LuauNormalizerStepwiseFuel) - consumeFuel(); + consumeFuel(); const FunctionType* hftv = get(here); LUAU_ASSERT(hftv); @@ -3074,8 +2970,7 @@ std::optional Normalizer::unionSaturatedFunctions(TypeId here, TypeId th // Proc. Principles and practice of declarative programming 2005, pp 198–208 // https://doi.org/10.1145/1069774.1069793 - if (FFlag::LuauNormalizerStepwiseFuel) - consumeFuel(); + consumeFuel(); const FunctionType* hftv = get(here); if (!hftv) @@ -3104,8 +2999,7 @@ std::optional Normalizer::unionSaturatedFunctions(TypeId here, TypeId th void Normalizer::intersectFunctionsWithFunction(NormalizedFunctionType& heres, TypeId there) { - if (FFlag::LuauNormalizerStepwiseFuel) - consumeFuel(); + consumeFuel(); if (heres.isNever()) return; @@ -3139,8 +3033,7 @@ void Normalizer::intersectFunctionsWithFunction(NormalizedFunctionType& heres, T void Normalizer::intersectFunctions(NormalizedFunctionType& heres, const NormalizedFunctionType& theres) { - if (FFlag::LuauNormalizerStepwiseFuel) - consumeFuel(); + consumeFuel(); if (heres.isNever()) return; @@ -3163,8 +3056,7 @@ NormalizationResult Normalizer::intersectTyvarsWithTy( Set& seenSetTypes ) { - if (FFlag::LuauNormalizerStepwiseFuel) - consumeFuel(); + consumeFuel(); for (auto it = here.begin(); it != here.end();) { @@ -3187,8 +3079,7 @@ NormalizationResult Normalizer::intersectNormals(NormalizedType& here, const Nor if (!withinResourceLimits()) return NormalizationResult::HitLimits; - if (FFlag::LuauNormalizerStepwiseFuel) - consumeFuel(); + consumeFuel(); if (!get(there.tops)) { @@ -3201,17 +3092,6 @@ NormalizationResult Normalizer::intersectNormals(NormalizedType& here, const Nor return unionNormals(here, there, ignoreSmallerTyvars); } - // Limit based on worst-case expansion of the table/function intersections - // This restriction can be relaxed when table intersection simplification is improved - if (!FFlag::LuauNormalizerStepwiseFuel) - { - if (here.tables.size() * there.tables.size() >= size_t(FInt::LuauNormalizeIntersectionLimit)) - return NormalizationResult::HitLimits; - - if (here.functions.parts.size() * there.functions.parts.size() >= size_t(FInt::LuauNormalizeIntersectionLimit)) - return NormalizationResult::HitLimits; - } - for (auto& [tyvar, inter] : there.tyvars) { int index = tyvarIndex(tyvar); @@ -3277,8 +3157,7 @@ NormalizationResult Normalizer::intersectNormalWithTy( if (!withinResourceLimits()) return NormalizationResult::HitLimits; - if (FFlag::LuauNormalizerStepwiseFuel) - consumeFuel(); + consumeFuel(); there = follow(there); @@ -3647,7 +3526,6 @@ TypeId Normalizer::typeFromNormal(const NormalizedType& norm) bool Normalizer::initializeFuel() { - LUAU_ASSERT(FFlag::LuauNormalizerStepwiseFuel); if (fuel) return false; @@ -3657,13 +3535,11 @@ bool Normalizer::initializeFuel() void Normalizer::clearFuel() { - LUAU_ASSERT(FFlag::LuauNormalizerStepwiseFuel); fuel = std::nullopt; } void Normalizer::consumeFuel() { - LUAU_ASSERT(FFlag::LuauNormalizerStepwiseFuel); if (fuel) { (*fuel)--; diff --git a/Analysis/src/OverloadResolution.cpp b/Analysis/src/OverloadResolution.cpp index 7573f72b5..4015b66a7 100644 --- a/Analysis/src/OverloadResolution.cpp +++ b/Analysis/src/OverloadResolution.cpp @@ -12,8 +12,7 @@ #include "Luau/TypeUtils.h" #include "Luau/Unifier2.h" -LUAU_FASTFLAG(LuauLimitUnification) -LUAU_FASTFLAG(LuauInstantiationUsesGenericPolarity) +LUAU_FASTFLAG(LuauInstantiationUsesGenericPolarity2) LUAU_FASTFLAG(LuauNewOverloadResolver2) namespace Luau @@ -1219,7 +1218,7 @@ static std::optional selectOverload( return {}; } -SolveResult solveFunctionCall( +SolveResult solveFunctionCall_DEPRECATED( NotNull arena, NotNull builtinTypes, NotNull normalizer, @@ -1246,7 +1245,7 @@ SolveResult solveFunctionCall( if (!u2.genericSubstitutions.empty() || !u2.genericPackSubstitutions.empty()) { - if (FFlag::LuauInstantiationUsesGenericPolarity) + if (FFlag::LuauInstantiationUsesGenericPolarity2) { Subtyping subtyping{builtinTypes, arena, normalizer, typeFunctionRuntime, iceReporter}; std::optional subst = instantiate2( @@ -1271,22 +1270,14 @@ SolveResult solveFunctionCall( } } - if (FFlag::LuauLimitUnification) + switch (unifyResult) { - switch (unifyResult) - { - case Luau::UnifyResult::Ok: - break; - case Luau::UnifyResult::OccursCheckFailed: - return {SolveResult::CodeTooComplex}; - case Luau::UnifyResult::TooComplex: - return {SolveResult::OccursCheckFailed}; - } - } - else - { - if (unifyResult != UnifyResult::Ok) - return {SolveResult::OccursCheckFailed}; + case Luau::UnifyResult::Ok: + break; + case Luau::UnifyResult::OccursCheckFailed: + return {SolveResult::CodeTooComplex}; + case Luau::UnifyResult::TooComplex: + return {SolveResult::OccursCheckFailed}; } SolveResult result; diff --git a/Analysis/src/Simplify.cpp b/Analysis/src/Simplify.cpp index 42bb07fd5..308478d86 100644 --- a/Analysis/src/Simplify.cpp +++ b/Analysis/src/Simplify.cpp @@ -21,8 +21,6 @@ LUAU_FASTFLAG(LuauSolverV2) LUAU_DYNAMIC_FASTINTVARIABLE(LuauSimplificationComplexityLimit, 8) LUAU_DYNAMIC_FASTINTVARIABLE(LuauTypeSimplificationIterationLimit, 128) LUAU_FASTFLAG(LuauRefineDistributesOverUnions) -LUAU_FASTFLAG(LuauPushTypeConstraint2) -LUAU_FASTFLAGVARIABLE(LuauSimplifyRefinementOfReadOnlyProperty) LUAU_FASTFLAGVARIABLE(LuauSimplifyIntersectionNoTreeSet) LUAU_FASTFLAG(LuauGetmetatableError) @@ -479,14 +477,11 @@ Relation relate(TypeId left, TypeId right, SimplifierSeenSet& seen) if (auto ut = get(left)) { - if (FFlag::LuauPushTypeConstraint2) + for (TypeId part : ut) { - for (TypeId part : ut) - { - Relation r = relate(part, right, seen); - if (r == Relation::Superset || r == Relation::Coincident) - return Relation::Superset; - } + Relation r = relate(part, right, seen); + if (r == Relation::Superset || r == Relation::Coincident) + return Relation::Superset; } return Relation::Intersects; } @@ -872,7 +867,7 @@ Inhabited intersectOneWithIntersection(TypeSimplifier& simplifier, TypeIds& sour break; case Relation::Intersects: { - // If the candidate and a member of the intersection may + // If the candidate and a member of the intersection may // intersect, then attempt to replace the member with // a simpler type, e.g.: // @@ -1076,7 +1071,7 @@ TypeId TypeSimplifier::intersectNegatedUnion(TypeId left, TypeId right) if (!changed) return right; - + return intersectFromParts(std::move(newParts)); } else @@ -1276,7 +1271,7 @@ TypeId TypeSimplifier::intersectTypeWithNegation(TypeId left, TypeId right) if (!changed) return right; - + return intersectFromParts(std::move(newParts)); } else @@ -1323,7 +1318,7 @@ TypeId TypeSimplifier::intersectTypeWithNegation(TypeId left, TypeId right) if (!changed) return right; - + return intersectFromParts_DEPRECATED(std::move(newParts)); } } @@ -1603,9 +1598,7 @@ std::optional TypeSimplifier::basicIntersect(TypeId left, TypeId right) if (1 == lt->props.size()) { const auto [propName, leftProp] = *begin(lt->props); - const bool leftPropIsRefinable = FFlag::LuauSimplifyRefinementOfReadOnlyProperty - ? leftProp.isShared() || leftProp.isReadOnly() - : leftProp.isShared(); + const bool leftPropIsRefinable = leftProp.isShared() || leftProp.isReadOnly(); auto it = rt->props.find(propName); if (it != rt->props.end() && leftPropIsRefinable && it->second.isShared()) diff --git a/Analysis/src/Subtyping.cpp b/Analysis/src/Subtyping.cpp index 0c88f5c46..6d60115d3 100644 --- a/Analysis/src/Subtyping.cpp +++ b/Analysis/src/Subtyping.cpp @@ -23,7 +23,6 @@ LUAU_DYNAMIC_FASTINTVARIABLE(LuauSubtypingRecursionLimit, 100) LUAU_FASTFLAGVARIABLE(DebugLuauSubtypingCheckPathValidity) LUAU_FASTINTVARIABLE(LuauSubtypingReasoningLimit, 100) -LUAU_FASTFLAGVARIABLE(LuauTrackUniqueness) LUAU_FASTFLAGVARIABLE(LuauIndexInMetatableSubtyping) LUAU_FASTFLAGVARIABLE(LuauSubtypingPackRecursionLimits) LUAU_FASTFLAGVARIABLE(LuauTryFindSubstitutionReturnOptional) @@ -983,13 +982,8 @@ SubtypingResult Subtyping::isCovariantWith(SubtypingEnvironment& env, TypeId sub result = isCovariantWith(env, p, scope); else if (auto p = get2(subTy, superTy)) { - if (FFlag::LuauTrackUniqueness) - { - const bool forceCovariantTest = uniqueTypes != nullptr && uniqueTypes->contains(subTy); - result = isCovariantWith(env, p.first, p.second, forceCovariantTest, scope); - } - else - result = isCovariantWith(env, p, scope); + const bool forceCovariantTest = uniqueTypes != nullptr && uniqueTypes->contains(subTy); + result = isCovariantWith(env, p.first, p.second, forceCovariantTest, scope); } else if (auto p = get2(subTy, superTy)) result = isCovariantWith(env, p, scope); @@ -1874,13 +1868,6 @@ SubtypingResult Subtyping::isCovariantWith( return {*subSingleton == *superSingleton}; } -// Compatibility shim for the unflagged codepath of FFlag::LuauTrackUniqueness -// TODO: Delete this when clipping that flag. -SubtypingResult Subtyping::isCovariantWith(SubtypingEnvironment& env, const TableType* subTable, const TableType* superTable, NotNull scope) -{ - return isCovariantWith(env, subTable, superTable, /*forceCovariantTest*/ false, scope); -} - SubtypingResult Subtyping::isCovariantWith( SubtypingEnvironment& env, const TableType* subTable, @@ -2305,7 +2292,7 @@ SubtypingResult Subtyping::isCovariantWith( if (superProp.isShared() && subProp.isShared()) { - if (FFlag::LuauTrackUniqueness && forceCovariantTest) + if (forceCovariantTest) res.andAlso(isCovariantWith(env, *subProp.readTy, *superProp.readTy, scope).withBothComponent(TypePath::Property::read(name))); else res.andAlso(isInvariantWith(env, *subProp.readTy, *superProp.readTy, scope).withBothComponent(TypePath::Property::read(name))); @@ -2314,16 +2301,8 @@ SubtypingResult Subtyping::isCovariantWith( { if (superProp.readTy.has_value() && subProp.readTy.has_value()) res.andAlso(isCovariantWith(env, *subProp.readTy, *superProp.readTy, scope).withBothComponent(TypePath::Property::read(name))); - if (FFlag::LuauTrackUniqueness) - { - if (superProp.writeTy.has_value() && subProp.writeTy.has_value() && !forceCovariantTest) - res.andAlso(isContravariantWith(env, *subProp.writeTy, *superProp.writeTy, scope).withBothComponent(TypePath::Property::write(name))); - } - else - { - if (superProp.writeTy.has_value() && subProp.writeTy.has_value()) - res.andAlso(isContravariantWith(env, *subProp.writeTy, *superProp.writeTy, scope).withBothComponent(TypePath::Property::write(name))); - } + if (superProp.writeTy.has_value() && subProp.writeTy.has_value() && !forceCovariantTest) + res.andAlso(isContravariantWith(env, *subProp.writeTy, *superProp.writeTy, scope).withBothComponent(TypePath::Property::write(name))); if (superProp.isReadWrite()) { diff --git a/Analysis/src/TableLiteralInference.cpp b/Analysis/src/TableLiteralInference.cpp index 84cbde50d..0a5462d64 100644 --- a/Analysis/src/TableLiteralInference.cpp +++ b/Analysis/src/TableLiteralInference.cpp @@ -10,14 +10,13 @@ #include "Luau/Subtyping.h" #include "Luau/Type.h" #include "Luau/ToString.h" -#include "Luau/TypeArena.h" #include "Luau/TypeUtils.h" #include "Luau/Unifier2.h" LUAU_FASTFLAGVARIABLE(LuauPushTypeConstraintIntersection) -LUAU_FASTFLAGVARIABLE(LuauPushTypeConstraintSingleton) LUAU_FASTFLAGVARIABLE(LuauPushTypeConstraintIndexer) LUAU_FASTFLAGVARIABLE(LuauPushTypeConstraintLambdas2) +LUAU_FASTFLAGVARIABLE(LuauPushTypeConstraintStripNilFromFunction) namespace Luau { @@ -138,7 +137,7 @@ struct BidirectionalTypePusher if (ft && get(ft->lowerBound) && fastIsSubtype(solver->builtinTypes->stringType, ft->upperBound) && fastIsSubtype(ft->lowerBound, solver->builtinTypes->stringType)) { - if (FFlag::LuauPushTypeConstraintSingleton && maybeSingleton(expectedType) && maybeSingleton(ft->lowerBound)) + if (maybeSingleton(expectedType) && maybeSingleton(ft->lowerBound)) { // If we see a pattern like: // @@ -261,7 +260,9 @@ struct BidirectionalTypePusher if (auto exprLambda = expr->as()) { const auto lambdaTy = get(exprType); - const auto expectedLambdaTy = get(expectedType); + const auto expectedLambdaTy = FFlag::LuauPushTypeConstraintStripNilFromFunction + ? get(stripNil(solver->builtinTypes, *solver->arena, expectedType)) + : get(expectedType); if (lambdaTy && expectedLambdaTy) { const auto& [lambdaArgTys, _lambdaTail] = flatten(lambdaTy->argTypes); diff --git a/Analysis/src/TypeChecker2.cpp b/Analysis/src/TypeChecker2.cpp index 6db31e600..242ad0665 100644 --- a/Analysis/src/TypeChecker2.cpp +++ b/Analysis/src/TypeChecker2.cpp @@ -35,16 +35,17 @@ LUAU_FASTFLAG(DebugLuauMagicTypes) LUAU_FASTFLAG(LuauExplicitTypeExpressionInstantiation) -LUAU_FASTFLAG(LuauNoMoreComparisonTypeFunctions) -LUAU_FASTFLAG(LuauTrackUniqueness) LUAU_FASTFLAGVARIABLE(LuauIceLess) LUAU_FASTFLAGVARIABLE(LuauCheckForInWithSubtyping3) LUAU_FASTFLAGVARIABLE(LuauNewOverloadResolver2) +LUAU_FASTFLAGVARIABLE(LuauHandleFunctionOversaturation) LUAU_FASTFLAG(LuauSimplifyIntersectionNoTreeSet) LUAU_FASTFLAG(LuauAddRefinementToAssertions) LUAU_FASTFLAGVARIABLE(LuauSuppressIndexingIntoError) +LUAU_FASTFLAG(LuauBetterTypeMismatchErrors) LUAU_FASTFLAGVARIABLE(LuauFixIndexingUnionWithNonTable) +LUAU_FASTFLAGVARIABLE(LuauCheckFunctionStatementTypes) namespace Luau { @@ -1292,6 +1293,24 @@ void TypeChecker2::visit(AstStatFunction* stat) { visit(stat->name, ValueContext::LValue); visit(stat->func); + + if (FFlag::LuauCheckFunctionStatementTypes) + { + // Consider a block of code like: + // + // type X = { x: (number) -> number } + // function f(t: X) + // function t.x(a: string): string + // return "Hello, " .. a + // end + // end + // + // We need to check that the function we're assigning to `t.x` has the + // correct type, like when we're assigning an expression to a local + auto lhsType = lookupType(stat->name); + auto rhsType = lookupType(stat->func); + testIsSubtype(rhsType, lhsType, stat->func->location); + } } void TypeChecker2::visit(AstStatLocalFunction* stat) @@ -1619,47 +1638,79 @@ void TypeChecker2::visitCall(AstExprCall* call) { size_t selfOffset = call->self ? 1 : 0; - auto [paramsHead, _] = extendTypePack(module->internalTypes, builtinTypes, fty->argTypes, call->args.size + selfOffset); + std::vector paramsHead = extendTypePack(module->internalTypes, builtinTypes, fty->argTypes, call->args.size + selfOffset).head; - for (size_t idx = 0; idx < call->args.size - 1; ++idx) + if (FFlag::LuauHandleFunctionOversaturation) { - auto argExpr = call->args.data[idx]; - auto argExprType = lookupType(argExpr); - argExprs.push_back(argExpr); - if (idx + selfOffset >= paramsHead.size() || isErrorSuppressing(argExpr->location, argExprType)) + for (size_t idx = 0; idx < call->args.size; ++idx) { - args.head.push_back(argExprType); - continue; - } - testLiteralOrAstTypeIsSubtype(argExpr, paramsHead[idx + selfOffset]); - args.head.push_back(paramsHead[idx + selfOffset]); - } + AstExpr* argExpr = call->args.data[idx]; - auto lastExpr = call->args.data[call->args.size - 1]; - argExprs.push_back(lastExpr); + // The last argument might be an ordinary value, but it can also be an entire pack. + if (idx == call->args.size - 1) + { + if (TypePackId* lastArgPack = module->astTypePacks.find(argExpr)) + { + auto [lastArgHead, lastArgTail] = flatten(*lastArgPack); + args.head.insert(args.head.end(), lastArgHead.begin(), lastArgHead.end()); + args.tail = lastArgTail; + continue; + } + } - if (auto argTail = module->astTypePacks.find(lastExpr)) - { - auto [lastExprHead, lastExprTail] = flatten(*argTail); - args.head.insert(args.head.end(), lastExprHead.begin(), lastExprHead.end()); - args.tail = lastExprTail; + TypeId argExprType = lookupType(argExpr); + argExprs.push_back(argExpr); + if (idx + selfOffset >= paramsHead.size() || isErrorSuppressing(argExpr->location, argExprType)) + args.head.push_back(argExprType); + else + { + testLiteralOrAstTypeIsSubtype(argExpr, paramsHead[idx + selfOffset]); + args.head.push_back(paramsHead[idx + selfOffset]); + } + } } - else if (paramsHead.size() >= call->args.size + selfOffset) + else { - auto lastType = paramsHead[call->args.size - 1 + selfOffset]; - auto lastExprType = lookupType(lastExpr); - if (isErrorSuppressing(lastExpr->location, lastExprType)) + for (size_t idx = 0; idx < call->args.size - 1; ++idx) { - args.head.push_back(lastExprType); + auto argExpr = call->args.data[idx]; + auto argExprType = lookupType(argExpr); + argExprs.push_back(argExpr); + if (idx + selfOffset >= paramsHead.size() || isErrorSuppressing(argExpr->location, argExprType)) + { + args.head.push_back(argExprType); + continue; + } + testLiteralOrAstTypeIsSubtype(argExpr, paramsHead[idx + selfOffset]); + args.head.push_back(paramsHead[idx + selfOffset]); } - else + + auto lastExpr = call->args.data[call->args.size - 1]; + argExprs.push_back(lastExpr); + + if (auto argTail = module->astTypePacks.find(lastExpr)) { - testLiteralOrAstTypeIsSubtype(lastExpr, lastType); - args.head.push_back(lastType); + auto [lastExprHead, lastExprTail] = flatten(*argTail); + args.head.insert(args.head.end(), lastExprHead.begin(), lastExprHead.end()); + args.tail = lastExprTail; + } + else if (paramsHead.size() >= call->args.size + selfOffset) + { + auto lastType = paramsHead[call->args.size - 1 + selfOffset]; + auto lastExprType = lookupType(lastExpr); + if (isErrorSuppressing(lastExpr->location, lastExprType)) + { + args.head.push_back(lastExprType); + } + else + { + testLiteralOrAstTypeIsSubtype(lastExpr, lastType); + args.head.push_back(lastType); + } } + else + args.tail = builtinTypes->anyTypePack; } - else - args.tail = builtinTypes->anyTypePack; } else { @@ -1708,8 +1759,7 @@ void TypeChecker2::visitCall(AstExprCall* call) call->location, }; DenseHashSet uniqueTypes{nullptr}; - if (FFlag::LuauTrackUniqueness) - findUniqueTypes(NotNull{&uniqueTypes}, argExprs, NotNull{&module->astTypes}); + findUniqueTypes(NotNull{&uniqueTypes}, argExprs, NotNull{&module->astTypes}); if (FFlag::LuauNewOverloadResolver2) { @@ -2445,16 +2495,13 @@ TypeId TypeChecker2::visit(AstExprBinary* expr, AstNode* overrideKey) } NormalizationResult typesHaveIntersection = normalizer.isIntersectionInhabited(leftType, rightType); - if (FFlag::LuauNoMoreComparisonTypeFunctions) + if (isEquality || isComparison) { - if (isEquality || isComparison) + // As a special exception, we allow anything to be compared to nil. + if (!isOkToCompare(normalizer, typesHaveIntersection, normLeft, normRight)) { - // As a special exception, we allow anything to be compared to nil. - if (!isOkToCompare(normalizer, typesHaveIntersection, normLeft, normRight)) - { - reportError(CannotCompareUnrelatedTypes{leftType, rightType, expr->op}, expr->location); - return builtinTypes->errorType; - } + reportError(CannotCompareUnrelatedTypes{leftType, rightType, expr->op}, expr->location); + return builtinTypes->errorType; } } @@ -3183,7 +3230,10 @@ Reasonings TypeChecker2::explainReasonings_(TID subTy, TID superTy, Location loc std::stringstream reason; - if (reasoning.subPath == reasoning.superPath) + if (FFlag::LuauBetterTypeMismatchErrors && reasoning.subPath == reasoning.superPath) + reason << toStringHuman(reasoning.subPath) << "`" << subLeafAsString << "` in the latter type and `" << superLeafAsString + << "` in the former type, and " << baseReason; + else if (reasoning.subPath == reasoning.superPath) reason << toStringHuman(reasoning.subPath) << "`" << subLeafAsString << "` in the former type and `" << superLeafAsString << "` in the latter type, and " << baseReason; else if (!reasoning.subPath.empty() && !reasoning.superPath.empty()) diff --git a/Analysis/src/TypeFunctionRuntime.cpp b/Analysis/src/TypeFunctionRuntime.cpp index 96aa8a473..178509637 100644 --- a/Analysis/src/TypeFunctionRuntime.cpp +++ b/Analysis/src/TypeFunctionRuntime.cpp @@ -3,6 +3,7 @@ #include "Luau/TypeFunctionRuntime.h" #include "Luau/Allocator.h" +#include "Luau/Common.h" #include "Luau/Lexer.h" #include "Luau/BuiltinTypeFunctions.h" #include "Luau/BytecodeBuilder.h" @@ -10,6 +11,7 @@ #include "Luau/Compiler.h" #include "Luau/DenseHash.h" #include "Luau/StringUtils.h" +#include "Luau/Type.h" #include "Luau/TypeFunction.h" #include "Luau/TypeFunctionRuntimeBuilder.h" @@ -22,6 +24,8 @@ LUAU_DYNAMIC_FASTINT(LuauTypeFunctionSerdeIterationLimit) +LUAU_FASTFLAGVARIABLE(LuauUnionofIntersectionofFlattens) + namespace Luau { @@ -213,6 +217,16 @@ TypeFunctionTypePackVar* allocateTypeFunctionTypePack(lua_State* L, TypeFunction return ctx->typePackArena.allocate(std::move(type)); } +void pushType(lua_State* L, TypeFunctionTypeId type) +{ + TypeFunctionTypeId* ptr = static_cast(lua_newuserdatatagged(L, sizeof(TypeFunctionTypeId), kTypeUserdataTag)); + *ptr = type; + + // set the new userdata's metatable to type metatable + luaL_getmetatable(L, "type"); + lua_setmetatable(L, -2); +} + // Pushes a new type userdata onto the stack void allocTypeUserData(lua_State* L, TypeFunctionTypeVariant type) { @@ -484,16 +498,42 @@ static int createUnion(lua_State* L) { // get the number of arguments for union int argSize = lua_gettop(L); - if (argSize < 2) + if (!FFlag::LuauUnionofIntersectionofFlattens && argSize < 2) luaL_error(L, "types.unionof: expected at least 2 types to union, but got %d", argSize); std::vector components; components.reserve(argSize); for (int i = 1; i <= argSize; i++) - components.push_back(getTypeUserData(L, i)); + { + if (FFlag::LuauUnionofIntersectionofFlattens) + { + TypeFunctionTypeId component = getTypeUserData(L, i); - allocTypeUserData(L, TypeFunctionUnionType{std::move(components)}); + if (auto unionComponent = get(component)) + components.insert(components.end(), unionComponent->components.begin(), unionComponent->components.end()); + else if (get(component)) + continue; + else + components.push_back(component); + } + else + { + components.push_back(getTypeUserData(L, i)); + } + } + + if (FFlag::LuauUnionofIntersectionofFlattens) + { + if (components.size() == 0) + allocTypeUserData(L, TypeFunctionNeverType{}); + else if (components.size() == 1) + pushType(L, components[0]); + else + allocTypeUserData(L, TypeFunctionUnionType{std::move(components)}); + } + else + allocTypeUserData(L, TypeFunctionUnionType{std::move(components)}); return 1; } @@ -504,16 +544,42 @@ static int createIntersection(lua_State* L) { // get the number of arguments for intersection int argSize = lua_gettop(L); - if (argSize < 2) + if (!FFlag::LuauUnionofIntersectionofFlattens && argSize < 2) luaL_error(L, "types.intersectionof: expected at least 2 types to intersection, but got %d", argSize); std::vector components; components.reserve(argSize); for (int i = 1; i <= argSize; i++) - components.push_back(getTypeUserData(L, i)); + { + if (FFlag::LuauUnionofIntersectionofFlattens) + { + TypeFunctionTypeId component = getTypeUserData(L, i); - allocTypeUserData(L, TypeFunctionIntersectionType{std::move(components)}); + if (auto intersectionComponent = get(component)) + components.insert(components.end(), intersectionComponent->components.begin(), intersectionComponent->components.end()); + else if (get(component)) + continue; + else + components.push_back(component); + } + else + { + components.push_back(getTypeUserData(L, i)); + } + } + + if (FFlag::LuauUnionofIntersectionofFlattens) + { + if (components.size() == 0) + allocTypeUserData(L, TypeFunctionUnknownType{}); + else if (components.size() == 1) + pushType(L, components[0]); + else + allocTypeUserData(L, TypeFunctionIntersectionType{std::move(components)}); + } + else + allocTypeUserData(L, TypeFunctionIntersectionType{std::move(components)}); return 1; } diff --git a/Analysis/src/TypeFunctionRuntimeBuilder.cpp b/Analysis/src/TypeFunctionRuntimeBuilder.cpp index c6461d676..1bb6a7253 100644 --- a/Analysis/src/TypeFunctionRuntimeBuilder.cpp +++ b/Analysis/src/TypeFunctionRuntimeBuilder.cpp @@ -20,6 +20,8 @@ // currently, controls serialization, deserialization, and `type.copy` LUAU_DYNAMIC_FASTINTVARIABLE(LuauTypeFunctionSerdeIterationLimit, 100'000); +LUAU_FASTFLAGVARIABLE(LuauTypeFunctionDeserializationShouldNotCrashOnGenericPacks) + namespace Luau { @@ -931,7 +933,13 @@ class TypeFunctionDeserializer for (auto ty : f2->generics) { auto gty = get(ty); - LUAU_ASSERT(gty && !gty->isPack); + if (FFlag::LuauTypeFunctionDeserializationShouldNotCrashOnGenericPacks && (!gty || gty->isPack)) + { + state->errors.emplace_back("Encountered unexpected generic"); + return; + } + else + LUAU_ASSERT(gty && !gty->isPack); std::pair nameKey = std::make_pair(gty->isNamed, gty->name); @@ -951,7 +959,13 @@ class TypeFunctionDeserializer for (auto tp : f2->genericPacks) { auto gtp = get(tp); - LUAU_ASSERT(gtp); + if (FFlag::LuauTypeFunctionDeserializationShouldNotCrashOnGenericPacks && !gtp) + { + state->errors.emplace_back("Encountered unexpected generic type pack"); + return; + } + else + LUAU_ASSERT(gtp); std::pair nameKey = std::make_pair(gtp->isNamed, gtp->name); diff --git a/Analysis/src/TypePath.cpp b/Analysis/src/TypePath.cpp index 43a9bbe40..7ae01d5e8 100644 --- a/Analysis/src/TypePath.cpp +++ b/Analysis/src/TypePath.cpp @@ -17,7 +17,6 @@ #include LUAU_FASTFLAG(LuauSolverV2); -LUAU_FASTFLAGVARIABLE(LuauConsiderErrorSuppressionInTypes) LUAU_FASTFLAG(LuauNewOverloadResolver2) LUAU_FASTFLAG(LuauNewNonStrictBetterCheckedFunctionErrorMessage) @@ -406,7 +405,7 @@ struct TraversalState { bool updatedCurrent = false; - if (FFlag::LuauConsiderErrorSuppressionInTypes && get(*currentType)) + if (get(*currentType)) { encounteredErrorSuppression = true; return false; @@ -415,66 +414,39 @@ struct TraversalState if (auto u = get(*currentType)) { auto it = begin(u); - if (FFlag::LuauConsiderErrorSuppressionInTypes) + // We want to track the index that updates the current type with `idx` while still iterating through the entire union to check for error types with `it`. + size_t idx = 0; + for (auto it = begin(u); it != end(u); ++it) { - // We want to track the index that updates the current type with `idx` while still iterating through the entire union to check for error types with `it`. - size_t idx = 0; - for (auto it = begin(u); it != end(u); ++it) - { - if (get(*it)) - encounteredErrorSuppression = true; - if (idx == index.index) - { - updateCurrent(*it); - updatedCurrent = true; - } - ++idx; - } - } - else - { - std::advance(it, index.index); - - if (it != end(u)) + if (get(*it)) + encounteredErrorSuppression = true; + if (idx == index.index) { updateCurrent(*it); - return true; + updatedCurrent = true; } + ++idx; } } else if (auto i = get(*currentType)) { auto it = begin(i); - if (FFlag::LuauConsiderErrorSuppressionInTypes) + // We want to track the index that updates the current type with `idx` while still iterating through the entire intersection to check for error types with `it`. + size_t idx = 0; + for (auto it = begin(i); it != end(i); ++it) { - // We want to track the index that updates the current type with `idx` while still iterating through the entire intersection to check for error types with `it`. - size_t idx = 0; - for (auto it = begin(i); it != end(i); ++it) - { - if (get(*it)) - encounteredErrorSuppression = true; - if (idx == index.index) - { - updateCurrent(*it); - updatedCurrent = true; - } - ++idx; - } - } - else - { - std::advance(it, index.index); - - if (it != end(i)) + if (get(*it)) + encounteredErrorSuppression = true; + if (idx == index.index) { updateCurrent(*it); - return true; + updatedCurrent = true; } + ++idx; } } - if (FFlag::LuauConsiderErrorSuppressionInTypes) - return updatedCurrent; + return updatedCurrent; } else { @@ -1075,7 +1047,7 @@ std::optional traverseForType(const TypeId root, const Path& path, const TraversalState state(follow(root), builtinTypes, arena); if (traverse(state, path)) { - if (FFlag::LuauConsiderErrorSuppressionInTypes && state.encounteredErrorSuppression) + if (state.encounteredErrorSuppression) return builtinTypes->errorType; const TypeId* ty = get(state.current); @@ -1095,7 +1067,7 @@ std::optional traverseForType( TraversalState state(follow(root), builtinTypes, arena); if (traverse(state, path)) { - if (FFlag::LuauConsiderErrorSuppressionInTypes && state.encounteredErrorSuppression) + if (state.encounteredErrorSuppression) return builtinTypes->errorType; const TypeId* ty = get(state.current); return ty ? std::make_optional(*ty) : std::nullopt; @@ -1114,7 +1086,7 @@ std::optional traverseForPack( TraversalState state(follow(root), builtinTypes, arena); if (traverse(state, path)) { - if (FFlag::LuauConsiderErrorSuppressionInTypes && state.encounteredErrorSuppression) + if (state.encounteredErrorSuppression) return builtinTypes->errorTypePack; const TypePackId* ty = get(state.current); return ty ? std::make_optional(*ty) : std::nullopt; @@ -1133,7 +1105,7 @@ std::optional traverseForPack( TraversalState state(follow(root), builtinTypes, arena); if (traverse(state, path)) { - if (FFlag::LuauConsiderErrorSuppressionInTypes && state.encounteredErrorSuppression) + if (state.encounteredErrorSuppression) return builtinTypes->errorTypePack; const TypePackId* ty = get(state.current); return ty ? std::make_optional(*ty) : std::nullopt; diff --git a/Analysis/src/TypeUtils.cpp b/Analysis/src/TypeUtils.cpp index ba8fecbd1..c7285dcc3 100644 --- a/Analysis/src/TypeUtils.cpp +++ b/Analysis/src/TypeUtils.cpp @@ -13,8 +13,6 @@ #include LUAU_FASTFLAG(LuauSolverV2) -LUAU_FASTFLAGVARIABLE(LuauTidyTypeUtils) -LUAU_FASTFLAG(LuauPushTypeConstraint2) namespace Luau { @@ -136,15 +134,10 @@ std::optional findMetatableEntry( auto it = mtt->props.find(entry); if (it != mtt->props.end()) { - if (FFlag::LuauTidyTypeUtils || FFlag::LuauSolverV2) - { - if (it->second.readTy) - return it->second.readTy; - else - return it->second.writeTy; - } + if (it->second.readTy) + return it->second.readTy; else - return it->second.type_DEPRECATED(); + return it->second.writeTy; } else return std::nullopt; @@ -178,18 +171,13 @@ std::optional findTablePropertyRespectingMeta( const auto& it = tableType->props.find(name); if (it != tableType->props.end()) { - if (FFlag::LuauTidyTypeUtils || FFlag::LuauSolverV2) + switch (context) { - switch (context) - { - case ValueContext::RValue: - return it->second.readTy; - case ValueContext::LValue: - return it->second.writeTy; - } + case ValueContext::RValue: + return it->second.readTy; + case ValueContext::LValue: + return it->second.writeTy; } - else - return it->second.type_DEPRECATED(); } } @@ -340,11 +328,9 @@ TypePack extendTypePack( TypePack newPack; newPack.tail = arena.freshTypePack(ftp->scope, ftp->polarity); - if (FFlag::LuauTidyTypeUtils) - trackInteriorFreeTypePack(ftp->scope, *newPack.tail); + trackInteriorFreeTypePack(ftp->scope, *newPack.tail); - if (FFlag::LuauTidyTypeUtils || FFlag::LuauSolverV2) - result.tail = newPack.tail; + result.tail = newPack.tail; size_t overridesIndex = 0; while (result.head.size() < length) { @@ -355,14 +341,9 @@ TypePack extendTypePack( } else { - if (FFlag::LuauTidyTypeUtils || FFlag::LuauSolverV2) - { - FreeType ft{ftp->scope, builtinTypes->neverType, builtinTypes->unknownType, ftp->polarity}; - t = arena.addType(ft); - trackInteriorFreeType(ftp->scope, t); - } - else - t = arena.freshType(builtinTypes, ftp->scope); + FreeType ft{ftp->scope, builtinTypes->neverType, builtinTypes->unknownType, ftp->polarity}; + t = arena.addType(ft); + trackInteriorFreeType(ftp->scope, t); } newPack.head.push_back(t); @@ -702,7 +683,7 @@ std::optional extractMatchingTableType(std::vector& tables, Type } } - if (FFlag::LuauPushTypeConstraint2 && fastIsSubtype(propType, expectedType)) + if (fastIsSubtype(propType, expectedType)) return ty; } } diff --git a/Analysis/src/Unifier.cpp b/Analysis/src/Unifier.cpp index 41d859406..24744039b 100644 --- a/Analysis/src/Unifier.cpp +++ b/Analysis/src/Unifier.cpp @@ -6,7 +6,6 @@ #include "Luau/RecursionCounter.h" #include "Luau/Scope.h" #include "Luau/StringUtils.h" -#include "Luau/TimeTrace.h" #include "Luau/ToString.h" #include "Luau/TypePack.h" #include "Luau/TypeUtils.h" @@ -21,8 +20,8 @@ LUAU_FASTFLAGVARIABLE(LuauInstantiateInSubtyping) LUAU_FASTFLAGVARIABLE(LuauTransitiveSubtyping) LUAU_FASTFLAG(LuauSolverV2) LUAU_FASTFLAGVARIABLE(LuauFixIndexerSubtypingOrdering) +LUAU_FASTFLAG(LuauBetterTypeMismatchErrors) LUAU_FASTFLAGVARIABLE(LuauUnifierRecursionOnRestart) -LUAU_FASTFLAG(LuauNormalizerStepwiseFuel) namespace Luau { @@ -1190,26 +1189,13 @@ TypePackId Unifier::tryApplyOverloadedFunction(TypeId function, const Normalized { innerState->log.clear(); innerState->tryUnify_(*result, ftv->retTypes); - if (FFlag::LuauNormalizerStepwiseFuel) - { - if (innerState->errors.empty()) - log.concat(std::move(innerState->log)); - // Annoyingly, since we don't support intersection of generic type packs, - // the intersection may fail. We rather arbitrarily use the first matching overload - // in that case. - else if (std::optional intersect = normalizer->intersectionOfTypePacks(*result, ftv->retTypes)) - result = intersect; - } - else - { - if (innerState->errors.empty()) - log.concat(std::move(innerState->log)); - // Annoyingly, since we don't support intersection of generic type packs, - // the intersection may fail. We rather arbitrarily use the first matching overload - // in that case. - else if (std::optional intersect = normalizer->intersectionOfTypePacks_INTERNAL(*result, ftv->retTypes)) - result = intersect; - } + if (innerState->errors.empty()) + log.concat(std::move(innerState->log)); + // Annoyingly, since we don't support intersection of generic type packs, + // the intersection may fail. We rather arbitrarily use the first matching overload + // in that case. + else if (std::optional intersect = normalizer->intersectionOfTypePacks(*result, ftv->retTypes)) + result = intersect; } else result = ftv->retTypes; @@ -2209,7 +2195,8 @@ void Unifier::tryUnifyScalarShape(TypeId subTy, TypeId superTy, bool reversed) auto fail = [&](std::optional e) { - std::string reason = "The former's metatable does not satisfy the requirements."; + std::string reason = FFlag::LuauBetterTypeMismatchErrors ? "The given type's metatable does not satisfy the requirements." + : "The former's metatable does not satisfy the requirements."; if (e) reportError(location, TypeMismatch{osuperTy, osubTy, std::move(reason), std::move(e), mismatchContext()}); else diff --git a/Analysis/src/Unifier2.cpp b/Analysis/src/Unifier2.cpp index d3725ffad..4c34a7c0c 100644 --- a/Analysis/src/Unifier2.cpp +++ b/Analysis/src/Unifier2.cpp @@ -23,7 +23,6 @@ LUAU_FASTINT(LuauTypeInferRecursionLimit) LUAU_FASTFLAG(LuauIndividualRecursionLimits) LUAU_DYNAMIC_FASTINTVARIABLE(LuauUnifierRecursionLimit, 100) -LUAU_FASTFLAGVARIABLE(LuauLimitUnification) LUAU_FASTFLAGVARIABLE(LuauLimitUnificationRecursion) namespace Luau @@ -147,13 +146,10 @@ UnifyResult Unifier2::unify(TypePackId subTp, TypePackId superTp) UnifyResult Unifier2::unify_(TypeId subTy, TypeId superTy) { - if (FFlag::LuauLimitUnification) - { - if (FInt::LuauTypeInferIterationLimit > 0 && iterationCount >= FInt::LuauTypeInferIterationLimit) - return UnifyResult::TooComplex; + if (FInt::LuauTypeInferIterationLimit > 0 && iterationCount >= FInt::LuauTypeInferIterationLimit) + return UnifyResult::TooComplex; - ++iterationCount; - } + ++iterationCount; // NOTE: It's a little odd that we are doing something non-exceptional for // the core of unification but not for occurs check, which may throw an @@ -624,13 +620,10 @@ UnifyResult Unifier2::unify_(const AnyType*, const MetatableType* superMetatable // rather than a boolean to signal an occurs check failure. UnifyResult Unifier2::unify_(TypePackId subTp, TypePackId superTp) { - if (FFlag::LuauLimitUnification) - { - if (FInt::LuauTypeInferIterationLimit > 0 && iterationCount >= FInt::LuauTypeInferIterationLimit) - return UnifyResult::TooComplex; + if (FInt::LuauTypeInferIterationLimit > 0 && iterationCount >= FInt::LuauTypeInferIterationLimit) + return UnifyResult::TooComplex; - ++iterationCount; - } + ++iterationCount; // NOTE: It's a little odd that we are doing something non-exceptional for // the core of unification but not for occurs check, which may throw an diff --git a/Ast/src/Parser.cpp b/Ast/src/Parser.cpp index b9f3102a7..9dc852c0f 100644 --- a/Ast/src/Parser.cpp +++ b/Ast/src/Parser.cpp @@ -21,7 +21,6 @@ LUAU_FASTFLAGVARIABLE(LuauSolverV2) LUAU_DYNAMIC_FASTFLAGVARIABLE(DebugLuauReportReturnTypeVariadicWithTypeSuffix, false) LUAU_FASTFLAGVARIABLE(DebugLuauStringSingletonBasedOnQuotes) LUAU_FASTFLAGVARIABLE(LuauExplicitTypeExpressionInstantiation) -LUAU_FASTFLAGVARIABLE(LuauAutocompleteAttributes) LUAU_FASTFLAG(LuauStandaloneParseType) LUAU_FASTFLAGVARIABLE(LuauCstStatDoWithStatsStart) @@ -951,15 +950,7 @@ void Parser::parseAttribute(TempVector& attributes) nextLexeme(); - if (FFlag::LuauAutocompleteAttributes) - { - attributes.push_back(allocator.alloc(loc, type.value_or(AstAttr::Type::Unknown), empty, AstName(name))); - } - else - { - if (type) - attributes.push_back(allocator.alloc(loc, *type, empty)); - } + attributes.push_back(allocator.alloc(loc, type.value_or(AstAttr::Type::Unknown), empty, AstName(name))); } else { @@ -989,30 +980,14 @@ void Parser::parseAttribute(TempVector& attributes) std::optional type = validateAttribute(nameLoc, attrName, attributes, args); - if (FFlag::LuauAutocompleteAttributes) - { - attributes.push_back( - allocator.alloc(Location(nameLoc, argsLocation), type.value_or(AstAttr::Type::Unknown), args, AstName(attrName)) - ); - } - else - { - if (type) - attributes.push_back(allocator.alloc(Location(nameLoc, argsLocation), *type, args)); - } + attributes.push_back( + allocator.alloc(Location(nameLoc, argsLocation), type.value_or(AstAttr::Type::Unknown), args, AstName(attrName)) + ); } else { std::optional type = validateAttribute(nameLoc, attrName, attributes, empty); - if (FFlag::LuauAutocompleteAttributes) - { - attributes.push_back(allocator.alloc(nameLoc, type.value_or(AstAttr::Type::Unknown), empty, AstName(attrName))); - } - else - { - if (type) - attributes.push_back(allocator.alloc(nameLoc, *type, empty)); - } + attributes.push_back(allocator.alloc(nameLoc, type.value_or(AstAttr::Type::Unknown), empty, AstName(attrName))); } if (lexer.current().type == ',') @@ -1029,13 +1004,10 @@ void Parser::parseAttribute(TempVector& attributes) { report(Location(open.location, lexer.current().location), "Attribute list cannot be empty"); - if (FFlag::LuauAutocompleteAttributes) - { - // autocomplete expects at least one unknown attribute. - attributes.push_back( - allocator.alloc(Location(open.location, lexer.current().location), AstAttr::Type::Unknown, empty, nameError) - ); - } + // autocomplete expects at least one unknown attribute. + attributes.push_back( + allocator.alloc(Location(open.location, lexer.current().location), AstAttr::Type::Unknown, empty, nameError) + ); } expectMatchAndConsume(']', open); diff --git a/CodeGen/include/Luau/AssemblyBuilderA64.h b/CodeGen/include/Luau/AssemblyBuilderA64.h index 411eb09dc..a5d6ef037 100644 --- a/CodeGen/include/Luau/AssemblyBuilderA64.h +++ b/CodeGen/include/Luau/AssemblyBuilderA64.h @@ -121,15 +121,23 @@ class AssemblyBuilderA64 // Address of embedded data void adr(RegisterA64 dst, const void* ptr, size_t size); void adr(RegisterA64 dst, uint64_t value); + void adr(RegisterA64 dst, float value); void adr(RegisterA64 dst, double value); + template + void adr(RegisterA64 dst, T value) = delete; // Prevent implicit conversions from happening + // Address of code (label) void adr(RegisterA64 dst, Label& label); // Floating-point scalar/vector moves - // Note: constant must be compatible with immediate floating point moves (see isFmovSupported) + // Note: constant must be compatible with immediate floating point moves (see isFmovSupportedFp64/isFmovSupportedFp32) void fmov(RegisterA64 dst, RegisterA64 src); void fmov(RegisterA64 dst, double src); + void fmov(RegisterA64 dst, float src); + + template + void fmov(RegisterA64 dst, T src) = delete; // Prevent implicit conversions from happening // Floating-point scalar/vector math void fabs(RegisterA64 dst, RegisterA64 src); @@ -146,6 +154,7 @@ class AssemblyBuilderA64 void ins_4s(RegisterA64 dst, RegisterA64 src, uint8_t index); void ins_4s(RegisterA64 dst, uint8_t dstIndex, RegisterA64 src, uint8_t srcIndex); void dup_4s(RegisterA64 dst, RegisterA64 src, uint8_t index); + void umov_4s(RegisterA64 dst, RegisterA64 src, uint8_t index); void fcmeq_4s(RegisterA64 dst, RegisterA64 src1, RegisterA64 src2); void bit(RegisterA64 dst, RegisterA64 src, RegisterA64 mask); @@ -210,7 +219,8 @@ class AssemblyBuilderA64 static bool isMaskSupported(uint32_t mask); // Check if fmov can be used to synthesize a constant - static bool isFmovSupported(double value); + static bool isFmovSupportedFp64(double value); + static bool isFmovSupportedFp32(float value); private: // Instruction archetypes diff --git a/CodeGen/include/Luau/AssemblyBuilderX64.h b/CodeGen/include/Luau/AssemblyBuilderX64.h index 4f8c9181f..a0bfac1f5 100644 --- a/CodeGen/include/Luau/AssemblyBuilderX64.h +++ b/CodeGen/include/Luau/AssemblyBuilderX64.h @@ -175,6 +175,7 @@ class AssemblyBuilderX64 void vpshufps(RegisterX64 dst, RegisterX64 src1, OperandX64 src2, uint8_t shuffle); void vpinsrd(RegisterX64 dst, RegisterX64 src1, OperandX64 src2, uint8_t offset); + void vpextrd(RegisterX64 dst, RegisterX64 src, uint8_t offset); void vdpps(OperandX64 dst, OperandX64 src1, OperandX64 src2, uint8_t mask); void vfmadd213ps(OperandX64 dst, OperandX64 src1, OperandX64 src2); diff --git a/CodeGen/include/Luau/ConditionA64.h b/CodeGen/include/Luau/ConditionA64.h index b0bd3fe6b..d10506c91 100644 --- a/CodeGen/include/Luau/ConditionA64.h +++ b/CodeGen/include/Luau/ConditionA64.h @@ -49,7 +49,10 @@ enum class ConditionA64 // AL: always Always, - Count + Count, + + UnsignedLess = CarryClear, + UnsignedGreaterEqual = CarrySet, }; // Returns a condition that for 'a op b' will result in 'b op a' @@ -63,9 +66,9 @@ inline ConditionA64 getInverseCondition(ConditionA64 cond) case ConditionA64::NotEqual: return ConditionA64::NotEqual; case ConditionA64::UnsignedGreater: - return ConditionA64::CarryClear; + return ConditionA64::UnsignedLess; case ConditionA64::UnsignedLessEqual: - return ConditionA64::CarrySet; + return ConditionA64::UnsignedGreaterEqual; case ConditionA64::GreaterEqual: return ConditionA64::LessEqual; case ConditionA64::Less: diff --git a/CodeGen/include/Luau/IrAnalysis.h b/CodeGen/include/Luau/IrAnalysis.h index e5d1f3f29..afe8374b5 100644 --- a/CodeGen/include/Luau/IrAnalysis.h +++ b/CodeGen/include/Luau/IrAnalysis.h @@ -85,8 +85,15 @@ struct CfgInfo // VM registers captured by nested closures // This set can never have an active variadic sequence RegisterSet captured; + + // VM registers defined in any block + // Lowest variadic sequence start is stored to mark variadic writes + RegisterSet written; }; +// Calculate lists of block predecessors and successors +void computeCfgBlockEdges(IrFunction& function); + // A quick refresher on dominance and dominator trees: // * If A is a dominator of B (A dom B), you can never execute B without executing A first // * A is a strict dominator of B (A sdom B) is similar to previous one but A != B diff --git a/CodeGen/include/Luau/IrData.h b/CodeGen/include/Luau/IrData.h index e8bf25144..1e77453d5 100644 --- a/CodeGen/include/Luau/IrData.h +++ b/CodeGen/include/Luau/IrData.h @@ -56,7 +56,7 @@ enum class IrCmd : uint8_t // A: Rn LOAD_INT, - // Load a float field from vector as a double number + // Load a float field from vector (use FLOAT_TO_NUM to convert to double) // A: Rn or Kn // B: int (offset from the start of TValue) LOAD_FLOAT, @@ -120,9 +120,9 @@ enum class IrCmd : uint8_t // Store a vector into TValue // When optional 'E' tag is present, it is written out to the TValue as well // A: Rn - // B: double (x) - // C: double (y) - // D: double (z) + // B: float (x) + // C: float (y) + // D: float (z) // E: tag (optional) STORE_VECTOR, @@ -144,6 +144,14 @@ enum class IrCmd : uint8_t ADD_INT, SUB_INT, + // Sign extend an 8-bit value + // A: int + SEXTI8_INT, + + // Sign extend a 16-bit value + // A: int + SEXTI16_INT, + // Add/Sub/Mul/Div/Idiv/Mod two double numbers // A, B: double // In final x64 lowering, B can also be Rn or Kn @@ -202,6 +210,12 @@ enum class IrCmd : uint8_t // C, D: TValue (condition arguments) SELECT_VEC, + // Select one of the TValues based on the truthyness of A + // A: TValue + // B: TValue (if true) + // C: TValue (if false) + SELECT_IF_TRUTHY, + // Add/Sub/Mul/Div/Idiv two vectors // A, B: TValue ADD_VEC, @@ -216,10 +230,15 @@ enum class IrCmd : uint8_t // A: TValue UNM_VEC, - // Compute dot product between two vectors + // Compute dot product between two vectors as a float number (use FLOAT_TO_NUM to convert to double) // A, B: TValue DOT_VEC, + // Extract a component of a vector (use FLOAT_TO_NUM to convert to double) + // A: TValue (vector) + // B: int (0-3 index) + EXTRACT_VEC, + // Compute Luau 'not' operation on destructured TValue // A: tag // B: int (value) @@ -355,6 +374,14 @@ enum class IrCmd : uint8_t // A: double NUM_TO_UINT, + // Converts a float number to a double + // A: float + FLOAT_TO_NUM, + + // Converts a double number to a float + // A: double + NUM_TO_FLOAT, + // Converts a double number to a vector with the value in X/Y/Z // A: double NUM_TO_VEC, @@ -363,6 +390,10 @@ enum class IrCmd : uint8_t // A: TValue TAG_VECTOR, + // Clear high register bits of an unsigned integer register. Used to sanitize value of 'producesDirtyHighRegisterBits' instructions. + // A: uint + TRUNCATE_UINT, + // Adjust stack top (L->top) to point at 'B' TValues *after* the specified register // This is used to return multiple values // A: Rn @@ -436,14 +467,13 @@ enum class IrCmd : uint8_t // Note: all referenced registers might be modified in the operation CONCAT, - // Load function upvalue into stack slot - // A: Rn - // B: UPn + // Load function upvalue + // A: UPn GET_UPVALUE, - // Store TValue from stack slot into a function upvalue + // Store TValue into a function upvalue // A: UPn - // B: Rn + // B: TValue // C: tag/undef (tag of the value that was written) SET_UPVALUE, @@ -766,15 +796,15 @@ enum class IrCmd : uint8_t // C: int (value) BUFFER_WRITEI32, - // Read float value (converted to double) from buffer storage at specified offset + // Read float value (use FLOAT_TO_NUM to convert to double) from buffer storage at specified offset // A: pointer (buffer) // B: int (offset) BUFFER_READF32, - // Write float value (converted from double) to buffer storage at specified offset + // Write float value (use NUM_TO_FLOAT to convert from double) to buffer storage at specified offset // A: pointer (buffer) // B: int (offset) - // C: double (value) + // C: float (value) BUFFER_WRITEF32, // Read double value from buffer storage at specified offset @@ -904,8 +934,11 @@ enum class IrValueKind : uint8_t Tag, Int, Pointer, + Float, Double, Tvalue, + + Count }; struct IrInst @@ -1093,7 +1126,8 @@ struct IrFunction std::vector valueRestoreOps_NEW; std::vector validRestoreOpBlocks; - BytecodeTypeInfo bcTypeInfo; + BytecodeTypeInfo bcOriginalTypeInfo; // Bytecode type information as loaded + BytecodeTypeInfo bcTypeInfo; // Bytecode type information with additional inferences Proto* proto = nullptr; bool variadic = false; diff --git a/CodeGen/include/Luau/IrRegAllocX64.h b/CodeGen/include/Luau/IrRegAllocX64.h index 441d0e4d0..b00fb6bce 100644 --- a/CodeGen/include/Luau/IrRegAllocX64.h +++ b/CodeGen/include/Luau/IrRegAllocX64.h @@ -86,8 +86,10 @@ struct IrRegAllocX64 std::array xmmInstUsers; uint8_t usableXmmRegCount = 0; - std::bitset<256> usedSpillSlots; - unsigned maxUsedSlot = 0; + std::bitset<256> usedSpillSlots_DEPRECATED; + std::bitset<512> usedSpillSlotHalfs; // A bit for every stack slot split in 4 byte halfs + unsigned maxUsedSlot = 0; // Maximum number of 8 byte stack slots used + unsigned nextSpillId = 1; std::vector spills; }; diff --git a/CodeGen/include/Luau/IrUtils.h b/CodeGen/include/Luau/IrUtils.h index ae98a070b..5c3d43a7b 100644 --- a/CodeGen/include/Luau/IrUtils.h +++ b/CodeGen/include/Luau/IrUtils.h @@ -6,6 +6,7 @@ #include "Luau/IrData.h" LUAU_FASTFLAG(LuauCodegenFloatLoadStoreProp) +LUAU_FASTFLAG(LuauCodegenUpvalueLoadProp) namespace Luau { @@ -90,6 +91,8 @@ inline bool hasResult(IrCmd cmd) case IrCmd::GET_CLOSURE_UPVAL_ADDR: case IrCmd::ADD_INT: case IrCmd::SUB_INT: + case IrCmd::SEXTI8_INT: + case IrCmd::SEXTI16_INT: case IrCmd::ADD_NUM: case IrCmd::SUB_NUM: case IrCmd::MUL_NUM: @@ -106,12 +109,14 @@ inline bool hasResult(IrCmd cmd) case IrCmd::ABS_NUM: case IrCmd::SIGN_NUM: case IrCmd::SELECT_NUM: + case IrCmd::SELECT_IF_TRUTHY: case IrCmd::ADD_VEC: case IrCmd::SUB_VEC: case IrCmd::MUL_VEC: case IrCmd::DIV_VEC: - case IrCmd::DOT_VEC: case IrCmd::UNM_VEC: + case IrCmd::DOT_VEC: + case IrCmd::EXTRACT_VEC: case IrCmd::NOT_ANY: case IrCmd::CMP_ANY: case IrCmd::CMP_INT: @@ -129,8 +134,11 @@ inline bool hasResult(IrCmd cmd) case IrCmd::UINT_TO_NUM: case IrCmd::NUM_TO_INT: case IrCmd::NUM_TO_UINT: + case IrCmd::FLOAT_TO_NUM: + case IrCmd::NUM_TO_FLOAT: case IrCmd::NUM_TO_VEC: case IrCmd::TAG_VECTOR: + case IrCmd::TRUNCATE_UINT: case IrCmd::SUBSTITUTE: case IrCmd::INVOKE_FASTCALL: case IrCmd::BITAND_UINT: @@ -157,6 +165,8 @@ inline bool hasResult(IrCmd cmd) case IrCmd::BUFFER_READF32: case IrCmd::BUFFER_READF64: return true; + case IrCmd::GET_UPVALUE: + return FFlag::LuauCodegenUpvalueLoadProp; default: break; } @@ -209,8 +219,36 @@ inline bool hasSideEffects(IrCmd cmd) return !hasResult(cmd); } +inline bool producesDirtyHighRegisterBits(IrCmd cmd) +{ + return cmd == IrCmd::NUM_TO_UINT || cmd == IrCmd::INVOKE_FASTCALL || cmd == IrCmd::CMP_ANY; +} + IrValueKind getCmdValueKind(IrCmd cmd); +template +void visitArguments(IrInst& inst, F&& func) +{ + if (isPseudo(inst.cmd)) + return; + + func(inst.a); + func(inst.b); + func(inst.c); + func(inst.d); + func(inst.e); + func(inst.f); + func(inst.g); +} +template +bool anyArgumentMatch(IrInst& inst, F&& func) +{ + if (isPseudo(inst.cmd)) + return false; + + return func(inst.a) || func(inst.b) || func(inst.c) || func(inst.d) || func(inst.e) || func(inst.f) || func(inst.g); +} + bool isGCO(uint8_t tag); // Optional bit has to be cleared at call site, otherwise, this will return 'false' for 'userdata?' @@ -271,5 +309,7 @@ IrBlock& getNextBlock(IrFunction& function, const std::vector& sortedB // Returns next block in a chain, marked by 'constPropInBlockChains' optimization pass IrBlock* tryGetNextBlockInChain(IrFunction& function, IrBlock& block); +bool isEntryBlock(const IrBlock& block); + } // namespace CodeGen } // namespace Luau diff --git a/CodeGen/include/Luau/IrVisitUseDef.h b/CodeGen/include/Luau/IrVisitUseDef.h index aaac4afe2..8aea6279e 100644 --- a/CodeGen/include/Luau/IrVisitUseDef.h +++ b/CodeGen/include/Luau/IrVisitUseDef.h @@ -4,6 +4,8 @@ #include "Luau/Common.h" #include "Luau/IrData.h" +LUAU_FASTFLAG(LuauCodegenUpvalueLoadProp) + namespace Luau { namespace CodeGen @@ -80,10 +82,12 @@ static void visitVmRegDefsUses(T& visitor, IrFunction& function, const IrInst& i visitor.defRange(vmRegOp(inst.a), function.uintOp(inst.b)); break; case IrCmd::GET_UPVALUE: - visitor.def(inst.a); + if (!FFlag::LuauCodegenUpvalueLoadProp) + visitor.def(inst.a); break; case IrCmd::SET_UPVALUE: - visitor.use(inst.b); + if (!FFlag::LuauCodegenUpvalueLoadProp) + visitor.use(inst.b); break; case IrCmd::INTERRUPT: break; diff --git a/CodeGen/src/AssemblyBuilderA64.cpp b/CodeGen/src/AssemblyBuilderA64.cpp index 6ba989d42..3290e9f1a 100644 --- a/CodeGen/src/AssemblyBuilderA64.cpp +++ b/CodeGen/src/AssemblyBuilderA64.cpp @@ -7,6 +7,9 @@ #include #include +LUAU_FASTFLAG(LuauCodegenUpvalueLoadProp) +LUAU_FASTFLAG(LuauCodegenSplitFloat) + namespace Luau { namespace CodeGen @@ -23,7 +26,7 @@ static_assert(sizeof(textForCondition) / sizeof(textForCondition[0]) == size_t(C const unsigned kMaxAlign = 32; -static int getFmovImm(double value) +static int getFmovImmFp64(double value) { uint64_t u; static_assert(sizeof(u) == sizeof(value), "expected double to be 64-bit"); @@ -44,6 +47,27 @@ static int getFmovImm(double value) return dec == int(u >> 48) ? imm : -1; } +static int getFmovImmFp32(float value) +{ + uint32_t u; + static_assert(sizeof(u) == sizeof(value), "expected float to be 32-bit"); + memcpy(&u, &value, sizeof(value)); + + // positive 0 is encodable via movi + if (u == 0) + return 256; + + // early out: fmov can only encode float with 19 least significant zeros + if ((u & ((1ull << 19) - 1)) != 0) + return -1; + + // f32 expansion is abcdfegh => aBbbbbbc defgh000 00000000 00000000 + int imm = (int(u >> 24) & 0x80) | (int(u >> 19) & 0x7f); + int dec = ((imm & 0x80) << 5) | ((imm & 0x40) ? 0b00000111'11000000 : 0b00001000'00000000) | (imm & 0x3f); + + return dec == int(u >> 19) ? imm : -1; +} + AssemblyBuilderA64::AssemblyBuilderA64(bool logText, unsigned int features) : logText(logText) , features(features) @@ -529,6 +553,17 @@ void AssemblyBuilderA64::adr(RegisterA64 dst, uint64_t value) patchOffset(location, -int(location) - int((data.size() - pos) / 4), Patch::Imm19); } +void AssemblyBuilderA64::adr(RegisterA64 dst, float value) +{ + size_t pos = allocateData(4, 4); + uint32_t location = getCodeSize(); + + writef32(&data[pos], value); + placeADR("adr", dst, 0b10000); + + patchOffset(location, -int(location) - int((data.size() - pos) / 4), Patch::Imm19); +} + void AssemblyBuilderA64::adr(RegisterA64 dst, double value) { size_t pos = allocateData(8, 8); @@ -547,19 +582,37 @@ void AssemblyBuilderA64::adr(RegisterA64 dst, Label& label) void AssemblyBuilderA64::fmov(RegisterA64 dst, RegisterA64 src) { - CODEGEN_ASSERT(dst.kind == KindA64::d && (src.kind == KindA64::d || src.kind == KindA64::x)); - - if (src.kind == KindA64::d) - placeR1("fmov", dst, src, 0b000'11110'01'1'0000'00'10000); + if (FFlag::LuauCodegenUpvalueLoadProp || FFlag::LuauCodegenSplitFloat) + { + if (dst.kind == KindA64::d && src.kind == KindA64::d) + placeR1("fmov", dst, src, 0b00'11110'01'1'0000'00'10000); + else if (dst.kind == KindA64::d && src.kind == KindA64::x) + placeR1("fmov", dst, src, 0b00'11110'01'1'00'111'000000); + else if (dst.kind == KindA64::x && src.kind == KindA64::d) + placeR1("fmov", dst, src, 0b00'11110'01'1'00'110'000000); + else if (FFlag::LuauCodegenSplitFloat && dst.kind == KindA64::s && src.kind == KindA64::s) + placeR1("fmov", dst, src, 0b00'11110'00'1'0000'00'10000); + else if (FFlag::LuauCodegenSplitFloat && dst.kind == KindA64::s && src.kind == KindA64::w) + placeR1("fmov", dst, src, 0b00'11110'00'1'00'111'000000); + else + CODEGEN_ASSERT(!"Unsupported fmov kind"); + } else - placeR1("fmov", dst, src, 0b000'11110'01'1'00'111'000000); + { + CODEGEN_ASSERT(dst.kind == KindA64::d && (src.kind == KindA64::d || src.kind == KindA64::x)); + + if (src.kind == KindA64::d) + placeR1("fmov", dst, src, 0b000'11110'01'1'0000'00'10000); + else + placeR1("fmov", dst, src, 0b000'11110'01'1'00'111'000000); + } } void AssemblyBuilderA64::fmov(RegisterA64 dst, double src) { CODEGEN_ASSERT(dst.kind == KindA64::d || dst.kind == KindA64::q); - int imm = getFmovImm(src); + int imm = getFmovImmFp64(src); CODEGEN_ASSERT(imm >= 0 && imm <= 256); // fmov can't encode 0, but movi can; movi is otherwise not useful for fp immediates because it encodes repeating patterns @@ -579,6 +632,30 @@ void AssemblyBuilderA64::fmov(RegisterA64 dst, double src) } } +void AssemblyBuilderA64::fmov(RegisterA64 dst, float src) +{ + CODEGEN_ASSERT(dst.kind == KindA64::s || dst.kind == KindA64::q); + + int imm = getFmovImmFp32(src); + CODEGEN_ASSERT(imm >= 0 && imm <= 256); + + // fmov can't encode 0, but movi can; movi is otherwise not useful for fp immediates because it encodes repeating patterns + if (dst.kind == KindA64::s) + { + if (imm == 256) + placeFMOV("movi", dst, src, 0b001'0111100000'000'1110'01'00000); + else + placeFMOV("fmov", dst, src, 0b000'11110'00'1'00000000'100'00000 | (imm << 8)); + } + else + { + if (imm == 256) + placeFMOV("movi.4s", dst, src, 0b010'0111100000'000'0000'01'00000); + else + placeFMOV("fmov.4s", dst, src, 0b010'0111100000'000'1111'0'1'00000 | ((imm >> 5) << 11) | (imm & 31)); + } +} + void AssemblyBuilderA64::fabs(RegisterA64 dst, RegisterA64 src) { CODEGEN_ASSERT(dst.kind == KindA64::d && src.kind == KindA64::d); @@ -811,6 +888,22 @@ void AssemblyBuilderA64::dup_4s(RegisterA64 dst, RegisterA64 src, uint8_t index) commit(); } +void AssemblyBuilderA64::umov_4s(RegisterA64 dst, RegisterA64 src, uint8_t index) +{ + CODEGEN_ASSERT(dst.kind == KindA64::w); + CODEGEN_ASSERT(src.kind == KindA64::q); + CODEGEN_ASSERT(index < 4); + + if (logText) + logAppend(" %-12sw%d,v%d.s[%d]\n", "umov", dst.index, src.index, index); + + // Q A-SIMD SzIdx Opcode Rn Rd + uint32_t op = 0b0'0'0'01110000'00100'001111'00000'00000; + + place(dst.index | (src.index << 5) | op | (index << 19)); + + commit(); +} void AssemblyBuilderA64::fcmeq_4s(RegisterA64 dst, RegisterA64 src1, RegisterA64 src2) { if (logText) @@ -1018,9 +1111,14 @@ bool AssemblyBuilderA64::isMaskSupported(uint32_t mask) (mask >> rz) == (1u << (32 - lz - rz)) - 1; // sequence of 1s must be contiguous } -bool AssemblyBuilderA64::isFmovSupported(double value) +bool AssemblyBuilderA64::isFmovSupportedFp64(double value) +{ + return getFmovImmFp64(value) >= 0; +} + +bool AssemblyBuilderA64::isFmovSupportedFp32(float value) { - return getFmovImm(value) >= 0; + return getFmovImmFp32(value) >= 0; } void AssemblyBuilderA64::place0(const char* name, uint32_t op) diff --git a/CodeGen/src/AssemblyBuilderX64.cpp b/CodeGen/src/AssemblyBuilderX64.cpp index 84c2a40fd..60e098bcd 100644 --- a/CodeGen/src/AssemblyBuilderX64.cpp +++ b/CodeGen/src/AssemblyBuilderX64.cpp @@ -6,6 +6,8 @@ #include #include +LUAU_FASTFLAG(LuauCodegenBufferLoadProp2) + namespace Luau { namespace CodeGen @@ -264,13 +266,27 @@ void AssemblyBuilderX64::movsx(RegisterX64 lhs, OperandX64 rhs) if (logText) log("movsx", lhs, rhs); - CODEGEN_ASSERT(rhs.memSize == SizeX64::byte || rhs.memSize == SizeX64::word); + if (FFlag::LuauCodegenBufferLoadProp2) + { + SizeX64 size = rhs.cat == CategoryX64::reg ? rhs.base.size : rhs.memSize; + CODEGEN_ASSERT(size == SizeX64::byte || size == SizeX64::word); - placeRex(lhs, rhs); - place(0x0f); - place(rhs.memSize == SizeX64::byte ? 0xbe : 0xbf); - placeRegAndModRegMem(lhs, rhs); - commit(); + placeRex(lhs, rhs); + place(0x0f); + place(size == SizeX64::byte ? 0xbe : 0xbf); + placeRegAndModRegMem(lhs, rhs); + commit(); + } + else + { + CODEGEN_ASSERT(rhs.memSize == SizeX64::byte || rhs.memSize == SizeX64::word); + + placeRex(lhs, rhs); + place(0x0f); + place(rhs.memSize == SizeX64::byte ? 0xbe : 0xbf); + placeRegAndModRegMem(lhs, rhs); + commit(); + } } void AssemblyBuilderX64::movzx(RegisterX64 lhs, OperandX64 rhs) @@ -278,13 +294,27 @@ void AssemblyBuilderX64::movzx(RegisterX64 lhs, OperandX64 rhs) if (logText) log("movzx", lhs, rhs); - CODEGEN_ASSERT(rhs.memSize == SizeX64::byte || rhs.memSize == SizeX64::word); + if (FFlag::LuauCodegenBufferLoadProp2) + { + SizeX64 size = rhs.cat == CategoryX64::reg ? rhs.base.size : rhs.memSize; + CODEGEN_ASSERT(size == SizeX64::byte || size == SizeX64::word); - placeRex(lhs, rhs); - place(0x0f); - place(rhs.memSize == SizeX64::byte ? 0xb6 : 0xb7); - placeRegAndModRegMem(lhs, rhs); - commit(); + placeRex(lhs, rhs); + place(0x0f); + place(size == SizeX64::byte ? 0xb6 : 0xb7); + placeRegAndModRegMem(lhs, rhs); + commit(); + } + else + { + CODEGEN_ASSERT(rhs.memSize == SizeX64::byte || rhs.memSize == SizeX64::word); + + placeRex(lhs, rhs); + place(0x0f); + place(rhs.memSize == SizeX64::byte ? 0xb6 : 0xb7); + placeRegAndModRegMem(lhs, rhs); + commit(); + } } void AssemblyBuilderX64::div(OperandX64 op) @@ -964,12 +994,25 @@ void AssemblyBuilderX64::vpinsrd(RegisterX64 dst, RegisterX64 src1, OperandX64 s placeAvx("vpinsrd", dst, src1, src2, offset, 0x22, false, AVX_0F3A, AVX_66); } +void AssemblyBuilderX64::vpextrd(RegisterX64 dst, RegisterX64 src, uint8_t offset) +{ + // 'placeAvx' wrapper doesn't have an overload for this archetype (opcode r/m, reg, imm8) + if (logText) + log("vpextrd", dst, src, offset); + + placeVex(src, noreg, dst, false, AVX_0F3A, AVX_66); + place(0x16); + placeRegAndModRegMem(src, dst, /*extraCodeBytes=*/1); + placeImm8(offset); + + commit(); +} + void AssemblyBuilderX64::vdpps(OperandX64 dst, OperandX64 src1, OperandX64 src2, uint8_t mask) { placeAvx("vdpps", dst, src1, src2, mask, 0x40, false, AVX_0F3A, AVX_66); } - void AssemblyBuilderX64::vfmadd213ps(OperandX64 dst, OperandX64 src1, OperandX64 src2) { placeAvx("vfmadd213ps", dst, src1, src2, 0xA8, false, AVX_0F38, AVX_66); @@ -1348,7 +1391,10 @@ void AssemblyBuilderX64::placeAvx( uint8_t prefix ) { - CODEGEN_ASSERT((dst.cat == CategoryX64::mem && src.cat == CategoryX64::reg) || (dst.cat == CategoryX64::reg && src.cat == CategoryX64::mem)); + CODEGEN_ASSERT( + (dst.cat == CategoryX64::mem && src.cat == CategoryX64::reg) || (dst.cat == CategoryX64::reg && src.cat == CategoryX64::mem) || + (dst.cat == CategoryX64::reg && src.cat == CategoryX64::reg) + ); if (logText) log(name, dst, src); @@ -1402,7 +1448,12 @@ void AssemblyBuilderX64:: CODEGEN_ASSERT(src2.cat == CategoryX64::reg || src2.cat == CategoryX64::mem); if (logText) - log(name, dst, src1, src2, imm8); + { + if (src1.base == noreg) + log(name, src2, dst, imm8); + else + log(name, dst, src1, src2, imm8); + } placeVex(dst, src1, src2, setW, mode, prefix); place(code); diff --git a/CodeGen/src/BytecodeAnalysis.cpp b/CodeGen/src/BytecodeAnalysis.cpp index 627f9676b..31e29e31a 100644 --- a/CodeGen/src/BytecodeAnalysis.cpp +++ b/CodeGen/src/BytecodeAnalysis.cpp @@ -10,6 +10,8 @@ #include +LUAU_FASTFLAG(LuauCodegenSetBlockEntryState) + namespace Luau { namespace CodeGen @@ -107,6 +109,10 @@ void loadBytecodeTypeInfo(IrFunction& function) } } + // Preserve original information + if (FFlag::LuauCodegenSetBlockEntryState) + function.bcOriginalTypeInfo = function.bcTypeInfo; + CODEGEN_ASSERT(offset == size_t(proto->sizetypeinfo)); } diff --git a/CodeGen/src/CodeGenLower.h b/CodeGen/src/CodeGenLower.h index cbddfd45b..04183f2cd 100644 --- a/CodeGen/src/CodeGenLower.h +++ b/CodeGen/src/CodeGenLower.h @@ -27,6 +27,7 @@ LUAU_FASTINT(CodegenHeuristicsInstructionLimit) LUAU_FASTINT(CodegenHeuristicsBlockLimit) LUAU_FASTINT(CodegenHeuristicsBlockInstructionLimit) LUAU_FASTFLAG(LuauCodegenBlockSafeEnv) +LUAU_FASTFLAG(LuauCodegenBetterSccRemoval) namespace Luau { @@ -376,6 +377,12 @@ inline bool lowerFunction( markDeadStoresInBlockChains(ir); + if (FFlag::LuauCodegenBetterSccRemoval) + { + // Recompute the CFG predecessors/successors to match block uses after optimizations + computeCfgBlockEdges(ir.function); + } + std::vector sortedBlocks = getSortedBlockOrder(ir.function); // In order to allocate registers during lowering, we need to know where instruction results are last used diff --git a/CodeGen/src/EmitCommonX64.cpp b/CodeGen/src/EmitCommonX64.cpp index 897c93e22..d224c9974 100644 --- a/CodeGen/src/EmitCommonX64.cpp +++ b/CodeGen/src/EmitCommonX64.cpp @@ -14,8 +14,8 @@ #include - -LUAU_DYNAMIC_FASTFLAGVARIABLE(AddReturnExectargetCheck, false); +LUAU_DYNAMIC_FASTFLAGVARIABLE(AddReturnExectargetCheck, false) +LUAU_FASTFLAG(LuauCodegenUpvalueLoadProp) namespace Luau { @@ -228,8 +228,50 @@ void callSetTable(IrRegAllocX64& regs, AssemblyBuilderX64& build, int rb, Operan emitUpdateBase(build); } -void checkObjectBarrierConditions(AssemblyBuilderX64& build, RegisterX64 tmp, RegisterX64 object, IrOp ra, int ratag, Label& skip) +void checkObjectBarrierConditions(AssemblyBuilderX64& build, RegisterX64 tmp, RegisterX64 object, RegisterX64 ra, IrOp raOp, int ratag, Label& skip) { + CODEGEN_ASSERT(FFlag::LuauCodegenUpvalueLoadProp); + + // Barrier should've been optimized away if we know that it's not collectable, checking for correctness + if (ratag == -1 || !isGCO(ratag)) + { + // iscollectable(ra) + if (raOp.kind == IrOpKind::Inst) + { + build.vpextrd(dwordReg(tmp), ra, 3); + build.cmp(dwordReg(tmp), LUA_TSTRING); + } + else + { + OperandX64 tag = (raOp.kind == IrOpKind::VmReg) ? luauRegTag(vmRegOp(raOp)) : luauConstantTag(vmConstOp(raOp)); + build.cmp(tag, LUA_TSTRING); + } + + build.jcc(ConditionX64::Less, skip); + } + + // isblack(obj2gco(o)) + build.test(byte[object + offsetof(GCheader, marked)], bitmask(BLACKBIT)); + build.jcc(ConditionX64::Zero, skip); + + // iswhite(gcvalue(ra)) + if (raOp.kind == IrOpKind::Inst) + { + build.vmovq(tmp, ra); + } + else + { + OperandX64 value = (raOp.kind == IrOpKind::VmReg) ? luauRegValue(vmRegOp(raOp)) : luauConstantValue(vmConstOp(raOp)); + build.mov(tmp, value); + } + build.test(byte[tmp + offsetof(GCheader, marked)], bit2mask(WHITE0BIT, WHITE1BIT)); + build.jcc(ConditionX64::Zero, skip); +} + +void checkObjectBarrierConditions_DEPRECATED(AssemblyBuilderX64& build, RegisterX64 tmp, RegisterX64 object, IrOp ra, int ratag, Label& skip) +{ + CODEGEN_ASSERT(!FFlag::LuauCodegenUpvalueLoadProp); + // Barrier should've been optimized away if we know that it's not collectable, checking for correctness if (ratag == -1 || !isGCO(ratag)) { @@ -250,13 +292,36 @@ void checkObjectBarrierConditions(AssemblyBuilderX64& build, RegisterX64 tmp, Re build.jcc(ConditionX64::Zero, skip); } +void callBarrierObject(IrRegAllocX64& regs, AssemblyBuilderX64& build, RegisterX64 object, IrOp objectOp, RegisterX64 ra, IrOp raOp, int ratag) +{ + CODEGEN_ASSERT(FFlag::LuauCodegenUpvalueLoadProp); + + Label skip; + + ScopedRegX64 tmp{regs, SizeX64::qword}; + checkObjectBarrierConditions(build, tmp.reg, object, ra, raOp, ratag, skip); + + { + ScopedSpills spillGuard(regs); + + IrCallWrapperX64 callWrap(regs, build); + callWrap.addArgument(SizeX64::qword, rState); + callWrap.addArgument(SizeX64::qword, object, objectOp); + callWrap.addArgument(SizeX64::qword, tmp); + callWrap.call(qword[rNativeContext + offsetof(NativeContext, luaC_barrierf)]); + } -void callBarrierObject(IrRegAllocX64& regs, AssemblyBuilderX64& build, RegisterX64 object, IrOp objectOp, IrOp ra, int ratag) + build.setLabel(skip); +} + +void callBarrierObject_DEPRECATED(IrRegAllocX64& regs, AssemblyBuilderX64& build, RegisterX64 object, IrOp objectOp, IrOp ra, int ratag) { + CODEGEN_ASSERT(!FFlag::LuauCodegenUpvalueLoadProp); + Label skip; ScopedRegX64 tmp{regs, SizeX64::qword}; - checkObjectBarrierConditions(build, tmp.reg, object, ra, ratag, skip); + checkObjectBarrierConditions_DEPRECATED(build, tmp.reg, object, ra, ratag, skip); { ScopedSpills spillGuard(regs); diff --git a/CodeGen/src/EmitCommonX64.h b/CodeGen/src/EmitCommonX64.h index efa4bd9ec..da05d4a26 100644 --- a/CodeGen/src/EmitCommonX64.h +++ b/CodeGen/src/EmitCommonX64.h @@ -208,8 +208,10 @@ void callArithHelper(IrRegAllocX64& regs, AssemblyBuilderX64& build, int ra, Ope void callLengthHelper(IrRegAllocX64& regs, AssemblyBuilderX64& build, int ra, int rb); void callGetTable(IrRegAllocX64& regs, AssemblyBuilderX64& build, int rb, OperandX64 c, int ra); void callSetTable(IrRegAllocX64& regs, AssemblyBuilderX64& build, int rb, OperandX64 c, int ra); -void checkObjectBarrierConditions(AssemblyBuilderX64& build, RegisterX64 tmp, RegisterX64 object, IrOp ra, int ratag, Label& skip); -void callBarrierObject(IrRegAllocX64& regs, AssemblyBuilderX64& build, RegisterX64 object, IrOp objectOp, IrOp ra, int ratag); +void checkObjectBarrierConditions(AssemblyBuilderX64& build, RegisterX64 tmp, RegisterX64 object, RegisterX64 ra, IrOp raOp, int ratag, Label& skip); +void checkObjectBarrierConditions_DEPRECATED(AssemblyBuilderX64& build, RegisterX64 tmp, RegisterX64 object, IrOp ra, int ratag, Label& skip); +void callBarrierObject(IrRegAllocX64& regs, AssemblyBuilderX64& build, RegisterX64 object, IrOp objectOp, RegisterX64 ra, IrOp raOp, int ratag); +void callBarrierObject_DEPRECATED(IrRegAllocX64& regs, AssemblyBuilderX64& build, RegisterX64 object, IrOp objectOp, IrOp ra, int ratag); void callBarrierTableFast(IrRegAllocX64& regs, AssemblyBuilderX64& build, RegisterX64 table, IrOp tableOp); void callStepGc(IrRegAllocX64& regs, AssemblyBuilderX64& build); diff --git a/CodeGen/src/IrAnalysis.cpp b/CodeGen/src/IrAnalysis.cpp index 58b945fb5..3d1c476b8 100644 --- a/CodeGen/src/IrAnalysis.cpp +++ b/CodeGen/src/IrAnalysis.cpp @@ -63,7 +63,7 @@ void updateLastUseLocations(IrFunction& function, const std::vector& s { std::vector& instructions = function.instructions; -#if defined(CODEGEN_ASSERTENABLED) +#if defined(LUAU_ASSERTENABLED) // Last use assignements should be called only once for (IrInst& inst : instructions) CODEGEN_ASSERT(inst.lastUse == 0); @@ -508,6 +508,29 @@ static void computeCfgLiveInOutRegSets(IrFunction& function) } } + // Collect data on all registers that are written + function.cfg.written.regs.reset(); + + for (size_t blockIdx = 0; blockIdx < function.blocks.size(); blockIdx++) + { + const IrBlock& block = function.blocks[blockIdx]; + + if (block.kind == IrBlockKind::Dead) + continue; + + RegisterSet& defRs = info.def[blockIdx]; + + function.cfg.written.regs |= defRs.regs; + + if (defRs.varargSeq) + { + if (!function.cfg.written.varargSeq || defRs.varargStart < function.cfg.written.varargStart) + function.cfg.written.varargStart = defRs.varargStart; + + function.cfg.written.varargSeq = true; + } + } + // If Proto data is available, validate that entry block arguments match required registers if (function.proto) { @@ -520,7 +543,7 @@ static void computeCfgLiveInOutRegSets(IrFunction& function) } } -static void computeCfgBlockEdges(IrFunction& function) +void computeCfgBlockEdges(IrFunction& function) { CfgInfo& info = function.cfg; diff --git a/CodeGen/src/IrBuilder.cpp b/CodeGen/src/IrBuilder.cpp index 13c4a2abf..d761aacf4 100644 --- a/CodeGen/src/IrBuilder.cpp +++ b/CodeGen/src/IrBuilder.cpp @@ -13,7 +13,7 @@ #include LUAU_FASTFLAG(LuauCodegenBlockSafeEnv) - +LUAU_FASTFLAG(LuauCodegenSetBlockEntryState) LUAU_FASTFLAG(LuauCodegenChainLink) namespace Luau @@ -42,7 +42,7 @@ static bool hasTypedParameters(const BytecodeTypeInfo& typeInfo) static void buildArgumentTypeChecks(IrBuilder& build) { - const BytecodeTypeInfo& typeInfo = build.function.bcTypeInfo; + const BytecodeTypeInfo& typeInfo = FFlag::LuauCodegenSetBlockEntryState ? build.function.bcOriginalTypeInfo : build.function.bcTypeInfo; CODEGEN_ASSERT(hasTypedParameters(typeInfo)); for (size_t i = 0; i < typeInfo.argumentTypes.size(); i++) diff --git a/CodeGen/src/IrDump.cpp b/CodeGen/src/IrDump.cpp index e82ebaccd..a7343e8f8 100644 --- a/CodeGen/src/IrDump.cpp +++ b/CodeGen/src/IrDump.cpp @@ -137,6 +137,10 @@ const char* getCmdName(IrCmd cmd) return "ADD_INT"; case IrCmd::SUB_INT: return "SUB_INT"; + case IrCmd::SEXTI8_INT: + return "SEXTI8_INT"; + case IrCmd::SEXTI16_INT: + return "SEXTI16_INT"; case IrCmd::ADD_NUM: return "ADD_NUM"; case IrCmd::SUB_NUM: @@ -173,6 +177,8 @@ const char* getCmdName(IrCmd cmd) return "MULADD_NUM"; case IrCmd::SELECT_VEC: return "SELECT_VEC"; + case IrCmd::SELECT_IF_TRUTHY: + return "SELECT_IF_TRUTHY"; case IrCmd::ADD_VEC: return "ADD_VEC"; case IrCmd::SUB_VEC: @@ -181,12 +187,14 @@ const char* getCmdName(IrCmd cmd) return "MUL_VEC"; case IrCmd::DIV_VEC: return "DIV_VEC"; + case IrCmd::MULADD_VEC: + return "MULADD_VEC"; case IrCmd::UNM_VEC: return "UNM_VEC"; case IrCmd::DOT_VEC: return "DOT_VEC"; - case IrCmd::MULADD_VEC: - return "MULADD_VEC"; + case IrCmd::EXTRACT_VEC: + return "EXTRACT_VEC"; case IrCmd::NOT_ANY: return "NOT_ANY"; case IrCmd::CMP_ANY: @@ -239,10 +247,16 @@ const char* getCmdName(IrCmd cmd) return "NUM_TO_INT"; case IrCmd::NUM_TO_UINT: return "NUM_TO_UINT"; + case IrCmd::FLOAT_TO_NUM: + return "FLOAT_TO_NUM"; + case IrCmd::NUM_TO_FLOAT: + return "NUM_TO_FLOAT"; case IrCmd::NUM_TO_VEC: return "NUM_TO_VEC"; case IrCmd::TAG_VECTOR: return "TAG_VECTOR"; + case IrCmd::TRUNCATE_UINT: + return "TRUNCATE_UINT"; case IrCmd::ADJUST_STACK_TO_REG: return "ADJUST_STACK_TO_REG"; case IrCmd::ADJUST_STACK_TO_TOP: @@ -799,7 +813,7 @@ void toStringDetailed( ) { // Report captured registers for entry block - if (includeRegFlowInfo == IncludeRegFlowInfo::Yes && block.useCount == 0 && block.kind != IrBlockKind::Dead && ctx.cfg.captured.regs.any()) + if (includeRegFlowInfo == IncludeRegFlowInfo::Yes && isEntryBlock(block) && ctx.cfg.captured.regs.any()) { append(ctx.result, "; captured regs: "); appendRegisterSet(ctx, ctx.cfg.captured, ", "); diff --git a/CodeGen/src/IrLoweringA64.cpp b/CodeGen/src/IrLoweringA64.cpp index ae874f75c..143d580e4 100644 --- a/CodeGen/src/IrLoweringA64.cpp +++ b/CodeGen/src/IrLoweringA64.cpp @@ -13,6 +13,9 @@ #include "lgc.h" LUAU_FASTFLAG(LuauCodegenBlockSafeEnv) +LUAU_FASTFLAG(LuauCodegenUpvalueLoadProp) +LUAU_FASTFLAG(LuauCodegenNumIntFolds2) +LUAU_FASTFLAG(LuauCodegenSplitFloat) namespace Luau { @@ -129,8 +132,10 @@ static void emitAddOffset(AssemblyBuilderA64& build, RegisterA64 dst, RegisterA6 } } -static void checkObjectBarrierConditions(AssemblyBuilderA64& build, RegisterA64 object, RegisterA64 temp, IrOp ra, int ratag, Label& skip) +static void checkObjectBarrierConditions_DEPRECATED(AssemblyBuilderA64& build, RegisterA64 object, RegisterA64 temp, IrOp ra, int ratag, Label& skip) { + CODEGEN_ASSERT(!FFlag::LuauCodegenUpvalueLoadProp); + RegisterA64 tempw = castReg(KindA64::w, temp); AddressA64 addr = temp; @@ -250,6 +255,14 @@ static uint64_t getDoubleBits(double value) return result; } +static uint32_t getFloatBits(float value) +{ + uint32_t result; + static_assert(sizeof(result) == sizeof(value), "Expecting float to be 32-bit"); + memcpy(&result, &value, sizeof(value)); + return result; +} + IrLoweringA64::IrLoweringA64(AssemblyBuilderA64& build, ModuleHelpers& helpers, IrFunction& function, LoweringStats* stats) : build(build) , helpers(helpers) @@ -307,12 +320,22 @@ void IrLoweringA64::lowerInst(IrInst& inst, uint32_t index, const IrBlock& next) } case IrCmd::LOAD_FLOAT: { - inst.regA64 = regs.allocReg(KindA64::d, index); - RegisterA64 temp = castReg(KindA64::s, inst.regA64); // safe to alias a fresh register - AddressA64 addr = tempAddr(inst.a, intOp(inst.b)); + if (FFlag::LuauCodegenSplitFloat) + { + inst.regA64 = regs.allocReg(KindA64::s, index); + AddressA64 addr = tempAddr(inst.a, intOp(inst.b)); - build.ldr(temp, addr); - build.fcvt(inst.regA64, temp); + build.ldr(inst.regA64, addr); + } + else + { + inst.regA64 = regs.allocReg(KindA64::d, index); + RegisterA64 temp = castReg(KindA64::s, inst.regA64); // safe to alias a fresh register + AddressA64 addr = tempAddr(inst.a, intOp(inst.b)); + + build.ldr(temp, addr); + build.fcvt(inst.regA64, temp); + } break; } case IrCmd::LOAD_TVALUE: @@ -486,20 +509,36 @@ void IrLoweringA64::lowerInst(IrInst& inst, uint32_t index, const IrBlock& next) } case IrCmd::STORE_VECTOR: { - RegisterA64 temp1 = tempDouble(inst.b); - RegisterA64 temp2 = tempDouble(inst.c); - RegisterA64 temp3 = tempDouble(inst.d); - RegisterA64 temp4 = regs.allocTemp(KindA64::s); + if (FFlag::LuauCodegenSplitFloat) + { + RegisterA64 temp1 = tempFloat(inst.b); + RegisterA64 temp2 = tempFloat(inst.c); + RegisterA64 temp3 = tempFloat(inst.d); - AddressA64 addr = tempAddr(inst.a, offsetof(TValue, value)); - CODEGEN_ASSERT(addr.kind == AddressKindA64::imm && addr.data % 4 == 0 && unsigned(addr.data + 8) / 4 <= AddressA64::kMaxOffset); + AddressA64 addr = tempAddr(inst.a, offsetof(TValue, value)); + CODEGEN_ASSERT(addr.kind == AddressKindA64::imm && addr.data % 4 == 0 && unsigned(addr.data + 8) / 4 <= AddressA64::kMaxOffset); + + build.str(temp1, AddressA64(addr.base, addr.data + 0)); + build.str(temp2, AddressA64(addr.base, addr.data + 4)); + build.str(temp3, AddressA64(addr.base, addr.data + 8)); + } + else + { + RegisterA64 temp1 = tempDouble(inst.b); + RegisterA64 temp2 = tempDouble(inst.c); + RegisterA64 temp3 = tempDouble(inst.d); + RegisterA64 temp4 = regs.allocTemp(KindA64::s); - build.fcvt(temp4, temp1); - build.str(temp4, AddressA64(addr.base, addr.data + 0)); - build.fcvt(temp4, temp2); - build.str(temp4, AddressA64(addr.base, addr.data + 4)); - build.fcvt(temp4, temp3); - build.str(temp4, AddressA64(addr.base, addr.data + 8)); + AddressA64 addr = tempAddr(inst.a, offsetof(TValue, value)); + CODEGEN_ASSERT(addr.kind == AddressKindA64::imm && addr.data % 4 == 0 && unsigned(addr.data + 8) / 4 <= AddressA64::kMaxOffset); + + build.fcvt(temp4, temp1); + build.str(temp4, AddressA64(addr.base, addr.data + 0)); + build.fcvt(temp4, temp2); + build.str(temp4, AddressA64(addr.base, addr.data + 4)); + build.fcvt(temp4, temp3); + build.str(temp4, AddressA64(addr.base, addr.data + 8)); + } if (inst.e.kind != IrOpKind::None) { @@ -577,6 +616,16 @@ void IrLoweringA64::lowerInst(IrInst& inst, uint32_t index, const IrBlock& next) build.sub(inst.regA64, temp1, temp2); } break; + case IrCmd::SEXTI8_INT: + inst.regA64 = regs.allocReuse(KindA64::w, index, {inst.a}); + + build.sbfx(inst.regA64, regOp(inst.a), 0, 8); // sextb + break; + case IrCmd::SEXTI16_INT: + inst.regA64 = regs.allocReuse(KindA64::w, index, {inst.a}); + + build.sbfx(inst.regA64, regOp(inst.a), 0, 16); // sexth + break; case IrCmd::ADD_NUM: { inst.regA64 = regs.allocReuse(KindA64::d, index, {inst.a, inst.b}); @@ -759,6 +808,36 @@ void IrLoweringA64::lowerInst(IrInst& inst, uint32_t index, const IrBlock& next) build.bit(inst.regA64, temp2, mask); break; } + case IrCmd::SELECT_IF_TRUTHY: + { + inst.regA64 = regs.allocReg(KindA64::q, index); + + // Place lhs as the result, we will overwrite it with rhs if 'A' is falsy later + build.mov(inst.regA64, regOp(inst.b)); + + // Get rhs register early, so a potential restore happens on both sides of a conditional control flow + RegisterA64 c = regOp(inst.c); + + RegisterA64 temp = regs.allocTemp(KindA64::w); + Label saveRhs, exit; + + // Check tag first + build.umov_4s(temp, regOp(inst.a), 3); + build.cmp(temp, LUA_TBOOLEAN); + + build.b(ConditionA64::UnsignedLess, saveRhs); // rhs if 'A' is nil + build.b(ConditionA64::UnsignedGreater, exit); // Keep lhs if 'A' is not a boolean + + // Check the boolean value + build.umov_4s(temp, regOp(inst.a), 0); + build.cbnz(temp, exit); // Keep lhs if 'A' is true + + build.setLabel(saveRhs); + build.mov(inst.regA64, c); + + build.setLabel(exit); + break; + } case IrCmd::MULADD_VEC: { RegisterA64 tempA = regOp(inst.a); @@ -817,17 +896,66 @@ void IrLoweringA64::lowerInst(IrInst& inst, uint32_t index, const IrBlock& next) } case IrCmd::DOT_VEC: { - inst.regA64 = regs.allocReg(KindA64::d, index); + if (FFlag::LuauCodegenSplitFloat) + { + inst.regA64 = regs.allocReg(KindA64::s, index); - RegisterA64 temp = regs.allocTemp(KindA64::q); - RegisterA64 temps = castReg(KindA64::s, temp); - RegisterA64 regs = castReg(KindA64::s, inst.regA64); + RegisterA64 temp = regs.allocTemp(KindA64::q); + RegisterA64 temps = castReg(KindA64::s, temp); + + build.fmul(temp, regOp(inst.a), regOp(inst.b)); + build.faddp(inst.regA64, temps); // x+y + build.dup_4s(temp, temp, 2); + build.fadd(inst.regA64, inst.regA64, temps); // +z + } + else + { + inst.regA64 = regs.allocReg(KindA64::d, index); + + RegisterA64 temp = regs.allocTemp(KindA64::q); + RegisterA64 temps = castReg(KindA64::s, temp); + RegisterA64 regs = castReg(KindA64::s, inst.regA64); + + build.fmul(temp, regOp(inst.a), regOp(inst.b)); + build.faddp(regs, temps); // x+y + build.dup_4s(temp, temp, 2); + build.fadd(regs, regs, temps); // +z + build.fcvt(inst.regA64, regs); + } + break; + } + case IrCmd::EXTRACT_VEC: + { + if (FFlag::LuauCodegenSplitFloat) + { + inst.regA64 = regs.allocReg(KindA64::s, index); - build.fmul(temp, regOp(inst.a), regOp(inst.b)); - build.faddp(regs, temps); // x+y - build.dup_4s(temp, temp, 2); - build.fadd(regs, regs, temps); // +z - build.fcvt(inst.regA64, regs); + if (intOp(inst.b) == 0) + { + // Lane vN.s[0] can just be read directly as sN + build.fmov(inst.regA64, castReg(KindA64::s, regOp(inst.a))); + } + else + { + build.dup_4s(inst.regA64, regOp(inst.a), intOp(inst.b)); + } + } + else + { + inst.regA64 = regs.allocReg(KindA64::d, index); + + if (intOp(inst.b) == 0) + { + // Lane vN.s[0] can just be read directly as sN + build.fcvt(inst.regA64, castReg(KindA64::s, regOp(inst.a))); + } + else + { + RegisterA64 temp = regs.allocTemp(KindA64::s); + build.dup_4s(temp, regOp(inst.a), intOp(inst.b)); + build.fcvt(inst.regA64, temp); + } + } break; } case IrCmd::NOT_ANY: @@ -909,6 +1037,7 @@ void IrLoweringA64::lowerInst(IrInst& inst, uint32_t index, const IrBlock& next) emitUpdateBase(build); inst.regA64 = regs.takeReg(w0, index); + // Skipping high register bits clear, only consumer is JUMP_CMP_INT which doesn't read them break; } case IrCmd::CMP_TAG: @@ -1222,6 +1351,9 @@ void IrLoweringA64::lowerInst(IrInst& inst, uint32_t index, const IrBlock& next) build.blr(x1); inst.regA64 = regs.takeReg(w0, index); + + if (FFlag::LuauCodegenNumIntFolds2) + build.ubfx(inst.regA64, inst.regA64, 0, 32); // Ensure high register bits are cleared break; } case IrCmd::STRING_LEN: @@ -1365,6 +1497,16 @@ void IrLoweringA64::lowerInst(IrInst& inst, uint32_t index, const IrBlock& next) build.fcvtzs(castReg(KindA64::x, inst.regA64), temp); break; } + case IrCmd::FLOAT_TO_NUM: + inst.regA64 = regs.allocReg(KindA64::d, index); + + build.fcvt(inst.regA64, regOp(inst.a)); + break; + case IrCmd::NUM_TO_FLOAT: + inst.regA64 = regs.allocReg(KindA64::s, index); + + build.fcvt(inst.regA64, regOp(inst.a)); + break; case IrCmd::NUM_TO_VEC: { inst.regA64 = regs.allocReg(KindA64::q, index); @@ -1376,9 +1518,9 @@ void IrLoweringA64::lowerInst(IrInst& inst, uint32_t index, const IrBlock& next) static_assert(sizeof(asU32) == sizeof(value), "Expecting float to be 32-bit"); memcpy(&asU32, &value, sizeof(value)); - if (AssemblyBuilderA64::isFmovSupported(value)) + if (AssemblyBuilderA64::isFmovSupportedFp64(value)) { - build.fmov(inst.regA64, value); + build.fmov(inst.regA64, double(value)); } else { @@ -1413,6 +1555,11 @@ void IrLoweringA64::lowerInst(IrInst& inst, uint32_t index, const IrBlock& next) build.ins_4s(inst.regA64, tempw, 3); break; } + case IrCmd::TRUNCATE_UINT: + inst.regA64 = regs.allocReuse(KindA64::w, index, {inst.a}); + + build.ubfx(castReg(KindA64::x, inst.regA64), castReg(KindA64::x, regOp(inst.a)), 0, 32); // explicit uxtw + break; case IrCmd::ADJUST_STACK_TO_REG: { RegisterA64 temp = regs.allocTemp(KindA64::x); @@ -1495,6 +1642,7 @@ void IrLoweringA64::lowerInst(IrInst& inst, uint32_t index, const IrBlock& next) build.blr(x6); inst.regA64 = regs.takeReg(w0, index); + // Skipping high register bits clear, only consumer is CHECK_FASTCALL_RES which doesn't read them break; } case IrCmd::CHECK_FASTCALL_RES: @@ -1657,58 +1805,119 @@ void IrLoweringA64::lowerInst(IrInst& inst, uint32_t index, const IrBlock& next) break; case IrCmd::GET_UPVALUE: { - RegisterA64 temp1 = regs.allocTemp(KindA64::x); - RegisterA64 temp2 = regs.allocTemp(KindA64::q); - RegisterA64 temp3 = regs.allocTemp(KindA64::w); + if (FFlag::LuauCodegenUpvalueLoadProp) + { + inst.regA64 = regs.allocReg(KindA64::q, index); - build.add(temp1, rClosure, uint16_t(offsetof(Closure, l.uprefs) + sizeof(TValue) * vmUpvalueOp(inst.b))); + RegisterA64 temp1 = regs.allocTemp(KindA64::x); + RegisterA64 temp2 = regs.allocTemp(KindA64::w); - // uprefs[] is either an actual value, or it points to UpVal object which has a pointer to value - Label skip; - build.ldr(temp3, mem(temp1, offsetof(TValue, tt))); - build.cmp(temp3, LUA_TUPVAL); - build.b(ConditionA64::NotEqual, skip); + build.add(temp1, rClosure, uint16_t(offsetof(Closure, l.uprefs) + sizeof(TValue) * vmUpvalueOp(inst.a))); - // UpVal.v points to the value (either on stack, or on heap inside each UpVal, but we can deref it unconditionally) - build.ldr(temp1, mem(temp1, offsetof(TValue, value.gc))); - build.ldr(temp1, mem(temp1, offsetof(UpVal, v))); + // uprefs[] is either an actual value, or it points to UpVal object which has a pointer to value + Label skip; + build.ldr(temp2, mem(temp1, offsetof(TValue, tt))); + build.cmp(temp2, LUA_TUPVAL); + build.b(ConditionA64::NotEqual, skip); - build.setLabel(skip); + // UpVal.v points to the value (either on stack, or on heap inside each UpVal, but we can deref it unconditionally) + build.ldr(temp1, mem(temp1, offsetof(TValue, value.gc))); + build.ldr(temp1, mem(temp1, offsetof(UpVal, v))); + + build.setLabel(skip); + + build.ldr(inst.regA64, temp1); + } + else + { + RegisterA64 temp1 = regs.allocTemp(KindA64::x); + RegisterA64 temp2 = regs.allocTemp(KindA64::q); + RegisterA64 temp3 = regs.allocTemp(KindA64::w); + + build.add(temp1, rClosure, uint16_t(offsetof(Closure, l.uprefs) + sizeof(TValue) * vmUpvalueOp(inst.b))); - build.ldr(temp2, temp1); - build.str(temp2, mem(rBase, vmRegOp(inst.a) * sizeof(TValue))); + // uprefs[] is either an actual value, or it points to UpVal object which has a pointer to value + Label skip; + build.ldr(temp3, mem(temp1, offsetof(TValue, tt))); + build.cmp(temp3, LUA_TUPVAL); + build.b(ConditionA64::NotEqual, skip); + + // UpVal.v points to the value (either on stack, or on heap inside each UpVal, but we can deref it unconditionally) + build.ldr(temp1, mem(temp1, offsetof(TValue, value.gc))); + build.ldr(temp1, mem(temp1, offsetof(UpVal, v))); + + build.setLabel(skip); + + build.ldr(temp2, temp1); + build.str(temp2, mem(rBase, vmRegOp(inst.a) * sizeof(TValue))); + } break; } case IrCmd::SET_UPVALUE: { - RegisterA64 temp1 = regs.allocTemp(KindA64::x); - RegisterA64 temp2 = regs.allocTemp(KindA64::x); - RegisterA64 temp3 = regs.allocTemp(KindA64::q); + if (FFlag::LuauCodegenUpvalueLoadProp) + { + RegisterA64 temp1 = regs.allocTemp(KindA64::x); + RegisterA64 temp2 = regs.allocTemp(KindA64::x); + + // UpVal* + build.ldr(temp1, mem(rClosure, offsetof(Closure, l.uprefs) + sizeof(TValue) * vmUpvalueOp(inst.a) + offsetof(TValue, value.gc))); + + build.ldr(temp2, mem(temp1, offsetof(UpVal, v))); + build.str(regOp(inst.b), temp2); + + if (inst.c.kind == IrOpKind::Undef || isGCO(tagOp(inst.c))) + { + RegisterA64 value = regOp(inst.b); - // UpVal* - build.ldr(temp1, mem(rClosure, offsetof(Closure, l.uprefs) + sizeof(TValue) * vmUpvalueOp(inst.a) + offsetof(TValue, value.gc))); + Label skip; + checkObjectBarrierConditions(temp1, temp2, value, inst.b, inst.c.kind == IrOpKind::Undef ? -1 : tagOp(inst.c), skip); - build.ldr(temp2, mem(temp1, offsetof(UpVal, v))); - build.ldr(temp3, mem(rBase, vmRegOp(inst.b) * sizeof(TValue))); - build.str(temp3, temp2); + size_t spills = regs.spill(index, {temp1, value}); - if (inst.c.kind == IrOpKind::Undef || isGCO(tagOp(inst.c))) + build.mov(x1, temp1); + build.mov(x0, rState); + build.fmov(x2, castReg(KindA64::d, value)); + build.ldr(x3, mem(rNativeContext, offsetof(NativeContext, luaC_barrierf))); + build.blr(x3); + + regs.restore(spills); // need to restore before skip so that registers are in a consistent state + + // note: no emitUpdateBase necessary because luaC_ barriers do not reallocate stack + build.setLabel(skip); + } + } + else { - Label skip; - checkObjectBarrierConditions(build, temp1, temp2, inst.b, inst.c.kind == IrOpKind::Undef ? -1 : tagOp(inst.c), skip); + RegisterA64 temp1 = regs.allocTemp(KindA64::x); + RegisterA64 temp2 = regs.allocTemp(KindA64::x); + RegisterA64 temp3 = regs.allocTemp(KindA64::q); - size_t spills = regs.spill(index, {temp1}); + // UpVal* + build.ldr(temp1, mem(rClosure, offsetof(Closure, l.uprefs) + sizeof(TValue) * vmUpvalueOp(inst.a) + offsetof(TValue, value.gc))); - build.mov(x1, temp1); - build.mov(x0, rState); - build.ldr(x2, mem(rBase, vmRegOp(inst.b) * sizeof(TValue) + offsetof(TValue, value))); - build.ldr(x3, mem(rNativeContext, offsetof(NativeContext, luaC_barrierf))); - build.blr(x3); + build.ldr(temp2, mem(temp1, offsetof(UpVal, v))); + build.ldr(temp3, mem(rBase, vmRegOp(inst.b) * sizeof(TValue))); + build.str(temp3, temp2); - regs.restore(spills); // need to restore before skip so that registers are in a consistent state + if (inst.c.kind == IrOpKind::Undef || isGCO(tagOp(inst.c))) + { + Label skip; + checkObjectBarrierConditions_DEPRECATED(build, temp1, temp2, inst.b, inst.c.kind == IrOpKind::Undef ? -1 : tagOp(inst.c), skip); - // note: no emitUpdateBase necessary because luaC_ barriers do not reallocate stack - build.setLabel(skip); + size_t spills = regs.spill(index, {temp1}); + + build.mov(x1, temp1); + build.mov(x0, rState); + build.ldr(x2, mem(rBase, vmRegOp(inst.b) * sizeof(TValue) + offsetof(TValue, value))); + build.ldr(x3, mem(rNativeContext, offsetof(NativeContext, luaC_barrierf))); + build.blr(x3); + + regs.restore(spills); // need to restore before skip so that registers are in a consistent state + + // note: no emitUpdateBase necessary because luaC_ barriers do not reallocate stack + build.setLabel(skip); + } } break; } @@ -1909,6 +2118,9 @@ void IrLoweringA64::lowerInst(IrInst& inst, uint32_t index, const IrBlock& next) if (inst.b.kind == IrOpKind::Inst) { + if (FFlag::LuauCodegenNumIntFolds2) + CODEGEN_ASSERT(!producesDirtyHighRegisterBits(function.instOp(inst.b).cmd)); // Ensure that high register bits are cleared + if (accessSize == 1) { // fails if offset >= len @@ -2008,7 +2220,10 @@ void IrLoweringA64::lowerInst(IrInst& inst, uint32_t index, const IrBlock& next) RegisterA64 temp = regs.allocTemp(KindA64::x); Label skip; - checkObjectBarrierConditions(build, regOp(inst.a), temp, inst.b, inst.c.kind == IrOpKind::Undef ? -1 : tagOp(inst.c), skip); + if (FFlag::LuauCodegenUpvalueLoadProp) + checkObjectBarrierConditions(regOp(inst.a), temp, noreg, inst.b, inst.c.kind == IrOpKind::Undef ? -1 : tagOp(inst.c), skip); + else + checkObjectBarrierConditions_DEPRECATED(build, regOp(inst.a), temp, inst.b, inst.c.kind == IrOpKind::Undef ? -1 : tagOp(inst.c), skip); RegisterA64 reg = regOp(inst.a); // note: we need to call regOp before spill so that we don't do redundant reloads size_t spills = regs.spill(index, {reg}); @@ -2052,7 +2267,10 @@ void IrLoweringA64::lowerInst(IrInst& inst, uint32_t index, const IrBlock& next) RegisterA64 temp = regs.allocTemp(KindA64::x); Label skip; - checkObjectBarrierConditions(build, regOp(inst.a), temp, inst.b, inst.c.kind == IrOpKind::Undef ? -1 : tagOp(inst.c), skip); + if (FFlag::LuauCodegenUpvalueLoadProp) + checkObjectBarrierConditions(regOp(inst.a), temp, noreg, inst.b, inst.c.kind == IrOpKind::Undef ? -1 : tagOp(inst.c), skip); + else + checkObjectBarrierConditions_DEPRECATED(build, regOp(inst.a), temp, inst.b, inst.c.kind == IrOpKind::Undef ? -1 : tagOp(inst.c), skip); RegisterA64 reg = regOp(inst.a); // note: we need to call regOp before spill so that we don't do redundant reloads AddressA64 addr = tempAddr(inst.b, offsetof(TValue, value)); @@ -2666,23 +2884,43 @@ void IrLoweringA64::lowerInst(IrInst& inst, uint32_t index, const IrBlock& next) case IrCmd::BUFFER_READF32: { - inst.regA64 = regs.allocReg(KindA64::d, index); - RegisterA64 temp = castReg(KindA64::s, inst.regA64); // safe to alias a fresh register - AddressA64 addr = tempAddrBuffer(inst.a, inst.b, inst.c.kind == IrOpKind::None ? LUA_TBUFFER : tagOp(inst.c)); + if (FFlag::LuauCodegenSplitFloat) + { + inst.regA64 = regs.allocReg(KindA64::s, index); + AddressA64 addr = tempAddrBuffer(inst.a, inst.b, inst.c.kind == IrOpKind::None ? LUA_TBUFFER : tagOp(inst.c)); - build.ldr(temp, addr); - build.fcvt(inst.regA64, temp); + build.ldr(inst.regA64, addr); + } + else + { + inst.regA64 = regs.allocReg(KindA64::d, index); + RegisterA64 temp = castReg(KindA64::s, inst.regA64); // safe to alias a fresh register + AddressA64 addr = tempAddrBuffer(inst.a, inst.b, inst.c.kind == IrOpKind::None ? LUA_TBUFFER : tagOp(inst.c)); + + build.ldr(temp, addr); + build.fcvt(inst.regA64, temp); + } break; } case IrCmd::BUFFER_WRITEF32: { - RegisterA64 temp1 = tempDouble(inst.c); - RegisterA64 temp2 = regs.allocTemp(KindA64::s); - AddressA64 addr = tempAddrBuffer(inst.a, inst.b, inst.d.kind == IrOpKind::None ? LUA_TBUFFER : tagOp(inst.d)); + if (FFlag::LuauCodegenSplitFloat) + { + RegisterA64 temp = tempFloat(inst.c); + AddressA64 addr = tempAddrBuffer(inst.a, inst.b, inst.d.kind == IrOpKind::None ? LUA_TBUFFER : tagOp(inst.d)); + + build.str(temp, addr); + } + else + { + RegisterA64 temp1 = tempDouble(inst.c); + RegisterA64 temp2 = regs.allocTemp(KindA64::s); + AddressA64 addr = tempAddrBuffer(inst.a, inst.b, inst.d.kind == IrOpKind::None ? LUA_TBUFFER : tagOp(inst.d)); - build.fcvt(temp2, temp1); - build.str(temp2, addr); + build.fcvt(temp2, temp1); + build.str(temp2, addr); + } break; } @@ -2721,7 +2959,7 @@ void IrLoweringA64::finishBlock(const IrBlock& curr, const IrBlock& next) { // If we have spills remaining, we have to immediately lower the successor block for (uint32_t predIdx : predecessors(function.cfg, function.getBlockIndex(next))) - CODEGEN_ASSERT(predIdx == function.getBlockIndex(curr)); + CODEGEN_ASSERT(predIdx == function.getBlockIndex(curr) || function.blocks[predIdx].kind == IrBlockKind::Dead); // And the next block cannot be a join block in cfg CODEGEN_ASSERT(next.useCount == 1); @@ -2828,6 +3066,49 @@ void IrLoweringA64::checkSafeEnv(IrOp target, const IrBlock& next) finalizeTargetLabel(target, fresh); } +void IrLoweringA64::checkObjectBarrierConditions(RegisterA64 object, RegisterA64 temp, RegisterA64 ra, IrOp raOp, int ratag, Label& skip) +{ + CODEGEN_ASSERT(FFlag::LuauCodegenUpvalueLoadProp); + + RegisterA64 tempw = castReg(KindA64::w, temp); + + // iscollectable(ra) + if (ratag == -1 || !isGCO(ratag)) + { + if (raOp.kind == IrOpKind::Inst) + { + build.umov_4s(tempw, ra, 3); + } + else + { + AddressA64 addr = tempAddr(raOp, offsetof(TValue, tt), temp); + build.ldr(tempw, addr); + } + + build.cmp(tempw, LUA_TSTRING); + build.b(ConditionA64::Less, skip); + } + + // isblack(obj2gco(o)) + build.ldrb(tempw, mem(object, offsetof(GCheader, marked))); + build.tbz(tempw, BLACKBIT, skip); + + // iswhite(gcvalue(ra)) + if (raOp.kind == IrOpKind::Inst) + { + build.fmov(temp, castReg(KindA64::d, ra)); + } + else + { + AddressA64 addr = tempAddr(raOp, offsetof(TValue, value), temp); + build.ldr(temp, addr); + } + + build.ldrb(tempw, mem(temp, offsetof(GCheader, marked))); + build.tst(tempw, bit2mask(WHITE0BIT, WHITE1BIT)); + build.b(ConditionA64::Equal, skip); // Equal = Zero after tst +} + RegisterA64 IrLoweringA64::tempDouble(IrOp op) { if (op.kind == IrOpKind::Inst) @@ -2836,7 +3117,7 @@ RegisterA64 IrLoweringA64::tempDouble(IrOp op) { double val = doubleOp(op); - if (AssemblyBuilderA64::isFmovSupported(val)) + if (AssemblyBuilderA64::isFmovSupportedFp64(val)) { RegisterA64 temp = regs.allocTemp(KindA64::d); build.fmov(temp, val); @@ -2876,6 +3157,51 @@ RegisterA64 IrLoweringA64::tempDouble(IrOp op) } } +RegisterA64 IrLoweringA64::tempFloat(IrOp op) +{ + if (op.kind == IrOpKind::Inst) + return regOp(op); + else if (op.kind == IrOpKind::Constant) + { + float val = float(doubleOp(op)); + + if (AssemblyBuilderA64::isFmovSupportedFp32(val)) + { + RegisterA64 temp = regs.allocTemp(KindA64::s); + build.fmov(temp, val); + return temp; + } + else + { + RegisterA64 temp = regs.allocTemp(KindA64::s); + + uint32_t vali = getFloatBits(val); + + if ((vali & 0xffff) == 0) + { + RegisterA64 temp2 = regs.allocTemp(KindA64::w); + + build.movz(temp2, uint16_t(vali >> 16), 16); + build.fmov(temp, temp2); + } + else + { + RegisterA64 temp2 = regs.allocTemp(KindA64::x); + + build.adr(temp2, val); + build.ldr(temp, temp2); + } + + return temp; + } + } + else + { + CODEGEN_ASSERT(!"Unsupported instruction form"); + return noreg; + } +} + RegisterA64 IrLoweringA64::tempInt(IrOp op) { if (op.kind == IrOpKind::Inst) @@ -2910,7 +3236,7 @@ RegisterA64 IrLoweringA64::tempUint(IrOp op) } } -AddressA64 IrLoweringA64::tempAddr(IrOp op, int offset) +AddressA64 IrLoweringA64::tempAddr(IrOp op, int offset, RegisterA64 tempStorage) { // This is needed to tighten the bounds checks in the VmConst case below CODEGEN_ASSERT(offset % 4 == 0); @@ -2918,7 +3244,9 @@ AddressA64 IrLoweringA64::tempAddr(IrOp op, int offset) CODEGEN_ASSERT(offset >= 0 && unsigned(offset / 4) <= AssemblyBuilderA64::kMaxImmediate); if (op.kind == IrOpKind::VmReg) + { return mem(rBase, vmRegOp(op) * sizeof(TValue) + offset); + } else if (op.kind == IrOpKind::VmConst) { size_t constantOffset = vmConstOp(op) * sizeof(TValue) + offset; @@ -2927,15 +3255,30 @@ AddressA64 IrLoweringA64::tempAddr(IrOp op, int offset) if (constantOffset / 4 <= AddressA64::kMaxOffset) return mem(rConstants, int(constantOffset)); - RegisterA64 temp = regs.allocTemp(KindA64::x); + if (FFlag::LuauCodegenUpvalueLoadProp) + { + RegisterA64 temp = tempStorage == noreg ? regs.allocTemp(KindA64::x) : tempStorage; + CODEGEN_ASSERT(temp.kind == KindA64::x && "temp storage, when provided, must be an 'x' register"); - emitAddOffset(build, temp, rConstants, constantOffset); - return temp; + emitAddOffset(build, temp, rConstants, constantOffset); + return temp; + } + else + { + CODEGEN_ASSERT(tempStorage == noreg); + + RegisterA64 temp = regs.allocTemp(KindA64::x); + + emitAddOffset(build, temp, rConstants, constantOffset); + return temp; + } } // If we have a register, we assume it's a pointer to TValue - // We might introduce explicit operand types in the future to make this more robust else if (op.kind == IrOpKind::Inst) + { + CODEGEN_ASSERT(getCmdValueKind(function.instOp(op).cmd) == IrValueKind::Pointer); return mem(regOp(op), offset); + } else { CODEGEN_ASSERT(!"Unsupported instruction form"); @@ -2950,6 +3293,9 @@ AddressA64 IrLoweringA64::tempAddrBuffer(IrOp bufferOp, IrOp indexOp, uint8_t ta if (indexOp.kind == IrOpKind::Inst) { + if (FFlag::LuauCodegenNumIntFolds2) + CODEGEN_ASSERT(!producesDirtyHighRegisterBits(function.instOp(indexOp).cmd)); + RegisterA64 temp = regs.allocTemp(KindA64::x); build.add(temp, regOp(bufferOp), regOp(indexOp)); // implicit uxtw return mem(temp, dataOffset); diff --git a/CodeGen/src/IrLoweringA64.h b/CodeGen/src/IrLoweringA64.h index ce0139498..5275061a0 100644 --- a/CodeGen/src/IrLoweringA64.h +++ b/CodeGen/src/IrLoweringA64.h @@ -39,13 +39,15 @@ struct IrLoweringA64 void finalizeTargetLabel(IrOp op, Label& fresh); void checkSafeEnv(IrOp target, const IrBlock& next); + void checkObjectBarrierConditions(RegisterA64 object, RegisterA64 temp, RegisterA64 ra, IrOp raOp, int ratag, Label& skip); // Operand data build helpers // May emit data/address synthesis instructions RegisterA64 tempDouble(IrOp op); + RegisterA64 tempFloat(IrOp op); RegisterA64 tempInt(IrOp op); RegisterA64 tempUint(IrOp op); - AddressA64 tempAddr(IrOp op, int offset); + AddressA64 tempAddr(IrOp op, int offset, RegisterA64 tempStorage = noreg); // Existing temporary register can be provided AddressA64 tempAddrBuffer(IrOp bufferOp, IrOp indexOp, uint8_t tag); // May emit restore instructions diff --git a/CodeGen/src/IrLoweringX64.cpp b/CodeGen/src/IrLoweringX64.cpp index 92d45236e..24e1946a2 100644 --- a/CodeGen/src/IrLoweringX64.cpp +++ b/CodeGen/src/IrLoweringX64.cpp @@ -18,6 +18,9 @@ LUAU_FASTFLAG(LuauCodegenBlockSafeEnv) LUAU_FASTFLAG(LuauCodegenIntegerAddSub) +LUAU_FASTFLAG(LuauCodegenUpvalueLoadProp) +LUAU_FASTFLAG(LuauCodegenNumIntFolds2) +LUAU_FASTFLAG(LuauCodegenSplitFloat) namespace Luau { @@ -98,16 +101,30 @@ void IrLoweringX64::lowerInst(IrInst& inst, uint32_t index, const IrBlock& next) build.mov(inst.regX64, luauRegValueInt(vmRegOp(inst.a))); break; case IrCmd::LOAD_FLOAT: - inst.regX64 = regs.allocReg(SizeX64::xmmword, index); + if (FFlag::LuauCodegenSplitFloat) + { + inst.regX64 = regs.allocReg(SizeX64::xmmword, index); - if (inst.a.kind == IrOpKind::VmReg) - build.vcvtss2sd(inst.regX64, inst.regX64, dword[rBase + vmRegOp(inst.a) * sizeof(TValue) + offsetof(TValue, value) + intOp(inst.b)]); - else if (inst.a.kind == IrOpKind::VmConst) - build.vcvtss2sd( - inst.regX64, inst.regX64, dword[rConstants + vmConstOp(inst.a) * sizeof(TValue) + offsetof(TValue, value) + intOp(inst.b)] - ); + if (inst.a.kind == IrOpKind::VmReg) + build.vmovss(inst.regX64, dword[rBase + vmRegOp(inst.a) * sizeof(TValue) + offsetof(TValue, value) + intOp(inst.b)]); + else if (inst.a.kind == IrOpKind::VmConst) + build.vmovss(inst.regX64, dword[rConstants + vmConstOp(inst.a) * sizeof(TValue) + offsetof(TValue, value) + intOp(inst.b)]); + else + CODEGEN_ASSERT(!"Unsupported instruction form"); + } else - CODEGEN_ASSERT(!"Unsupported instruction form"); + { + inst.regX64 = regs.allocReg(SizeX64::xmmword, index); + + if (inst.a.kind == IrOpKind::VmReg) + build.vcvtss2sd(inst.regX64, inst.regX64, dword[rBase + vmRegOp(inst.a) * sizeof(TValue) + offsetof(TValue, value) + intOp(inst.b)]); + else if (inst.a.kind == IrOpKind::VmConst) + build.vcvtss2sd( + inst.regX64, inst.regX64, dword[rConstants + vmConstOp(inst.a) * sizeof(TValue) + offsetof(TValue, value) + intOp(inst.b)] + ); + else + CODEGEN_ASSERT(!"Unsupported instruction form"); + } break; case IrCmd::LOAD_TVALUE: { @@ -277,9 +294,18 @@ void IrLoweringX64::lowerInst(IrInst& inst, uint32_t index, const IrBlock& next) CODEGEN_ASSERT(!"Unsupported instruction form"); break; case IrCmd::STORE_VECTOR: - storeDoubleAsFloat(luauRegValueVector(vmRegOp(inst.a), 0), inst.b); - storeDoubleAsFloat(luauRegValueVector(vmRegOp(inst.a), 1), inst.c); - storeDoubleAsFloat(luauRegValueVector(vmRegOp(inst.a), 2), inst.d); + if (FFlag::LuauCodegenSplitFloat) + { + storeFloat(luauRegValueVector(vmRegOp(inst.a), 0), inst.b); + storeFloat(luauRegValueVector(vmRegOp(inst.a), 1), inst.c); + storeFloat(luauRegValueVector(vmRegOp(inst.a), 2), inst.d); + } + else + { + storeDoubleAsFloat(luauRegValueVector(vmRegOp(inst.a), 0), inst.b); + storeDoubleAsFloat(luauRegValueVector(vmRegOp(inst.a), 1), inst.c); + storeDoubleAsFloat(luauRegValueVector(vmRegOp(inst.a), 2), inst.d); + } if (inst.e.kind != IrOpKind::None) build.mov(luauRegTag(vmRegOp(inst.a)), tagOp(inst.e)); @@ -414,6 +440,16 @@ void IrLoweringX64::lowerInst(IrInst& inst, uint32_t index, const IrBlock& next) build.lea(inst.regX64, addr[regOp(inst.a) - intOp(inst.b)]); } break; + case IrCmd::SEXTI8_INT: + inst.regX64 = regs.allocRegOrReuse(SizeX64::dword, index, {inst.a}); + + build.movsx(inst.regX64, byteReg(regOp(inst.a))); + break; + case IrCmd::SEXTI16_INT: + inst.regX64 = regs.allocRegOrReuse(SizeX64::dword, index, {inst.a}); + + build.movsx(inst.regX64, wordReg(regOp(inst.a))); + break; case IrCmd::ADD_NUM: inst.regX64 = regs.allocRegOrReuse(SizeX64::xmmword, index, {inst.a, inst.b}); @@ -731,6 +767,37 @@ void IrLoweringX64::lowerInst(IrInst& inst, uint32_t index, const IrBlock& next) break; } + case IrCmd::SELECT_IF_TRUTHY: + { + inst.regX64 = regs.allocReg(SizeX64::xmmword, index); // No reuse since multiple inputs can be shared + + // Place lhs as the result, we will overwrite it with rhs if 'A' is falsy later + build.vmovaps(inst.regX64, regOp(inst.b)); + + // Get rhs register early, so a potential restore happens on both sides of a conditional control flow + RegisterX64 c = regOp(inst.c); + + ScopedRegX64 tmp{regs, SizeX64::dword}; + Label saveRhs, exit; + + // Check tag first + build.vpextrd(tmp.reg, regOp(inst.a), 3); + build.cmp(tmp.reg, LUA_TBOOLEAN); + + build.jcc(ConditionX64::Below, saveRhs); // rhs if 'A' is nil + build.jcc(ConditionX64::Above, exit); // Keep lhs if 'A' is not a boolean + + // Check the boolean value + build.vpextrd(tmp.reg, regOp(inst.a), 0); + build.test(tmp.reg, tmp.reg); + build.jcc(ConditionX64::NotZero, exit); // Keep lhs if 'A' is true + + build.setLabel(saveRhs); + build.vmovaps(inst.regX64, c); + + build.setLabel(exit); + break; + } case IrCmd::ADD_VEC: { inst.regX64 = regs.allocRegOrReuse(SizeX64::xmmword, index, {inst.a, inst.b}); @@ -827,7 +894,19 @@ void IrLoweringX64::lowerInst(IrInst& inst, uint32_t index, const IrBlock& next) RegisterX64 tmpb = (inst.a == inst.b) ? tmpa : vecOp(inst.b, tmp2); build.vdpps(inst.regX64, tmpa, tmpb, 0x71); // 7 = 0b0111, sum first 3 products into first float - build.vcvtss2sd(inst.regX64, inst.regX64, inst.regX64); + + if (!FFlag::LuauCodegenSplitFloat) + build.vcvtss2sd(inst.regX64, inst.regX64, inst.regX64); + break; + } + case IrCmd::EXTRACT_VEC: + { + inst.regX64 = regs.allocRegOrReuse(SizeX64::xmmword, index, {inst.a}); + + build.vpshufps(inst.regX64, regOp(inst.a), regOp(inst.a), intOp(inst.b)); + + if (!FFlag::LuauCodegenSplitFloat) + build.vcvtss2sd(inst.regX64, inst.regX64, inst.regX64); break; } case IrCmd::NOT_ANY: @@ -920,6 +999,7 @@ void IrLoweringX64::lowerInst(IrInst& inst, uint32_t index, const IrBlock& next) emitUpdateBase(build); inst.regX64 = regs.takeReg(eax, index); + // Skipping high register bits clear, only consumer is JUMP_CMP_INT which doesn't read them break; } case IrCmd::CMP_TAG: @@ -1140,7 +1220,11 @@ void IrLoweringX64::lowerInst(IrInst& inst, uint32_t index, const IrBlock& next) IrCallWrapperX64 callWrap(regs, build, index); callWrap.addArgument(SizeX64::qword, regOp(inst.a), inst.a); callWrap.call(qword[rNativeContext + offsetof(NativeContext, luaH_getn)]); + inst.regX64 = regs.takeReg(eax, index); + + if (FFlag::LuauCodegenNumIntFolds2) + build.mov(inst.regX64, inst.regX64); // Ensure high register bits are cleared break; } case IrCmd::TABLE_SETNUM: @@ -1263,6 +1347,18 @@ void IrLoweringX64::lowerInst(IrInst& inst, uint32_t index, const IrBlock& next) // Note: we perform 'uint64_t = (long long)double' for consistency with C++ code build.vcvttsd2si(qwordReg(inst.regX64), memRegDoubleOp(inst.a)); break; + + case IrCmd::FLOAT_TO_NUM: + inst.regX64 = regs.allocRegOrReuse(SizeX64::xmmword, index, {inst.a}); + + build.vcvtss2sd(inst.regX64, inst.regX64, memRegDoubleOp(inst.a)); + break; + + case IrCmd::NUM_TO_FLOAT: + inst.regX64 = regs.allocRegOrReuse(SizeX64::xmmword, index, {inst.a}); + + build.vcvtsd2ss(inst.regX64, inst.regX64, memRegDoubleOp(inst.a)); + break; case IrCmd::NUM_TO_VEC: inst.regX64 = regs.allocReg(SizeX64::xmmword, index); @@ -1286,6 +1382,12 @@ void IrLoweringX64::lowerInst(IrInst& inst, uint32_t index, const IrBlock& next) build.vpinsrd(inst.regX64, regOp(inst.a), build.i32(LUA_TVECTOR), 3); break; + case IrCmd::TRUNCATE_UINT: + inst.regX64 = regs.allocRegOrReuse(SizeX64::dword, index, {inst.a}); + + // Might generate mov with the same source and destination register which is not a no-op + build.mov(inst.regX64, regOp(inst.a)); + break; case IrCmd::ADJUST_STACK_TO_REG: { ScopedRegX64 tmp{regs, SizeX64::qword}; @@ -1394,6 +1496,7 @@ void IrLoweringX64::lowerInst(IrInst& inst, uint32_t index, const IrBlock& next) callWrap.call(func.release()); inst.regX64 = regs.takeReg(eax, index); // Result of a builtin call is returned in eax + // Skipping high register bits clear, only consumer is CHECK_FASTCALL_RES which doesn't read them break; } case IrCmd::CHECK_FASTCALL_RES: @@ -1493,25 +1596,50 @@ void IrLoweringX64::lowerInst(IrInst& inst, uint32_t index, const IrBlock& next) } case IrCmd::GET_UPVALUE: { - ScopedRegX64 tmp1{regs, SizeX64::qword}; - ScopedRegX64 tmp2{regs, SizeX64::xmmword}; + if (FFlag::LuauCodegenUpvalueLoadProp) + { + inst.regX64 = regs.allocReg(SizeX64::xmmword, index); - build.mov(tmp1.reg, sClosure); - build.add(tmp1.reg, offsetof(Closure, l.uprefs) + sizeof(TValue) * vmUpvalueOp(inst.b)); + ScopedRegX64 tmp1{regs, SizeX64::qword}; - // uprefs[] is either an actual value, or it points to UpVal object which has a pointer to value - Label skip; - build.cmp(dword[tmp1.reg + offsetof(TValue, tt)], LUA_TUPVAL); - build.jcc(ConditionX64::NotEqual, skip); + build.mov(tmp1.reg, sClosure); + build.add(tmp1.reg, offsetof(Closure, l.uprefs) + sizeof(TValue) * vmUpvalueOp(inst.a)); - // UpVal.v points to the value (either on stack, or on heap inside each UpVal, but we can deref it unconditionally) - build.mov(tmp1.reg, qword[tmp1.reg + offsetof(TValue, value.gc)]); - build.mov(tmp1.reg, qword[tmp1.reg + offsetof(UpVal, v)]); + // uprefs[] is either an actual value, or it points to UpVal object which has a pointer to value + Label skip; + build.cmp(dword[tmp1.reg + offsetof(TValue, tt)], LUA_TUPVAL); + build.jcc(ConditionX64::NotEqual, skip); - build.setLabel(skip); + // UpVal.v points to the value (either on stack, or on heap inside each UpVal, but we can deref it unconditionally) + build.mov(tmp1.reg, qword[tmp1.reg + offsetof(TValue, value.gc)]); + build.mov(tmp1.reg, qword[tmp1.reg + offsetof(UpVal, v)]); - build.vmovups(tmp2.reg, xmmword[tmp1.reg]); - build.vmovups(luauReg(vmRegOp(inst.a)), tmp2.reg); + build.setLabel(skip); + + build.vmovups(inst.regX64, xmmword[tmp1.reg]); + } + else + { + ScopedRegX64 tmp1{regs, SizeX64::qword}; + ScopedRegX64 tmp2{regs, SizeX64::xmmword}; + + build.mov(tmp1.reg, sClosure); + build.add(tmp1.reg, offsetof(Closure, l.uprefs) + sizeof(TValue) * vmUpvalueOp(inst.b)); + + // uprefs[] is either an actual value, or it points to UpVal object which has a pointer to value + Label skip; + build.cmp(dword[tmp1.reg + offsetof(TValue, tt)], LUA_TUPVAL); + build.jcc(ConditionX64::NotEqual, skip); + + // UpVal.v points to the value (either on stack, or on heap inside each UpVal, but we can deref it unconditionally) + build.mov(tmp1.reg, qword[tmp1.reg + offsetof(TValue, value.gc)]); + build.mov(tmp1.reg, qword[tmp1.reg + offsetof(UpVal, v)]); + + build.setLabel(skip); + + build.vmovups(tmp2.reg, xmmword[tmp1.reg]); + build.vmovups(luauReg(vmRegOp(inst.a)), tmp2.reg); + } break; } case IrCmd::SET_UPVALUE: @@ -1524,16 +1652,29 @@ void IrLoweringX64::lowerInst(IrInst& inst, uint32_t index, const IrBlock& next) build.mov(tmp1.reg, qword[tmp2.reg + offsetof(UpVal, v)]); + if (FFlag::LuauCodegenUpvalueLoadProp) + { + build.vmovups(xmmword[tmp1.reg], regOp(inst.b)); + } + else { - ScopedRegX64 tmp3{regs, SizeX64::xmmword}; - build.vmovups(tmp3.reg, luauReg(vmRegOp(inst.b))); - build.vmovups(xmmword[tmp1.reg], tmp3.reg); + // If LuauCodegenUpvalueLoadProp is removed as false, this scope has to be preserved! + { + ScopedRegX64 tmp3{regs, SizeX64::xmmword}; + build.vmovups(tmp3.reg, luauReg(vmRegOp(inst.b))); + build.vmovups(xmmword[tmp1.reg], tmp3.reg); + } } tmp1.free(); if (inst.c.kind == IrOpKind::Undef || isGCO(tagOp(inst.c))) - callBarrierObject(regs, build, tmp2.release(), {}, inst.b, inst.c.kind == IrOpKind::Undef ? -1 : tagOp(inst.c)); + { + if (FFlag::LuauCodegenUpvalueLoadProp) + callBarrierObject(regs, build, tmp2.release(), {}, regOp(inst.b), inst.b, inst.c.kind == IrOpKind::Undef ? -1 : tagOp(inst.c)); + else + callBarrierObject_DEPRECATED(regs, build, tmp2.release(), {}, inst.b, inst.c.kind == IrOpKind::Undef ? -1 : tagOp(inst.c)); + } break; } case IrCmd::CHECK_TAG: @@ -1670,6 +1811,9 @@ void IrLoweringX64::lowerInst(IrInst& inst, uint32_t index, const IrBlock& next) if (inst.b.kind == IrOpKind::Inst) { + if (FFlag::LuauCodegenNumIntFolds2) + CODEGEN_ASSERT(!producesDirtyHighRegisterBits(function.instOp(inst.b).cmd)); // Ensure that high register bits are cleared + if (accessSize == 1) { // Simpler check for a single byte access @@ -1685,16 +1829,23 @@ void IrLoweringX64::lowerInst(IrInst& inst, uint32_t index, const IrBlock& next) // Access size is then added using a 64 bit addition // This will make sure that addition will not wrap around for values like 0xffffffff - if (IrCmd source = function.instOp(inst.b).cmd; source == IrCmd::NUM_TO_INT) + if (FFlag::LuauCodegenNumIntFolds2) { - // When previous operation is a conversion to an integer (common case), it is guaranteed to have high register bits cleared build.lea(tmp1.reg, addr[qwordReg(regOp(inst.b)) + accessSize]); } else { - // When the source of the index is unknown, it could contain garbage in the high bits, so we zero-extend it explicitly - build.mov(dwordReg(tmp1.reg), regOp(inst.b)); - build.add(tmp1.reg, accessSize); + if (IrCmd source = function.instOp(inst.b).cmd; source == IrCmd::NUM_TO_INT) + { + // When previous operation is a conversion to an integer (common case), it is guaranteed to have high register bits cleared + build.lea(tmp1.reg, addr[qwordReg(regOp(inst.b)) + accessSize]); + } + else + { + // When the source of the index is unknown, it could contain garbage in the high bits, so we zero-extend it explicitly + build.mov(dwordReg(tmp1.reg), regOp(inst.b)); + build.add(tmp1.reg, accessSize); + } } build.mov(tmp2.reg, dword[regOp(inst.a) + offsetof(Buffer, len)]); @@ -1753,7 +1904,10 @@ void IrLoweringX64::lowerInst(IrInst& inst, uint32_t index, const IrBlock& next) callStepGc(regs, build); break; case IrCmd::BARRIER_OBJ: - callBarrierObject(regs, build, regOp(inst.a), inst.a, inst.b, inst.c.kind == IrOpKind::Undef ? -1 : tagOp(inst.c)); + if (FFlag::LuauCodegenUpvalueLoadProp) + callBarrierObject(regs, build, regOp(inst.a), inst.a, noreg, inst.b, inst.c.kind == IrOpKind::Undef ? -1 : tagOp(inst.c)); + else + callBarrierObject_DEPRECATED(regs, build, regOp(inst.a), inst.a, inst.b, inst.c.kind == IrOpKind::Undef ? -1 : tagOp(inst.c)); break; case IrCmd::BARRIER_TABLE_BACK: callBarrierTableFast(regs, build, regOp(inst.a), inst.a); @@ -1764,7 +1918,10 @@ void IrLoweringX64::lowerInst(IrInst& inst, uint32_t index, const IrBlock& next) ScopedRegX64 tmp{regs, SizeX64::qword}; - checkObjectBarrierConditions(build, tmp.reg, regOp(inst.a), inst.b, inst.c.kind == IrOpKind::Undef ? -1 : tagOp(inst.c), skip); + if (FFlag::LuauCodegenUpvalueLoadProp) + checkObjectBarrierConditions(build, tmp.reg, regOp(inst.a), noreg, inst.b, inst.c.kind == IrOpKind::Undef ? -1 : tagOp(inst.c), skip); + else + checkObjectBarrierConditions_DEPRECATED(build, tmp.reg, regOp(inst.a), inst.b, inst.c.kind == IrOpKind::Undef ? -1 : tagOp(inst.c), skip); { ScopedSpills spillGuard(regs); @@ -2325,13 +2482,31 @@ void IrLoweringX64::lowerInst(IrInst& inst, uint32_t index, const IrBlock& next) } case IrCmd::BUFFER_READF32: - inst.regX64 = regs.allocReg(SizeX64::xmmword, index); + if (FFlag::LuauCodegenSplitFloat) + { + inst.regX64 = regs.allocReg(SizeX64::xmmword, index); - build.vcvtss2sd(inst.regX64, inst.regX64, dword[bufferAddrOp(inst.a, inst.b, inst.c.kind == IrOpKind::None ? LUA_TBUFFER : tagOp(inst.c))]); + build.vmovss(inst.regX64, dword[bufferAddrOp(inst.a, inst.b, inst.c.kind == IrOpKind::None ? LUA_TBUFFER : tagOp(inst.c))]); + } + else + { + inst.regX64 = regs.allocReg(SizeX64::xmmword, index); + + build.vcvtss2sd( + inst.regX64, inst.regX64, dword[bufferAddrOp(inst.a, inst.b, inst.c.kind == IrOpKind::None ? LUA_TBUFFER : tagOp(inst.c))] + ); + } break; case IrCmd::BUFFER_WRITEF32: - storeDoubleAsFloat(dword[bufferAddrOp(inst.a, inst.b, inst.d.kind == IrOpKind::None ? LUA_TBUFFER : tagOp(inst.d))], inst.c); + if (FFlag::LuauCodegenSplitFloat) + { + storeFloat(dword[bufferAddrOp(inst.a, inst.b, inst.d.kind == IrOpKind::None ? LUA_TBUFFER : tagOp(inst.d))], inst.c); + } + else + { + storeDoubleAsFloat(dword[bufferAddrOp(inst.a, inst.b, inst.d.kind == IrOpKind::None ? LUA_TBUFFER : tagOp(inst.d))], inst.c); + } break; case IrCmd::BUFFER_READF64: @@ -2510,6 +2685,25 @@ void IrLoweringX64::jumpOrAbortOnUndef(IrOp target, const IrBlock& next) jumpOrAbortOnUndef(ConditionX64::Count, target, next); } +void IrLoweringX64::storeFloat(OperandX64 dst, IrOp src) +{ + if (src.kind == IrOpKind::Constant) + { + ScopedRegX64 tmp{regs, SizeX64::xmmword}; + build.vmovss(tmp.reg, build.f32(float(doubleOp(src)))); + build.vmovss(dst, tmp.reg); + } + else if (src.kind == IrOpKind::Inst) + { + CODEGEN_ASSERT(getCmdValueKind(function.instOp(src).cmd) == IrValueKind::Float); + build.vmovss(dst, regOp(src)); + } + else + { + CODEGEN_ASSERT(!"Unsupported instruction form"); + } +} + void IrLoweringX64::storeDoubleAsFloat(OperandX64 dst, IrOp src) { ScopedRegX64 tmp{regs, SizeX64::xmmword}; @@ -2610,9 +2804,16 @@ OperandX64 IrLoweringX64::bufferAddrOp(IrOp bufferOp, IrOp indexOp, uint8_t tag) int dataOffset = tag == LUA_TBUFFER ? offsetof(Buffer, data) : offsetof(Udata, data); if (indexOp.kind == IrOpKind::Inst) + { + if (FFlag::LuauCodegenNumIntFolds2) + CODEGEN_ASSERT(!producesDirtyHighRegisterBits(function.instOp(indexOp).cmd)); // Ensure that high register bits are cleared + return regOp(bufferOp) + qwordReg(regOp(indexOp)) + dataOffset; + } else if (indexOp.kind == IrOpKind::Constant) + { return regOp(bufferOp) + intOp(indexOp) + dataOffset; + } CODEGEN_ASSERT(!"Unsupported instruction form"); return noreg; diff --git a/CodeGen/src/IrLoweringX64.h b/CodeGen/src/IrLoweringX64.h index 212b61d2b..7ce47070f 100644 --- a/CodeGen/src/IrLoweringX64.h +++ b/CodeGen/src/IrLoweringX64.h @@ -43,6 +43,7 @@ struct IrLoweringX64 void jumpOrAbortOnUndef(ConditionX64 cond, IrOp target, const IrBlock& next); void jumpOrAbortOnUndef(IrOp target, const IrBlock& next); + void storeFloat(OperandX64 dst, IrOp src); void storeDoubleAsFloat(OperandX64 dst, IrOp src); void checkSafeEnv(IrOp target, const IrBlock& next); diff --git a/CodeGen/src/IrRegAllocA64.cpp b/CodeGen/src/IrRegAllocA64.cpp index 1e50b672c..dcb7c752b 100644 --- a/CodeGen/src/IrRegAllocA64.cpp +++ b/CodeGen/src/IrRegAllocA64.cpp @@ -12,6 +12,7 @@ LUAU_FASTFLAGVARIABLE(DebugCodegenChaosA64) LUAU_FASTFLAG(LuauCodegenChainedSpills) +LUAU_FASTFLAGVARIABLE(LuauCodegenSpillRestoreFreeTemp) namespace Luau { @@ -56,6 +57,8 @@ static int getReloadOffset_DEPRECATED(IrCmd cmd) { case IrValueKind::Unknown: case IrValueKind::None: + case IrValueKind::Float: + case IrValueKind::Count: CODEGEN_ASSERT(!"Invalid operand restore value kind"); break; case IrValueKind::Tag: @@ -98,6 +101,8 @@ static int getReloadOffset(IrValueKind kind) { case IrValueKind::Unknown: case IrValueKind::None: + case IrValueKind::Float: + case IrValueKind::Count: CODEGEN_ASSERT(!"Invalid operand restore value kind"); break; case IrValueKind::Tag: @@ -339,6 +344,18 @@ void IrRegAllocA64::freeLastUseRegs(const IrInst& inst, uint32_t index) checkOp(inst.g); } +void IrRegAllocA64::freeTemp(RegisterA64 reg) +{ + Set& set = getSet(reg.kind); + + CODEGEN_ASSERT((set.base & (1u << reg.index)) != 0); + CODEGEN_ASSERT((set.free & (1u << reg.index)) == 0); + CODEGEN_ASSERT((set.temp & (1u << reg.index)) != 0); + + set.free |= 1u << reg.index; + set.temp &= ~(1u << reg.index); +} + void IrRegAllocA64::freeTempRegs() { CODEGEN_ASSERT((gpr.free & gpr.temp) == 0); @@ -502,6 +519,12 @@ void IrRegAllocA64::restore(const IrRegAllocA64::Spill& s, RegisterA64 reg) build.fcvtzs(castReg(KindA64::x, reg), temp); // note: we don't use fcvtzu for consistency with C++ code else CODEGEN_ASSERT(!"re-materialization not supported for this conversion command"); + + if (FFlag::LuauCodegenSpillRestoreFreeTemp) + { + // Temporary might have taken a spot needed for other registers in spill restore process + freeTemp(temp); + } } else { diff --git a/CodeGen/src/IrRegAllocA64.h b/CodeGen/src/IrRegAllocA64.h index b5b02f277..bf0b80fdf 100644 --- a/CodeGen/src/IrRegAllocA64.h +++ b/CodeGen/src/IrRegAllocA64.h @@ -40,6 +40,7 @@ struct IrRegAllocA64 void freeLastUseReg(IrInst& target, uint32_t index); void freeLastUseRegs(const IrInst& inst, uint32_t index); + void freeTemp(RegisterA64 reg); void freeTempRegs(); // Spills all live registers that outlive current instruction; all allocated registers are assumed to be undefined diff --git a/CodeGen/src/IrRegAllocX64.cpp b/CodeGen/src/IrRegAllocX64.cpp index fab14e896..f262f459d 100644 --- a/CodeGen/src/IrRegAllocX64.cpp +++ b/CodeGen/src/IrRegAllocX64.cpp @@ -7,6 +7,8 @@ #include "EmitCommonX64.h" LUAU_FASTFLAG(LuauCodegenChainedSpills) +LUAU_FASTFLAG(LuauCodegenSplitFloat) +LUAU_FASTFLAGVARIABLE(LuauCodegenDwordSpillSlots) namespace Luau { @@ -15,6 +17,9 @@ namespace CodeGen namespace X64 { +static constexpr unsigned kValueDwordSize[] = {0, 0, 1, 1, 2, 1, 2, 4}; +static_assert(sizeof(kValueDwordSize) / sizeof(kValueDwordSize[0]) == size_t(IrValueKind::Count), "all kinds have to be covered"); + static const RegisterX64 kGprAllocOrder[] = {rax, rdx, rcx, rbx, rsi, rdi, r8, r9, r10, r11}; IrRegAllocX64::IrRegAllocX64(AssemblyBuilderX64& build, IrFunction& function, LoweringStats* stats) @@ -206,28 +211,56 @@ void IrRegAllocX64::preserve(IrInst& inst) { unsigned i = findSpillStackSlot(spill.valueKind); - if (spill.valueKind == IrValueKind::Tvalue) - build.vmovups(xmmword[sSpillArea + i * 8], inst.regX64); - else if (spill.valueKind == IrValueKind::Double) - build.vmovsd(qword[sSpillArea + i * 8], inst.regX64); - else if (spill.valueKind == IrValueKind::Pointer) - build.mov(qword[sSpillArea + i * 8], inst.regX64); - else if (spill.valueKind == IrValueKind::Tag || spill.valueKind == IrValueKind::Int) - build.mov(dword[sSpillArea + i * 8], inst.regX64); - else - CODEGEN_ASSERT(!"Unsupported value kind"); + if (FFlag::LuauCodegenDwordSpillSlots) + { + if (spill.valueKind == IrValueKind::Tvalue) + build.vmovups(xmmword[sSpillArea + i * 4], inst.regX64); + else if (spill.valueKind == IrValueKind::Double) + build.vmovsd(qword[sSpillArea + i * 4], inst.regX64); + else if (spill.valueKind == IrValueKind::Pointer) + build.mov(qword[sSpillArea + i * 4], inst.regX64); + else if (spill.valueKind == IrValueKind::Tag || spill.valueKind == IrValueKind::Int) + build.mov(dword[sSpillArea + i * 4], inst.regX64); + else if (FFlag::LuauCodegenSplitFloat && spill.valueKind == IrValueKind::Float) + build.vmovss(dword[sSpillArea + i * 4], inst.regX64); + else + CODEGEN_ASSERT(!"Unsupported value kind"); - usedSpillSlots.set(i); + unsigned end = i + kValueDwordSize[int(spill.valueKind)]; - if (i + 1 > maxUsedSlot) - maxUsedSlot = i + 1; + for (unsigned pos = i; pos < end; pos++) + usedSpillSlotHalfs.set(pos); - if (spill.valueKind == IrValueKind::Tvalue) + if ((end + 1) / 2 > maxUsedSlot) + maxUsedSlot = (end + 1) / 2; + } + else { - usedSpillSlots.set(i + 1); + if (spill.valueKind == IrValueKind::Tvalue) + build.vmovups(xmmword[sSpillArea + i * 8], inst.regX64); + else if (spill.valueKind == IrValueKind::Double) + build.vmovsd(qword[sSpillArea + i * 8], inst.regX64); + else if (spill.valueKind == IrValueKind::Pointer) + build.mov(qword[sSpillArea + i * 8], inst.regX64); + else if (spill.valueKind == IrValueKind::Tag || spill.valueKind == IrValueKind::Int) + build.mov(dword[sSpillArea + i * 8], inst.regX64); + else if (FFlag::LuauCodegenSplitFloat && spill.valueKind == IrValueKind::Float) + build.vmovss(dword[sSpillArea + i * 8], inst.regX64); + else + CODEGEN_ASSERT(!"Unsupported value kind"); - if (i + 2 > maxUsedSlot) - maxUsedSlot = i + 2; + usedSpillSlots_DEPRECATED.set(i); + + if (i + 1 > maxUsedSlot) + maxUsedSlot = i + 1; + + if (spill.valueKind == IrValueKind::Tvalue) + { + usedSpillSlots_DEPRECATED.set(i + 1); + + if (i + 2 > maxUsedSlot) + maxUsedSlot = i + 2; + } } spill.stackSlot = uint8_t(i); @@ -272,13 +305,31 @@ void IrRegAllocX64::restore(IrInst& inst, bool intoOriginalLocation) if (spill.stackSlot != kNoStackSlot) { - restoreAddr = addr[sSpillArea + spill.stackSlot * 8]; + restoreAddr = FFlag::LuauCodegenDwordSpillSlots ? addr[sSpillArea + spill.stackSlot * 4] : addr[sSpillArea + spill.stackSlot * 8]; restoreAddr.memSize = reg.size; - usedSpillSlots.set(spill.stackSlot, false); + if (FFlag::LuauCodegenSplitFloat) + { + if (spill.valueKind == IrValueKind::Double) + restoreAddr.memSize = SizeX64::qword; + else if (spill.valueKind == IrValueKind::Float) + restoreAddr.memSize = SizeX64::dword; + } + + if (FFlag::LuauCodegenDwordSpillSlots) + { + unsigned end = spill.stackSlot + kValueDwordSize[int(spill.valueKind)]; + + for (unsigned pos = spill.stackSlot; pos < end; pos++) + usedSpillSlotHalfs.set(pos, false); + } + else + { + usedSpillSlots_DEPRECATED.set(spill.stackSlot, false); - if (spill.valueKind == IrValueKind::Tvalue) - usedSpillSlots.set(spill.stackSlot + 1, false); + if (spill.valueKind == IrValueKind::Tvalue) + usedSpillSlots_DEPRECATED.set(spill.stackSlot + 1, false); + } } else { @@ -303,10 +354,22 @@ void IrRegAllocX64::restore(IrInst& inst, bool intoOriginalLocation) else CODEGEN_ASSERT(!"re-materialization not supported for this conversion command"); } - else + else if (!FFlag::LuauCodegenSplitFloat) + { + build.mov(reg, restoreAddr); + } + else if (spill.valueKind == IrValueKind::Tag || spill.valueKind == IrValueKind::Int || spill.valueKind == IrValueKind::Pointer) { build.mov(reg, restoreAddr); } + else if (spill.valueKind == IrValueKind::Float) + { + build.vmovss(reg, restoreAddr); + } + else + { + CODEGEN_ASSERT(!"value kind not supported for restore"); + } } else { @@ -317,13 +380,24 @@ void IrRegAllocX64::restore(IrInst& inst, bool intoOriginalLocation) if (spill.stackSlot != kNoStackSlot) { - restoreLocation = addr[sSpillArea + spill.stackSlot * 8]; + restoreLocation = + FFlag::LuauCodegenDwordSpillSlots ? addr[sSpillArea + spill.stackSlot * 4] : addr[sSpillArea + spill.stackSlot * 8]; restoreLocation.memSize = reg.size; - usedSpillSlots.set(spill.stackSlot, false); + if (FFlag::LuauCodegenDwordSpillSlots) + { + unsigned end = spill.stackSlot + kValueDwordSize[int(spill.valueKind)]; - if (spill.valueKind == IrValueKind::Tvalue) - usedSpillSlots.set(spill.stackSlot + 1, false); + for (unsigned pos = spill.stackSlot; pos < end; pos++) + usedSpillSlotHalfs.set(pos, false); + } + else + { + usedSpillSlots_DEPRECATED.set(spill.stackSlot, false); + + if (spill.valueKind == IrValueKind::Tvalue) + usedSpillSlots_DEPRECATED.set(spill.stackSlot + 1, false); + } } else { @@ -334,8 +408,14 @@ void IrRegAllocX64::restore(IrInst& inst, bool intoOriginalLocation) build.vmovups(reg, restoreLocation); else if (spill.valueKind == IrValueKind::Double) build.vmovsd(reg, restoreLocation); - else + else if (!FFlag::LuauCodegenSplitFloat) build.mov(reg, restoreLocation); + else if (spill.valueKind == IrValueKind::Tag || spill.valueKind == IrValueKind::Int || spill.valueKind == IrValueKind::Pointer) + build.mov(reg, restoreLocation); + else if (spill.valueKind == IrValueKind::Float) + build.vmovss(reg, restoreLocation); + else + CODEGEN_ASSERT(!"value kind not supported for restore"); } inst.regX64 = reg; @@ -382,19 +462,56 @@ bool IrRegAllocX64::shouldFreeGpr(RegisterX64 reg) const unsigned IrRegAllocX64::findSpillStackSlot(IrValueKind valueKind) { - // Find a free stack slot. Two consecutive slots might be required for 16 byte TValues, so '- 1' is used - for (unsigned i = 0; i < unsigned(usedSpillSlots.size() - 1); ++i) + if (FFlag::LuauCodegenDwordSpillSlots) { - if (usedSpillSlots.test(i)) - continue; + if (valueKind == IrValueKind::Float || valueKind == IrValueKind::Int) + { + for (unsigned i = 0; i < unsigned(usedSpillSlotHalfs.size()); ++i) + { + if (usedSpillSlotHalfs.test(i)) + continue; - if (valueKind == IrValueKind::Tvalue && usedSpillSlots.test(i + 1)) + return i; + } + } + else { - ++i; // No need to retest this double position - continue; + // Find a free stack slot. Four consecutive slots might be required for 16 byte TValues, so '- 3' is used + // For 8 and 16 byte types we search in steps of 2 to return slot indices aligned by 2 + for (unsigned i = 0; i < unsigned(usedSpillSlotHalfs.size() - 3); i += 2) + { + if (usedSpillSlotHalfs.test(i) || usedSpillSlotHalfs.test(i + 1)) + continue; + + if (valueKind == IrValueKind::Tvalue) + { + if (usedSpillSlotHalfs.test(i + 2) || usedSpillSlotHalfs.test(i + 3)) + { + i += 2; // No need to retest this double position + continue; + } + } + + return i; + } } + } + else + { + // Find a free stack slot. Two consecutive slots might be required for 16 byte TValues, so '- 1' is used + for (unsigned i = 0; i < unsigned(usedSpillSlots_DEPRECATED.size() - 1); ++i) + { + if (usedSpillSlots_DEPRECATED.test(i)) + continue; + + if (valueKind == IrValueKind::Tvalue && usedSpillSlots_DEPRECATED.test(i + 1)) + { + ++i; // No need to retest this double position + continue; + } - return i; + return i; + } } CODEGEN_ASSERT(!"Nowhere to spill"); @@ -431,6 +548,8 @@ OperandX64 IrRegAllocX64::getRestoreAddress_DEPRECATED(const IrInst& inst, IrOp { case IrValueKind::Unknown: case IrValueKind::None: + case IrValueKind::Float: + case IrValueKind::Count: CODEGEN_ASSERT(!"Invalid operand restore value kind"); break; case IrValueKind::Tag: @@ -463,6 +582,8 @@ OperandX64 IrRegAllocX64::getRestoreAddress(const IrInst& inst, ValueRestoreLoca { case IrValueKind::Unknown: case IrValueKind::None: + case IrValueKind::Float: + case IrValueKind::Count: CODEGEN_ASSERT(!"Invalid operand restore value kind"); break; case IrValueKind::Tag: diff --git a/CodeGen/src/IrTranslateBuiltins.cpp b/CodeGen/src/IrTranslateBuiltins.cpp index 6b623d8b3..519e11338 100644 --- a/CodeGen/src/IrTranslateBuiltins.cpp +++ b/CodeGen/src/IrTranslateBuiltins.cpp @@ -8,7 +8,7 @@ #include -LUAU_FASTFLAGVARIABLE(LuauCodeGenVectorLerp2) +LUAU_FASTFLAGVARIABLE(LuauCodegenSplitFloat) // TODO: when nresults is less than our actual result count, we can skip computing/writing unused results @@ -285,7 +285,7 @@ static BuiltinImplResult translateBuiltinMathClamp( static BuiltinImplResult translateBuiltinVectorLerp(IrBuilder& build, int nparams, int ra, int arg, IrOp args, IrOp arg3, int nresults, int pcpos) { - if (!FFlag::LuauCodeGenVectorLerp2 || nparams < 3 || nresults > 1) + if (nparams < 3 || nresults > 1) return {BuiltinImplType::None, -1}; IrOp arg1 = build.vmReg(arg); @@ -795,7 +795,19 @@ static BuiltinImplResult translateBuiltinVector(IrBuilder& build, int nparams, i IrOp y = builtinLoadDouble(build, args); IrOp z = builtinLoadDouble(build, arg3); - build.inst(IrCmd::STORE_VECTOR, build.vmReg(ra), x, y, z); + if (FFlag::LuauCodegenSplitFloat) + { + IrOp xf = build.inst(IrCmd::NUM_TO_FLOAT, x); + IrOp yf = build.inst(IrCmd::NUM_TO_FLOAT, y); + IrOp zf = build.inst(IrCmd::NUM_TO_FLOAT, z); + + build.inst(IrCmd::STORE_VECTOR, build.vmReg(ra), xf, yf, zf); + } + else + { + build.inst(IrCmd::STORE_VECTOR, build.vmReg(ra), x, y, z); + } + build.inst(IrCmd::STORE_TAG, build.vmReg(ra), build.constTag(LUA_TVECTOR)); return {BuiltinImplType::Full, 1}; @@ -956,6 +968,9 @@ static BuiltinImplResult translateBuiltinVectorMagnitude( IrOp sum = build.inst(IrCmd::DOT_VEC, a, a); + if (FFlag::LuauCodegenSplitFloat) + sum = build.inst(IrCmd::FLOAT_TO_NUM, sum); + IrOp mag = build.inst(IrCmd::SQRT_NUM, sum); build.inst(IrCmd::STORE_DOUBLE, build.vmReg(ra), mag); @@ -985,6 +1000,9 @@ static BuiltinImplResult translateBuiltinVectorNormalize( IrOp a = build.inst(IrCmd::LOAD_TVALUE, arg1, build.constInt(0)); IrOp sum = build.inst(IrCmd::DOT_VEC, a, a); + if (FFlag::LuauCodegenSplitFloat) + sum = build.inst(IrCmd::FLOAT_TO_NUM, sum); + IrOp mag = build.inst(IrCmd::SQRT_NUM, sum); IrOp inv = build.inst(IrCmd::DIV_NUM, build.constDouble(1.0), mag); IrOp invvec = build.inst(IrCmd::NUM_TO_VEC, inv); @@ -1017,6 +1035,18 @@ static BuiltinImplResult translateBuiltinVectorCross(IrBuilder& build, int npara IrOp z1 = build.inst(IrCmd::LOAD_FLOAT, arg1, build.constInt(8)); IrOp z2 = build.inst(IrCmd::LOAD_FLOAT, args, build.constInt(8)); + if (FFlag::LuauCodegenSplitFloat) + { + x1 = build.inst(IrCmd::FLOAT_TO_NUM, x1); + x2 = build.inst(IrCmd::FLOAT_TO_NUM, x2); + + y1 = build.inst(IrCmd::FLOAT_TO_NUM, y1); + y2 = build.inst(IrCmd::FLOAT_TO_NUM, y2); + + z1 = build.inst(IrCmd::FLOAT_TO_NUM, z1); + z2 = build.inst(IrCmd::FLOAT_TO_NUM, z2); + } + IrOp y1z2 = build.inst(IrCmd::MUL_NUM, y1, z2); IrOp z1y2 = build.inst(IrCmd::MUL_NUM, z1, y2); IrOp xr = build.inst(IrCmd::SUB_NUM, y1z2, z1y2); @@ -1029,6 +1059,13 @@ static BuiltinImplResult translateBuiltinVectorCross(IrBuilder& build, int npara IrOp y1x2 = build.inst(IrCmd::MUL_NUM, y1, x2); IrOp zr = build.inst(IrCmd::SUB_NUM, x1y2, y1x2); + if (FFlag::LuauCodegenSplitFloat) + { + xr = build.inst(IrCmd::NUM_TO_FLOAT, xr); + yr = build.inst(IrCmd::NUM_TO_FLOAT, yr); + zr = build.inst(IrCmd::NUM_TO_FLOAT, zr); + } + build.inst(IrCmd::STORE_VECTOR, build.vmReg(ra), xr, yr, zr); build.inst(IrCmd::STORE_TAG, build.vmReg(ra), build.constTag(LUA_TVECTOR)); @@ -1050,6 +1087,9 @@ static BuiltinImplResult translateBuiltinVectorDot(IrBuilder& build, int nparams IrOp sum = build.inst(IrCmd::DOT_VEC, a, b); + if (FFlag::LuauCodegenSplitFloat) + sum = build.inst(IrCmd::FLOAT_TO_NUM, sum); + build.inst(IrCmd::STORE_DOUBLE, build.vmReg(ra), sum); build.inst(IrCmd::STORE_TAG, build.vmReg(ra), build.constTag(LUA_TNUMBER)); @@ -1079,10 +1119,24 @@ static BuiltinImplResult translateBuiltinVectorMap1( IrOp y1 = build.inst(IrCmd::LOAD_FLOAT, arg1, build.constInt(4)); IrOp z1 = build.inst(IrCmd::LOAD_FLOAT, arg1, build.constInt(8)); + if (FFlag::LuauCodegenSplitFloat) + { + x1 = build.inst(IrCmd::FLOAT_TO_NUM, x1); + y1 = build.inst(IrCmd::FLOAT_TO_NUM, y1); + z1 = build.inst(IrCmd::FLOAT_TO_NUM, z1); + } + IrOp xr = build.inst(cmd, x1); IrOp yr = build.inst(cmd, y1); IrOp zr = build.inst(cmd, z1); + if (FFlag::LuauCodegenSplitFloat) + { + xr = build.inst(IrCmd::NUM_TO_FLOAT, xr); + yr = build.inst(IrCmd::NUM_TO_FLOAT, yr); + zr = build.inst(IrCmd::NUM_TO_FLOAT, zr); + } + build.inst(IrCmd::STORE_VECTOR, build.vmReg(ra), xr, yr, zr); build.inst(IrCmd::STORE_TAG, build.vmReg(ra), build.constTag(LUA_TVECTOR)); @@ -1118,6 +1172,13 @@ static BuiltinImplResult translateBuiltinVectorClamp( IrOp xmin = build.inst(IrCmd::LOAD_FLOAT, args, build.constInt(0)); IrOp xmax = build.inst(IrCmd::LOAD_FLOAT, arg3, build.constInt(0)); + if (FFlag::LuauCodegenSplitFloat) + { + x = build.inst(IrCmd::FLOAT_TO_NUM, x); + xmin = build.inst(IrCmd::FLOAT_TO_NUM, xmin); + xmax = build.inst(IrCmd::FLOAT_TO_NUM, xmax); + } + build.inst(IrCmd::JUMP_CMP_NUM, xmin, xmax, build.cond(IrCondition::NotLessEqual), fallback, block1); build.beginBlock(block1); @@ -1126,6 +1187,13 @@ static BuiltinImplResult translateBuiltinVectorClamp( IrOp ymin = build.inst(IrCmd::LOAD_FLOAT, args, build.constInt(4)); IrOp ymax = build.inst(IrCmd::LOAD_FLOAT, arg3, build.constInt(4)); + if (FFlag::LuauCodegenSplitFloat) + { + y = build.inst(IrCmd::FLOAT_TO_NUM, y); + ymin = build.inst(IrCmd::FLOAT_TO_NUM, ymin); + ymax = build.inst(IrCmd::FLOAT_TO_NUM, ymax); + } + build.inst(IrCmd::JUMP_CMP_NUM, ymin, ymax, build.cond(IrCondition::NotLessEqual), fallback, block2); build.beginBlock(block2); @@ -1134,6 +1202,13 @@ static BuiltinImplResult translateBuiltinVectorClamp( IrOp zmin = build.inst(IrCmd::LOAD_FLOAT, args, build.constInt(8)); IrOp zmax = build.inst(IrCmd::LOAD_FLOAT, arg3, build.constInt(8)); + if (FFlag::LuauCodegenSplitFloat) + { + z = build.inst(IrCmd::FLOAT_TO_NUM, z); + zmin = build.inst(IrCmd::FLOAT_TO_NUM, zmin); + zmax = build.inst(IrCmd::FLOAT_TO_NUM, zmax); + } + build.inst(IrCmd::JUMP_CMP_NUM, zmin, zmax, build.cond(IrCondition::NotLessEqual), fallback, block3); build.beginBlock(block3); @@ -1147,6 +1222,13 @@ static BuiltinImplResult translateBuiltinVectorClamp( IrOp ztemp = build.inst(IrCmd::MAX_NUM, zmin, z); IrOp zclamped = build.inst(IrCmd::MIN_NUM, zmax, ztemp); + if (FFlag::LuauCodegenSplitFloat) + { + xclamped = build.inst(IrCmd::NUM_TO_FLOAT, xclamped); + yclamped = build.inst(IrCmd::NUM_TO_FLOAT, yclamped); + zclamped = build.inst(IrCmd::NUM_TO_FLOAT, zclamped); + } + build.inst(IrCmd::STORE_VECTOR, build.vmReg(ra), xclamped, yclamped, zclamped); build.inst(IrCmd::STORE_TAG, build.vmReg(ra), build.constTag(LUA_TVECTOR)); @@ -1181,10 +1263,28 @@ static BuiltinImplResult translateBuiltinVectorMap2( IrOp y2 = build.inst(IrCmd::LOAD_FLOAT, args, build.constInt(4)); IrOp z2 = build.inst(IrCmd::LOAD_FLOAT, args, build.constInt(8)); + if (FFlag::LuauCodegenSplitFloat) + { + x1 = build.inst(IrCmd::FLOAT_TO_NUM, x1); + y1 = build.inst(IrCmd::FLOAT_TO_NUM, y1); + z1 = build.inst(IrCmd::FLOAT_TO_NUM, z1); + + x2 = build.inst(IrCmd::FLOAT_TO_NUM, x2); + y2 = build.inst(IrCmd::FLOAT_TO_NUM, y2); + z2 = build.inst(IrCmd::FLOAT_TO_NUM, z2); + } + IrOp xr = build.inst(cmd, x1, x2); IrOp yr = build.inst(cmd, y1, y2); IrOp zr = build.inst(cmd, z1, z2); + if (FFlag::LuauCodegenSplitFloat) + { + xr = build.inst(IrCmd::NUM_TO_FLOAT, xr); + yr = build.inst(IrCmd::NUM_TO_FLOAT, yr); + zr = build.inst(IrCmd::NUM_TO_FLOAT, zr); + } + build.inst(IrCmd::STORE_VECTOR, build.vmReg(ra), xr, yr, zr); build.inst(IrCmd::STORE_TAG, build.vmReg(ra), build.constTag(LUA_TVECTOR)); @@ -1318,9 +1418,15 @@ BuiltinImplResult translateBuiltin( case LBF_BUFFER_WRITEU32: return translateBuiltinBufferWrite(build, nparams, ra, arg, args, arg3, nresults, pcpos, IrCmd::BUFFER_WRITEI32, 4, IrCmd::NUM_TO_UINT); case LBF_BUFFER_READF32: - return translateBuiltinBufferRead(build, nparams, ra, arg, args, arg3, nresults, pcpos, IrCmd::BUFFER_READF32, 4, IrCmd::NOP); + if (FFlag::LuauCodegenSplitFloat) + return translateBuiltinBufferRead(build, nparams, ra, arg, args, arg3, nresults, pcpos, IrCmd::BUFFER_READF32, 4, IrCmd::FLOAT_TO_NUM); + else + return translateBuiltinBufferRead(build, nparams, ra, arg, args, arg3, nresults, pcpos, IrCmd::BUFFER_READF32, 4, IrCmd::NOP); case LBF_BUFFER_WRITEF32: - return translateBuiltinBufferWrite(build, nparams, ra, arg, args, arg3, nresults, pcpos, IrCmd::BUFFER_WRITEF32, 4, IrCmd::NOP); + if (FFlag::LuauCodegenSplitFloat) + return translateBuiltinBufferWrite(build, nparams, ra, arg, args, arg3, nresults, pcpos, IrCmd::BUFFER_WRITEF32, 4, IrCmd::NUM_TO_FLOAT); + else + return translateBuiltinBufferWrite(build, nparams, ra, arg, args, arg3, nresults, pcpos, IrCmd::BUFFER_WRITEF32, 4, IrCmd::NOP); case LBF_BUFFER_READF64: return translateBuiltinBufferRead(build, nparams, ra, arg, args, arg3, nresults, pcpos, IrCmd::BUFFER_READF64, 8, IrCmd::NOP); case LBF_BUFFER_WRITEF64: diff --git a/CodeGen/src/IrTranslation.cpp b/CodeGen/src/IrTranslation.cpp index 30d1de0b3..b6619006d 100644 --- a/CodeGen/src/IrTranslation.cpp +++ b/CodeGen/src/IrTranslation.cpp @@ -13,6 +13,10 @@ #include "ltm.h" LUAU_FASTFLAG(LuauCodegenBlockSafeEnv) +LUAU_FASTFLAGVARIABLE(LuauCodegenLoopStepDetectFix) +LUAU_FASTFLAGVARIABLE(LuauCodegenLinearAndOr) +LUAU_FASTFLAG(LuauCodegenUpvalueLoadProp) +LUAU_FASTFLAG(LuauCodegenSplitFloat) namespace Luau { @@ -828,7 +832,15 @@ void translateInstGetUpval(IrBuilder& build, const Instruction* pc, int pcpos) int ra = LUAU_INSN_A(*pc); int up = LUAU_INSN_B(*pc); - build.inst(IrCmd::GET_UPVALUE, build.vmReg(ra), build.vmUpvalue(up)); + if (FFlag::LuauCodegenUpvalueLoadProp) + { + IrOp value = build.inst(IrCmd::GET_UPVALUE, build.vmUpvalue(up)); + build.inst(IrCmd::STORE_TVALUE, build.vmReg(ra), value); + } + else + { + build.inst(IrCmd::GET_UPVALUE, build.vmReg(ra), build.vmUpvalue(up)); + } } void translateInstSetUpval(IrBuilder& build, const Instruction* pc, int pcpos) @@ -836,7 +848,15 @@ void translateInstSetUpval(IrBuilder& build, const Instruction* pc, int pcpos) int ra = LUAU_INSN_A(*pc); int up = LUAU_INSN_B(*pc); - build.inst(IrCmd::SET_UPVALUE, build.vmUpvalue(up), build.vmReg(ra), build.undef()); + if (FFlag::LuauCodegenUpvalueLoadProp) + { + IrOp value = build.inst(IrCmd::LOAD_TVALUE, build.vmReg(ra)); + build.inst(IrCmd::SET_UPVALUE, build.vmUpvalue(up), value, build.undef()); + } + else + { + build.inst(IrCmd::SET_UPVALUE, build.vmUpvalue(up), build.vmReg(ra), build.undef()); + } } void translateInstCloseUpvals(IrBuilder& build, const Instruction* pc) @@ -935,7 +955,8 @@ static IrOp getLoopStepK(IrBuilder& build, int ra) { IrBlock& active = build.function.blocks[build.activeBlockIdx]; - if (active.start + 2 < build.function.instructions.size()) + if (FFlag::LuauCodegenLoopStepDetectFix ? active.start + 2 <= build.function.instructions.size() + : active.start + 2 < build.function.instructions.size()) { IrInst& sv = build.function.instructions[build.function.instructions.size() - 2]; IrInst& st = build.function.instructions[build.function.instructions.size() - 1]; @@ -1362,18 +1383,30 @@ void translateInstGetTableKS(IrBuilder& build, const Instruction* pc, int pcpos) if (str->len == 1 && (*field == 'X' || *field == 'x')) { IrOp value = build.inst(IrCmd::LOAD_FLOAT, build.vmReg(rb), build.constInt(0)); + + if (FFlag::LuauCodegenSplitFloat) + value = build.inst(IrCmd::FLOAT_TO_NUM, value); + build.inst(IrCmd::STORE_DOUBLE, build.vmReg(ra), value); build.inst(IrCmd::STORE_TAG, build.vmReg(ra), build.constTag(LUA_TNUMBER)); } else if (str->len == 1 && (*field == 'Y' || *field == 'y')) { IrOp value = build.inst(IrCmd::LOAD_FLOAT, build.vmReg(rb), build.constInt(4)); + + if (FFlag::LuauCodegenSplitFloat) + value = build.inst(IrCmd::FLOAT_TO_NUM, value); + build.inst(IrCmd::STORE_DOUBLE, build.vmReg(ra), value); build.inst(IrCmd::STORE_TAG, build.vmReg(ra), build.constTag(LUA_TNUMBER)); } else if (str->len == 1 && (*field == 'Z' || *field == 'z')) { IrOp value = build.inst(IrCmd::LOAD_FLOAT, build.vmReg(rb), build.constInt(8)); + + if (FFlag::LuauCodegenSplitFloat) + value = build.inst(IrCmd::FLOAT_TO_NUM, value); + build.inst(IrCmd::STORE_DOUBLE, build.vmReg(ra), value); build.inst(IrCmd::STORE_TAG, build.vmReg(ra), build.constTag(LUA_TNUMBER)); } @@ -1664,31 +1697,43 @@ void translateInstAndX(IrBuilder& build, const Instruction* pc, int pcpos, IrOp int ra = LUAU_INSN_A(*pc); int rb = LUAU_INSN_B(*pc); - IrOp fallthrough = build.block(IrBlockKind::Internal); - IrOp next = build.blockAtInst(pcpos + 1); - - IrOp target = (ra == rb) ? next : build.block(IrBlockKind::Internal); - - build.inst(IrCmd::JUMP_IF_FALSY, build.vmReg(rb), target, fallthrough); - build.beginBlock(fallthrough); - - IrOp load = build.inst(IrCmd::LOAD_TVALUE, c); - build.inst(IrCmd::STORE_TVALUE, build.vmReg(ra), load); - build.inst(IrCmd::JUMP, next); - - if (ra == rb) + if (FFlag::LuauCodegenLinearAndOr) { - build.beginBlock(next); + // "b and c" -> "truthy(b) ? c : b" + IrOp lhs = build.inst(IrCmd::LOAD_TVALUE, build.vmReg(rb)); + IrOp rhs = build.inst(IrCmd::LOAD_TVALUE, c); + + IrOp result = build.inst(IrCmd::SELECT_IF_TRUTHY, lhs, rhs, lhs); + build.inst(IrCmd::STORE_TVALUE, build.vmReg(ra), result); } else { - build.beginBlock(target); + IrOp fallthrough = build.block(IrBlockKind::Internal); + IrOp next = build.blockAtInst(pcpos + 1); - IrOp load1 = build.inst(IrCmd::LOAD_TVALUE, build.vmReg(rb)); - build.inst(IrCmd::STORE_TVALUE, build.vmReg(ra), load1); + IrOp target = (ra == rb) ? next : build.block(IrBlockKind::Internal); + + build.inst(IrCmd::JUMP_IF_FALSY, build.vmReg(rb), target, fallthrough); + build.beginBlock(fallthrough); + + IrOp load = build.inst(IrCmd::LOAD_TVALUE, c); + build.inst(IrCmd::STORE_TVALUE, build.vmReg(ra), load); build.inst(IrCmd::JUMP, next); - build.beginBlock(next); + if (ra == rb) + { + build.beginBlock(next); + } + else + { + build.beginBlock(target); + + IrOp load1 = build.inst(IrCmd::LOAD_TVALUE, build.vmReg(rb)); + build.inst(IrCmd::STORE_TVALUE, build.vmReg(ra), load1); + build.inst(IrCmd::JUMP, next); + + build.beginBlock(next); + } } } @@ -1697,31 +1742,43 @@ void translateInstOrX(IrBuilder& build, const Instruction* pc, int pcpos, IrOp c int ra = LUAU_INSN_A(*pc); int rb = LUAU_INSN_B(*pc); - IrOp fallthrough = build.block(IrBlockKind::Internal); - IrOp next = build.blockAtInst(pcpos + 1); - - IrOp target = (ra == rb) ? next : build.block(IrBlockKind::Internal); - - build.inst(IrCmd::JUMP_IF_TRUTHY, build.vmReg(rb), target, fallthrough); - build.beginBlock(fallthrough); - - IrOp load = build.inst(IrCmd::LOAD_TVALUE, c); - build.inst(IrCmd::STORE_TVALUE, build.vmReg(ra), load); - build.inst(IrCmd::JUMP, next); - - if (ra == rb) + if (FFlag::LuauCodegenLinearAndOr) { - build.beginBlock(next); + // "b or c" -> truthy(b) ? b : c + IrOp lhs = build.inst(IrCmd::LOAD_TVALUE, build.vmReg(rb)); + IrOp rhs = build.inst(IrCmd::LOAD_TVALUE, c); + + IrOp result = build.inst(IrCmd::SELECT_IF_TRUTHY, lhs, lhs, rhs); + build.inst(IrCmd::STORE_TVALUE, build.vmReg(ra), result); } else { - build.beginBlock(target); + IrOp fallthrough = build.block(IrBlockKind::Internal); + IrOp next = build.blockAtInst(pcpos + 1); - IrOp load1 = build.inst(IrCmd::LOAD_TVALUE, build.vmReg(rb)); - build.inst(IrCmd::STORE_TVALUE, build.vmReg(ra), load1); + IrOp target = (ra == rb) ? next : build.block(IrBlockKind::Internal); + + build.inst(IrCmd::JUMP_IF_TRUTHY, build.vmReg(rb), target, fallthrough); + build.beginBlock(fallthrough); + + IrOp load = build.inst(IrCmd::LOAD_TVALUE, c); + build.inst(IrCmd::STORE_TVALUE, build.vmReg(ra), load); build.inst(IrCmd::JUMP, next); - build.beginBlock(next); + if (ra == rb) + { + build.beginBlock(next); + } + else + { + build.beginBlock(target); + + IrOp load1 = build.inst(IrCmd::LOAD_TVALUE, build.vmReg(rb)); + build.inst(IrCmd::STORE_TVALUE, build.vmReg(ra), load1); + build.inst(IrCmd::JUMP, next); + + build.beginBlock(next); + } } } diff --git a/CodeGen/src/IrUtils.cpp b/CodeGen/src/IrUtils.cpp index 8ca480272..f6489df34 100644 --- a/CodeGen/src/IrUtils.cpp +++ b/CodeGen/src/IrUtils.cpp @@ -16,11 +16,20 @@ #include #include +LUAU_FASTFLAGVARIABLE(LuauCodegenBetterSccRemoval) +LUAU_FASTFLAGVARIABLE(LuauCodegenNumToUintFoldRange) +LUAU_FASTFLAG(LuauCodegenLinearAndOr) +LUAU_FASTFLAG(LuauCodegenUpvalueLoadProp) + +LUAU_FASTFLAG(LuauCodegenSplitFloat) + namespace Luau { namespace CodeGen { +constexpr double kDoubleMaxExactInteger = 9007199254740992.0; + int getOpLength(LuauOpcode op) { switch (int(op)) @@ -145,7 +154,7 @@ IrValueKind getCmdValueKind(IrCmd cmd) case IrCmd::LOAD_INT: return IrValueKind::Int; case IrCmd::LOAD_FLOAT: - return IrValueKind::Double; + return FFlag::LuauCodegenSplitFloat ? IrValueKind::Float : IrValueKind::Double; case IrCmd::LOAD_TVALUE: return IrValueKind::Tvalue; case IrCmd::LOAD_ENV: @@ -165,6 +174,8 @@ IrValueKind getCmdValueKind(IrCmd cmd) return IrValueKind::None; case IrCmd::ADD_INT: case IrCmd::SUB_INT: + case IrCmd::SEXTI8_INT: + case IrCmd::SEXTI16_INT: return IrValueKind::Int; case IrCmd::ADD_NUM: case IrCmd::SUB_NUM: @@ -190,10 +201,12 @@ IrValueKind getCmdValueKind(IrCmd cmd) case IrCmd::DIV_VEC: case IrCmd::UNM_VEC: case IrCmd::SELECT_VEC: + case IrCmd::SELECT_IF_TRUTHY: case IrCmd::MULADD_VEC: return IrValueKind::Tvalue; case IrCmd::DOT_VEC: - return IrValueKind::Double; + case IrCmd::EXTRACT_VEC: + return FFlag::LuauCodegenSplitFloat ? IrValueKind::Float : IrValueKind::Double; case IrCmd::NOT_ANY: case IrCmd::CMP_ANY: case IrCmd::CMP_INT: @@ -230,9 +243,15 @@ IrValueKind getCmdValueKind(IrCmd cmd) case IrCmd::NUM_TO_INT: case IrCmd::NUM_TO_UINT: return IrValueKind::Int; + case IrCmd::FLOAT_TO_NUM: + return IrValueKind::Double; + case IrCmd::NUM_TO_FLOAT: + return IrValueKind::Float; case IrCmd::NUM_TO_VEC: case IrCmd::TAG_VECTOR: return IrValueKind::Tvalue; + case IrCmd::TRUNCATE_UINT: + return IrValueKind::Int; case IrCmd::ADJUST_STACK_TO_REG: case IrCmd::ADJUST_STACK_TO_TOP: return IrValueKind::None; @@ -247,7 +266,9 @@ IrValueKind getCmdValueKind(IrCmd cmd) case IrCmd::SET_TABLE: case IrCmd::GET_CACHED_IMPORT: case IrCmd::CONCAT: + return IrValueKind::None; case IrCmd::GET_UPVALUE: + return FFlag::LuauCodegenUpvalueLoadProp ? IrValueKind::Tvalue : IrValueKind::None; case IrCmd::SET_UPVALUE: case IrCmd::CHECK_TAG: case IrCmd::CHECK_TRUTHY: @@ -323,6 +344,7 @@ IrValueKind getCmdValueKind(IrCmd cmd) case IrCmd::BUFFER_WRITEF64: return IrValueKind::None; case IrCmd::BUFFER_READF32: + return FFlag::LuauCodegenSplitFloat ? IrValueKind::Float : IrValueKind::Double; case IrCmd::BUFFER_READF64: return IrValueKind::Double; } @@ -459,6 +481,11 @@ void kill(IrFunction& function, uint32_t start, uint32_t end) if (curr.cmd == IrCmd::NOP) continue; + // Do not force destruction of instructions that are still in use + // When the operands are released, the instruction will be released automatically + if (FFlag::LuauCodegenBetterSccRemoval && curr.useCount != 0) + continue; + kill(function, curr); } } @@ -496,8 +523,11 @@ void replace(IrFunction& function, IrBlock& block, uint32_t instIdx, IrInst repl addUse(function, replacement.f); addUse(function, replacement.g); - // An extra reference is added so block will not remove itself - block.useCount++; + if (!FFlag::LuauCodegenBetterSccRemoval) + { + // An extra reference is added so block will not remove itself + block.useCount++; + } // If we introduced an earlier terminating instruction, all following instructions become dead if (!isBlockTerminator(inst.cmd) && isBlockTerminator(replacement.cmd)) @@ -508,25 +538,59 @@ void replace(IrFunction& function, IrBlock& block, uint32_t instIdx, IrInst repl kill(function, instIdx + 1, block.finish); + // If killing that range killed the current block we have to undo replacement instruction uses and exit + if (FFlag::LuauCodegenBetterSccRemoval && block.kind == IrBlockKind::Dead) + { + removeUse(function, replacement.a); + removeUse(function, replacement.b); + removeUse(function, replacement.c); + removeUse(function, replacement.d); + removeUse(function, replacement.e); + removeUse(function, replacement.f); + removeUse(function, replacement.g); + return; + } + block.finish = instIdx; } - removeUse(function, inst.a); - removeUse(function, inst.b); - removeUse(function, inst.c); - removeUse(function, inst.d); - removeUse(function, inst.e); - removeUse(function, inst.f); - removeUse(function, inst.g); + if (FFlag::LuauCodegenBetterSccRemoval) + { + // Before we remove old argument uses, we have to place our new instruction + IrInst copy = inst; - // Inherit existing use count (last use is skipped as it will be defined later) - replacement.useCount = inst.useCount; + // Inherit existing use count (last use is skipped as it will be defined later) + replacement.useCount = inst.useCount; - inst = replacement; + inst = replacement; - // Removing the earlier extra reference, this might leave the block without users without marking it as dead - // This will have to be handled by separate dead code elimination - block.useCount--; + removeUse(function, copy.a); + removeUse(function, copy.b); + removeUse(function, copy.c); + removeUse(function, copy.d); + removeUse(function, copy.e); + removeUse(function, copy.f); + removeUse(function, copy.g); + } + else + { + removeUse(function, inst.a); + removeUse(function, inst.b); + removeUse(function, inst.c); + removeUse(function, inst.d); + removeUse(function, inst.e); + removeUse(function, inst.f); + removeUse(function, inst.g); + + // Inherit existing use count (last use is skipped as it will be defined later) + replacement.useCount = inst.useCount; + + inst = replacement; + + // Removing the earlier extra reference, this might leave the block without users without marking it as dead + // This will have to be handled by separate dead code elimination + block.useCount--; + } } void substitute(IrFunction& function, IrInst& inst, IrOp replacement) @@ -699,6 +763,20 @@ void foldConstants(IrBuilder& build, IrFunction& function, IrBlock& block, uint3 substitute(function, inst, build.constInt(sum)); } break; + case IrCmd::SEXTI8_INT: + if (inst.a.kind == IrOpKind::Constant) + { + int32_t value = int8_t(function.intOp(inst.a)); + substitute(function, inst, build.constInt(value)); + } + break; + case IrCmd::SEXTI16_INT: + if (inst.a.kind == IrOpKind::Constant) + { + int32_t value = int16_t(function.intOp(inst.a)); + substitute(function, inst, build.constInt(value)); + } + break; case IrCmd::ADD_NUM: if (inst.a.kind == IrOpKind::Constant && inst.b.kind == IrOpKind::Constant) substitute(function, inst, build.constDouble(function.doubleOp(inst.a) + function.doubleOp(inst.b))); @@ -781,6 +859,25 @@ void foldConstants(IrBuilder& build, IrFunction& function, IrBlock& block, uint3 substitute(function, inst, c == d ? inst.b : inst.a); } + else if (FFlag::LuauCodegenLinearAndOr && inst.a == inst.b) + { + // If the values are the same, no need to worry about the condition check + substitute(function, inst, inst.a); + } + break; + case IrCmd::SELECT_VEC: + if (FFlag::LuauCodegenLinearAndOr && inst.a == inst.b) + { + // If the values are the same, no need to worry about the condition check + substitute(function, inst, inst.a); + } + break; + case IrCmd::SELECT_IF_TRUTHY: + if (FFlag::LuauCodegenLinearAndOr && inst.b == inst.c) + { + // If the values are the same, no need to worry about the condition check + substitute(function, inst, inst.b); + } break; case IrCmd::NOT_ANY: if (inst.a.kind == IrOpKind::Constant) @@ -940,7 +1037,7 @@ void foldConstants(IrBuilder& build, IrFunction& function, IrBlock& block, uint3 { double value = function.doubleOp(inst.a); - // To avoid undefined behavior of casting a value not representable in the target type, we check the range + // To avoid undefined behavior of casting a value not representable in the target type, check the range (matches luai_num2int) if (value >= INT_MIN && value <= INT_MAX) substitute(function, inst, build.constInt(int(value))); } @@ -950,11 +1047,29 @@ void foldConstants(IrBuilder& build, IrFunction& function, IrBlock& block, uint3 { double value = function.doubleOp(inst.a); - // To avoid undefined behavior of casting a value not representable in the target type, we check the range - if (value >= 0 && value <= UINT_MAX) - substitute(function, inst, build.constInt(unsigned(function.doubleOp(inst.a)))); + // To avoid undefined behavior of casting a value not representable in the target type, check the range (matches luai_num2unsigned) + if (FFlag::LuauCodegenNumToUintFoldRange) + { + if (value >= -kDoubleMaxExactInteger && value <= kDoubleMaxExactInteger) + substitute(function, inst, build.constInt(unsigned((long long)function.doubleOp(inst.a)))); + } + else + { + if (value >= 0 && value <= UINT_MAX) + substitute(function, inst, build.constInt(unsigned(function.doubleOp(inst.a)))); + } } break; + case IrCmd::FLOAT_TO_NUM: + // float -> double for a constant is a no-op + if (inst.a.kind == IrOpKind::Constant) + substitute(function, inst, build.constDouble(function.doubleOp(inst.a))); + break; + case IrCmd::NUM_TO_FLOAT: + // double -> float for a constant just needs to lower precision + if (inst.a.kind == IrOpKind::Constant) + substitute(function, inst, build.constDouble(float(function.doubleOp(inst.a)))); + break; case IrCmd::CHECK_TAG: if (inst.a.kind == IrOpKind::Constant && inst.b.kind == IrOpKind::Constant) { @@ -1231,5 +1346,10 @@ IrBlock* tryGetNextBlockInChain(IrFunction& function, IrBlock& block) return nullptr; } +bool isEntryBlock(const IrBlock& block) +{ + return block.useCount == 0 && block.kind != IrBlockKind::Dead; +} + } // namespace CodeGen } // namespace Luau diff --git a/CodeGen/src/IrValueLocationTracking.cpp b/CodeGen/src/IrValueLocationTracking.cpp index 464768181..befd2529c 100644 --- a/CodeGen/src/IrValueLocationTracking.cpp +++ b/CodeGen/src/IrValueLocationTracking.cpp @@ -4,6 +4,7 @@ #include "Luau/IrUtils.h" LUAU_FASTFLAGVARIABLE(LuauCodegenChainedSpills) +LUAU_FASTFLAG(LuauCodegenUpvalueLoadProp) namespace Luau { @@ -86,7 +87,8 @@ void IrValueLocationTracking::beforeInstLowering(IrInst& inst) invalidateRestoreVmRegs(vmRegOp(inst.a), function.uintOp(inst.b)); break; case IrCmd::GET_UPVALUE: - invalidateRestoreOp(inst.a, /*skipValueInvalidation*/ false); + if (!FFlag::LuauCodegenUpvalueLoadProp) + invalidateRestoreOp(inst.a, /*skipValueInvalidation*/ false); break; case IrCmd::CALL: // Even if result count is limited, all registers starting from function (ra) might be modified diff --git a/CodeGen/src/OptimizeConstProp.cpp b/CodeGen/src/OptimizeConstProp.cpp index 39702727c..5c19e8c03 100644 --- a/CodeGen/src/OptimizeConstProp.cpp +++ b/CodeGen/src/OptimizeConstProp.cpp @@ -22,19 +22,29 @@ LUAU_FASTINTVARIABLE(LuauCodeGenMinLinearBlockPath, 3) LUAU_FASTINTVARIABLE(LuauCodeGenReuseSlotLimit, 64) LUAU_FASTINTVARIABLE(LuauCodeGenReuseUdataTagLimit, 64) LUAU_FASTINTVARIABLE(LuauCodeGenLiveSlotReuseLimit, 8) +LUAU_FASTFLAG(LuauCodegenSplitFloat) LUAU_FASTFLAGVARIABLE(DebugLuauAbortingChecks) LUAU_FASTFLAGVARIABLE(LuauCodegenStorePriority) LUAU_FASTFLAGVARIABLE(LuauCodegenInterruptIsNotForWrites) LUAU_FASTFLAGVARIABLE(LuauCodegenFloatLoadStoreProp) +LUAU_FASTFLAGVARIABLE(LuauCodegenLoadFloatSubstituteLast) LUAU_FASTFLAGVARIABLE(LuauCodegenBlockSafeEnv) LUAU_FASTFLAGVARIABLE(LuauCodegenChainLink) LUAU_FASTFLAGVARIABLE(LuauCodegenIntegerAddSub) +LUAU_FASTFLAG(LuauCodegenBetterSccRemoval) +LUAU_FASTFLAGVARIABLE(LuauCodegenSetBlockEntryState) +LUAU_FASTFLAGVARIABLE(LuauCodegenHydrateLoadWithTag) +LUAU_FASTFLAGVARIABLE(LuauCodegenUpvalueLoadProp) +LUAU_FASTFLAGVARIABLE(LuauCodegenBufferLoadProp2) +LUAU_FASTFLAGVARIABLE(LuauCodegenNumIntFolds2) namespace Luau { namespace CodeGen { +constexpr uint8_t kUpvalueEmptyKey = 0xff; + // Data we know about the register value struct RegisterInfo { @@ -66,6 +76,19 @@ struct NumberedInstruction uint32_t finishPos = 0; }; +struct BufferLoadStoreInfo +{ + IrCmd loadCmd = IrCmd::NOP; + uint8_t accessSize = 0; + uint8_t tag = LUA_TNIL; + bool fromStore = false; + + IrOp address; + IrOp value; + + int offset = 0; +}; + static uint8_t tryGetTagForTypename(std::string_view name, bool forTypeof) { if (name == "nil") @@ -114,8 +137,9 @@ static bool safeIntegerConstant(double value) // Data we know about the current VM state struct ConstPropState { - ConstPropState(IrFunction& function) - : function(function) + ConstPropState(IrBuilder& build, IrFunction& function) + : build(build) + , function(function) , valueMap({}) { } @@ -125,6 +149,13 @@ struct ConstPropState if (RegisterInfo* info = tryGetRegisterInfo(op)) return info->tag; + // SSA register might be associated by a tag through a CHECK_TAG + if (FFlag::LuauCodegenUpvalueLoadProp && op.kind == IrOpKind::Inst) + { + if (uint8_t* info = instTag.find(op.index)) + return *info; + } + return 0xff; } @@ -244,7 +275,14 @@ struct ConstPropState void invalidateValuePropagation() { valueMap.clear(); + + if (FFlag::LuauCodegenUpvalueLoadProp) + upvalueMap.clear(); + tryNumToIndexCache.clear(); + + if (FFlag::LuauCodegenBufferLoadProp2) + bufferLoadStoreInfo.clear(); } // If table memory has changed, we can't reuse previously computed and validated table slot lookups @@ -261,6 +299,9 @@ struct ConstPropState void invalidateHeapBufferData() { checkBufferLenCache.clear(); + + if (FFlag::LuauCodegenBufferLoadProp2) + bufferLoadStoreInfo.clear(); } void invalidateUserdataData() @@ -276,6 +317,9 @@ struct ConstPropState invalidateHeapTableData(); // Buffer length checks are not invalidated since buffer size is immutable + + if (FFlag::LuauCodegenBufferLoadProp2) + bufferLoadStoreInfo.clear(); } void invalidateHeap(RegisterInfo& reg) @@ -289,6 +333,10 @@ struct ConstPropState { invalidateHeap(); invalidateCapturedRegisters(); + + if (FFlag::LuauCodegenUpvalueLoadProp) + upvalueMap.clear(); + inSafeEnv = false; } @@ -485,6 +533,380 @@ struct ConstPropState valueMap[versionedVmRegLoad(loadCmd, storeInst.a)] = storeInst.b.index; } + // When loading from a VM register, there might be an instruction that has loaded the whole TValue + // That TValue might have a CHECK_TAG data associated with it + bool substituteTagLoadWithTValueData(IrBuilder& build, IrInst& loadInst) + { + CODEGEN_ASSERT(FFlag::LuauCodegenUpvalueLoadProp); + CODEGEN_ASSERT(loadInst.a.kind == IrOpKind::VmReg); + + if (uint32_t* prevIdx = getPreviousVersionedLoadIndex(IrCmd::LOAD_TVALUE, loadInst.a)) + { + if (uint8_t* tag = instTag.find(*prevIdx); tag && *tag != 0xff) + { + substitute(function, loadInst, build.constTag(*tag)); + return true; + } + } + + return false; + } + + // When loading from a VM register, there might be an instruction that has loaded the whole TValue + // That TValue might have a value data associated with it, if not we will record it here + bool substituteOrRecordValueLoadWithTValueData(IrBuilder& build, IrInst& loadInst) + { + CODEGEN_ASSERT(FFlag::LuauCodegenUpvalueLoadProp); + CODEGEN_ASSERT(loadInst.a.kind == IrOpKind::VmReg); + + if (uint32_t* prevIdx = getPreviousVersionedLoadIndex(IrCmd::LOAD_TVALUE, loadInst.a)) + { + if (uint32_t* valueIdx = instValue.find(*prevIdx)) + { + IrInst& value = function.instructions[*valueIdx]; + + if (value.useCount != 0 && value.cmd == loadInst.cmd) + { + substitute(function, loadInst, IrOp{IrOpKind::Inst, *valueIdx}); + return true; + } + } + else + { + // Current instruction is now the holder of the value in the TValue + instValue[*prevIdx] = function.getInstIndex(loadInst); + } + } + + return false; + } + + IrInst versionedVmUpvalueLoad(IrInst& loadInst) + { + CODEGEN_ASSERT(FFlag::LuauCodegenUpvalueLoadProp); + IrOp op = loadInst.a; + CODEGEN_ASSERT(op.kind == IrOpKind::VmUpvalue); + uint32_t version = regs[vmUpvalueOp(op)].version; + CODEGEN_ASSERT(version <= 0xffffff); + op.index = vmUpvalueOp(loadInst.a) | (version << 8); + return IrInst{loadInst.cmd, op}; + } + + bool substituteOrRecordVmUpvalueLoad(IrInst& loadInst) + { + CODEGEN_ASSERT(FFlag::LuauCodegenUpvalueLoadProp); + CODEGEN_ASSERT(loadInst.a.kind == IrOpKind::VmUpvalue); + + if (uint32_t* prevIdx = upvalueMap.find(vmUpvalueOp(loadInst.a))) + { + if (*prevIdx != kInvalidInstIdx) + { + // Substitute load instruction with the previous value + substitute(function, loadInst, IrOp{IrOpKind::Inst, *prevIdx}); + return true; + } + } + + uint32_t instIdx = function.getInstIndex(loadInst); + + // Record load of this upvalue for future substitution + upvalueMap[vmUpvalueOp(loadInst.a)] = instIdx; + return false; + } + + void forwardVmUpvalueStoreToLoad(const IrInst& storeInst) + { + CODEGEN_ASSERT(FFlag::LuauCodegenUpvalueLoadProp); + + // Future loads of this upvalue version can use the value we stored + upvalueMap[vmUpvalueOp(storeInst.a)] = storeInst.b.index; + } + + // For a LOAD_FLOAT operation, it is possible to find an operand from a previous STORE_VECTOR instruction + std::optional findSubstituteComponentLoadFromStoreVector(IrBuilder& build, IrOp vmReg, int offset) + { + IrInst versionedLoad = versionedVmRegLoad(IrCmd::LOAD_FLOAT, vmReg); + + // Check if there is a value that already has this version of the register + if (uint32_t* prevIdx = getPreviousInstIndex(versionedLoad)) + { + IrInst& store = function.instructions[*prevIdx]; + CODEGEN_ASSERT(store.cmd == IrCmd::STORE_VECTOR); + + IrOp argOp; + + if (offset == 0) + argOp = store.b; + else if (offset == 4) + argOp = store.c; + else if (offset == 8) + argOp = store.d; + + if (IrInst* arg = function.asInstOp(argOp)) + { + // Argument can only be re-used if it contains the value of the same precision + if (arg->cmd == IrCmd::LOAD_FLOAT || arg->cmd == IrCmd::BUFFER_READF32 || + (FFlag::LuauCodegenSplitFloat && arg->cmd == IrCmd::NUM_TO_FLOAT)) + return argOp; + } + else if (argOp.kind == IrOpKind::Constant) + { + // Constant can be used if we bring it down to correct precision + return build.constDouble(float(function.doubleOp(argOp))); + } + } + + return {}; + }; + + void substituteOrRecordBufferLoad(IrBlock& block, uint32_t instIdx, IrInst& loadInst, uint8_t accessSize) + { + CODEGEN_ASSERT(FFlag::LuauCodegenBufferLoadProp2); + + // Only constant offsets are supported + if (loadInst.b.kind != IrOpKind::Constant) + return; + + int offset = function.intOp(loadInst.b); + uint8_t tag = loadInst.c.kind == IrOpKind::None ? LUA_TBUFFER : function.tagOp(loadInst.c); + + // Find if we have data for this kind of load + for (BufferLoadStoreInfo& info : bufferLoadStoreInfo) + { + if (info.address == loadInst.a && info.offset == offset && info.tag == tag) + { + // Values from stores can have a different, but compatible types and will convert on read + if (info.fromStore) + { + switch (loadInst.cmd) + { + case IrCmd::BUFFER_READI8: + if (info.loadCmd == IrCmd::BUFFER_READI8) + { + if (info.value.kind == IrOpKind::Inst) + { + replace(function, block, instIdx, IrInst{IrCmd::SEXTI8_INT, info.value}); + substituteOrRecord(loadInst, instIdx); + } + else + { + substitute(function, loadInst, info.value); + } + return; + } + break; + case IrCmd::BUFFER_READU8: + if (info.loadCmd == IrCmd::BUFFER_READI8) + { + if (info.value.kind == IrOpKind::Inst) + { + replace(function, block, instIdx, IrInst{IrCmd::BITAND_UINT, info.value, build.constInt(0xff)}); + substituteOrRecord(loadInst, instIdx); + } + else + { + substitute(function, loadInst, build.constInt(uint8_t(function.intOp(info.value)))); + } + + return; + } + break; + case IrCmd::BUFFER_READI16: + if (info.loadCmd == IrCmd::BUFFER_READI16) + { + if (info.value.kind == IrOpKind::Inst) + { + replace(function, block, instIdx, IrInst{IrCmd::SEXTI16_INT, info.value}); + substituteOrRecord(loadInst, instIdx); + } + else + { + substitute(function, loadInst, info.value); + } + return; + } + break; + case IrCmd::BUFFER_READU16: + if (info.loadCmd == IrCmd::BUFFER_READI16) + { + if (info.value.kind == IrOpKind::Inst) + { + replace(function, block, instIdx, IrInst{IrCmd::BITAND_UINT, info.value, build.constInt(0xffff)}); + substituteOrRecord(loadInst, instIdx); + } + else + { + substitute(function, loadInst, build.constInt(uint16_t(function.intOp(info.value)))); + } + + return; + } + break; + case IrCmd::BUFFER_READI32: + if (info.loadCmd == IrCmd::BUFFER_READI32) + { + if (IrInst* src = function.asInstOp(info.value); src && producesDirtyHighRegisterBits(src->cmd)) + { + replace(function, block, instIdx, IrInst{IrCmd::TRUNCATE_UINT, info.value}); + substituteOrRecord(loadInst, instIdx); + } + else + { + substitute(function, loadInst, info.value); + } + return; + } + break; + case IrCmd::BUFFER_READF32: + if (info.loadCmd == IrCmd::BUFFER_READF32) + { + if (FFlag::LuauCodegenSplitFloat) + { + substitute(function, loadInst, info.value); + return; + } + else + { + // Can only propagate if the value has float precision + if (IrInst* src = function.asInstOp(info.value)) + { + if (src->cmd == IrCmd::LOAD_FLOAT || src->cmd == IrCmd::BUFFER_READF32) + { + substitute(function, loadInst, info.value); + return; + } + } + else + { + substitute(function, loadInst, info.value); + return; + } + } + } + break; + case IrCmd::BUFFER_READF64: + if (info.loadCmd == IrCmd::BUFFER_READF64) + { + substitute(function, loadInst, info.value); + return; + } + break; + default: + CODEGEN_ASSERT(!"unknown load instruction"); + } + } + else if (info.loadCmd == loadInst.cmd) // Values from loads match exactly + { + substitute(function, loadInst, info.value); + return; + } + } + } + + // Record this load for future reuse + BufferLoadStoreInfo info; + + info.loadCmd = loadInst.cmd; + info.accessSize = accessSize; + info.tag = tag; + info.fromStore = false; + + info.address = loadInst.a; + info.value = IrOp{IrOpKind::Inst, function.getInstIndex(loadInst)}; + + info.offset = offset; + + bufferLoadStoreInfo.push_back(info); + } + + void forwardBufferStoreToLoad(const IrInst& storeInst, IrCmd loadCmd, uint8_t accessSize) + { + CODEGEN_ASSERT(FFlag::LuauCodegenBufferLoadProp2); + + uint8_t tag = storeInst.d.kind == IrOpKind::None ? LUA_TBUFFER : function.tagOp(storeInst.d); + + // Writing at unknown offset removes everything in the same kind of memory (buffer/userdata) + // For userdata, we could check where the pointer is coming from, but we don't have an example of such usage + if (storeInst.b.kind != IrOpKind::Constant) + { + for (size_t i = 0; i < bufferLoadStoreInfo.size();) + { + BufferLoadStoreInfo& info = bufferLoadStoreInfo[i]; + + if (info.tag == tag) + { + bufferLoadStoreInfo[i] = bufferLoadStoreInfo.back(); + bufferLoadStoreInfo.pop_back(); + } + else + { + i++; + } + } + + return; + } + + int offset = function.intOp(storeInst.b); + + // Write at a constant offset invalidates that range in every object unless we know the pointers are unrelated + for (size_t i = 0; i < bufferLoadStoreInfo.size();) + { + BufferLoadStoreInfo& info = bufferLoadStoreInfo[i]; + + bool intersectingRange = offset + accessSize - 1 >= info.offset && offset <= info.offset + info.accessSize - 1; + + if (intersectingRange && info.tag == tag) + { + const IrInst& currPtr = function.instOp(storeInst.a); + const IrInst& infoPtr = function.instOp(info.address); + + // Pointers from separate allocations cannot be the same + if (currPtr.cmd == IrCmd::NEW_USERDATA && infoPtr.cmd == IrCmd::NEW_USERDATA) + { + i++; + continue; + } + + bufferLoadStoreInfo[i] = bufferLoadStoreInfo.back(); + bufferLoadStoreInfo.pop_back(); + } + else + { + i++; + } + } + + IrOp value = storeInst.c; + + // Store of smaller type will truncate data + // Dynamic values are handled in 'substituteOrRecordBufferLoad' + if (storeInst.c.kind == IrOpKind::Constant) + { + if (loadCmd == IrCmd::BUFFER_READI8) + value = build.constInt(int8_t(function.intOp(storeInst.c))); + else if (loadCmd == IrCmd::BUFFER_READI16) + value = build.constInt(int16_t(function.intOp(storeInst.c))); + else if (loadCmd == IrCmd::BUFFER_READF32) + value = build.constDouble(float(function.doubleOp(storeInst.c))); + } + + // Record this store value for future reuse + BufferLoadStoreInfo info; + + info.loadCmd = loadCmd; + info.accessSize = accessSize; + info.tag = tag; + info.fromStore = true; + + info.address = storeInst.a; + info.value = value; + + info.offset = offset; + + bufferLoadStoreInfo.push_back(info); + } + // Used to compute the pressure of the cached value 'set' on the spill registers // We want to find out the maximum live range intersection count between the cached value at 'slot' and current instruction // Note that this pressure is approximate, as some values that might have been live at one point could have been marked dead later @@ -554,12 +976,19 @@ struct ConstPropState instLink.clear(); + if (FFlag::LuauCodegenUpvalueLoadProp) + { + instTag.clear(); + instValue.clear(); + } + invalidateValuePropagation(); invalidateHeapTableData(); invalidateHeapBufferData(); invalidateUserdataData(); } + IrBuilder& build; IrFunction& function; std::array regs; @@ -573,10 +1002,18 @@ struct ConstPropState bool inSafeEnv = false; bool checkedGc = false; - DenseHashMap instLink{~0u}; + // Stores which register does the instruction value correspond to (and at which version of the register) + DenseHashMap instLink{kInvalidInstIdx}; + + // Stored the tag of a TValue stored in an instruction and will never change + DenseHashMap instTag{kInvalidInstIdx}; + DenseHashMap instValue{kInvalidInstIdx}; DenseHashMap valueMap; + // For upvalue load-store optimizations, we just keep track of the last known value of the upvalue + DenseHashMap upvalueMap{kUpvalueEmptyKey}; + // Some instruction re-uses can't be stored in valueMap because of extra requirements std::vector tryNumToIndexCache; // Fallback block argument might be different @@ -592,6 +1029,8 @@ struct ConstPropState // Userdata tag cache can point to both NEW_USERDATA and CHECK_USERDATA_TAG instructions std::vector useradataTagCache; // Additionally, fallback block argument might be different + std::vector bufferLoadStoreInfo; + std::vector rangeEndTemp; }; @@ -721,21 +1160,36 @@ static void constPropInInst(ConstPropState& state, IrBuilder& build, IrFunction& } else if (inst.a.kind == IrOpKind::VmReg) { + if (FFlag::LuauCodegenUpvalueLoadProp && state.substituteTagLoadWithTValueData(build, inst)) + break; + state.substituteOrRecordVmRegLoad(inst); } break; case IrCmd::LOAD_POINTER: if (inst.a.kind == IrOpKind::VmReg) + { + if (FFlag::LuauCodegenUpvalueLoadProp && state.substituteOrRecordValueLoadWithTValueData(build, inst)) + break; + state.substituteOrRecordVmRegLoad(inst); + } break; case IrCmd::LOAD_DOUBLE: { IrOp value = state.tryGetValue(inst.a); if (function.asDoubleOp(value)) + { substitute(function, inst, value); + } else if (inst.a.kind == IrOpKind::VmReg) + { + if (FFlag::LuauCodegenUpvalueLoadProp && state.substituteOrRecordValueLoadWithTValueData(build, inst)) + break; + state.substituteOrRecordVmRegLoad(inst); + } break; } case IrCmd::LOAD_INT: @@ -743,51 +1197,136 @@ static void constPropInInst(ConstPropState& state, IrBuilder& build, IrFunction& IrOp value = state.tryGetValue(inst.a); if (function.asIntOp(value)) + { substitute(function, inst, value); + } else if (inst.a.kind == IrOpKind::VmReg) + { + if (FFlag::LuauCodegenUpvalueLoadProp && state.substituteOrRecordValueLoadWithTValueData(build, inst)) + break; + state.substituteOrRecordVmRegLoad(inst); + } break; } case IrCmd::LOAD_FLOAT: if (FFlag::LuauCodegenFloatLoadStoreProp && inst.a.kind == IrOpKind::VmReg) { - if (state.substituteOrRecordVmRegLoad(inst)) + if (!FFlag::LuauCodegenLoadFloatSubstituteLast && state.substituteOrRecordVmRegLoad(inst)) break; - IrInst versionedLoad = state.versionedVmRegLoad(IrCmd::LOAD_FLOAT, inst.a); - - // Check if there is a value that already has this version of the register - if (uint32_t* prevIdx = state.getPreviousInstIndex(versionedLoad)) + if (FFlag::LuauCodegenUpvalueLoadProp) { - IrInst& store = function.instructions[*prevIdx]; - CODEGEN_ASSERT(store.cmd == IrCmd::STORE_VECTOR); - - IrOp argOp; - - if (std::optional intOp = function.asIntOp(inst.b)) + if (std::optional subst = state.findSubstituteComponentLoadFromStoreVector(build, inst.a, function.intOp(inst.b))) { - if (*intOp == 0) - argOp = store.b; - else if (*intOp == 4) - argOp = store.c; - else if (*intOp == 8) - argOp = store.d; + substitute(function, inst, *subst); + break; } - if (IrInst* arg = function.asInstOp(argOp)) + // There could be a whole TValue that we can extract a field from + if (uint32_t* prevIdxPtr = state.getPreviousVersionedLoadIndex(IrCmd::LOAD_TVALUE, inst.a)) { - // Argument can only be re-used if it contains the value of the same precision - if (arg->cmd == IrCmd::LOAD_FLOAT || arg->cmd == IrCmd::BUFFER_READF32) - substitute(function, inst, argOp); + uint32_t prevIdx = *prevIdxPtr; + IrInst& prev = function.instructions[prevIdx]; + + // Unpack the STORE_TVALUE of a TAG_VECTOR value + if (prev.cmd == IrCmd::TAG_VECTOR) + { + if (IrInst* untaggedValue = function.asInstOp(prev.a)) + prevIdx = prev.a.index; + } + + IrInst& value = function.instructions[prevIdx]; + + unsigned byteOffset = unsigned(function.intOp(inst.b)); + CODEGEN_ASSERT(byteOffset % 4 == 0); + + unsigned component = byteOffset / 4; + CODEGEN_ASSERT(component <= 3); + + // If we are extracting from a constant, we can substitute with it + if (value.cmd == IrCmd::LOAD_TVALUE && value.a.kind == IrOpKind::VmConst && function.proto) + { + TValue* tv = &function.proto->k[vmConstOp(value.a)]; + + if (ttisvector(tv)) + { + const float* v = vvalue(tv); + substitute(function, inst, build.constDouble(v[component])); + break; + } + } + else if (value.cmd == IrCmd::LOAD_TVALUE && value.a.kind == IrOpKind::VmReg) + { + if (std::optional subst = state.findSubstituteComponentLoadFromStoreVector(build, value.a, function.intOp(inst.b))) + { + substitute(function, inst, *subst); + break; + } + } + + replace(function, block, index, IrInst{IrCmd::EXTRACT_VEC, IrOp{IrOpKind::Inst, prevIdx}, build.constInt(component)}); + + if (FFlag::LuauCodegenLoadFloatSubstituteLast) + break; } + } + else + { + IrInst versionedLoad = state.versionedVmRegLoad(IrCmd::LOAD_FLOAT, inst.a); - break; + // Check if there is a value that already has this version of the register + if (uint32_t* prevIdx = state.getPreviousInstIndex(versionedLoad)) + { + IrInst& store = function.instructions[*prevIdx]; + CODEGEN_ASSERT(store.cmd == IrCmd::STORE_VECTOR); + + IrOp argOp; + + if (std::optional intOp = function.asIntOp(inst.b)) + { + if (*intOp == 0) + argOp = store.b; + else if (*intOp == 4) + argOp = store.c; + else if (*intOp == 8) + argOp = store.d; + } + + if (IrInst* arg = function.asInstOp(argOp)) + { + // Argument can only be re-used if it contains the value of the same precision + if (arg->cmd == IrCmd::LOAD_FLOAT || arg->cmd == IrCmd::BUFFER_READF32 || + (FFlag::LuauCodegenSplitFloat && arg->cmd == IrCmd::NUM_TO_FLOAT)) + substitute(function, inst, argOp); + } + + break; + } } + + if (FFlag::LuauCodegenLoadFloatSubstituteLast) + state.substituteOrRecordVmRegLoad(inst); } break; case IrCmd::LOAD_TVALUE: if (inst.a.kind == IrOpKind::VmReg) - state.substituteOrRecordVmRegLoad(inst); + { + if (FFlag::LuauCodegenHydrateLoadWithTag) + { + if (!state.substituteOrRecordVmRegLoad(inst) && inst.c.kind == IrOpKind::None) + { + // Provide information about what kind of tag is being loaded, this helps dead store elimination later + if (uint8_t tag = state.tryGetTag(inst.a); tag != 0xff) + replace(function, block, index, IrInst{IrCmd::LOAD_TVALUE, inst.a, build.constInt(0), build.constTag(tag)}); + } + } + else + { + if (inst.a.kind == IrOpKind::VmReg) + state.substituteOrRecordVmRegLoad(inst); + } + } break; case IrCmd::STORE_TAG: if (inst.a.kind == IrOpKind::VmReg) @@ -872,6 +1411,13 @@ static void constPropInInst(ConstPropState& state, IrBuilder& build, IrFunction& } break; case IrCmd::STORE_INT: + if (FFlag::LuauCodegenNumIntFolds2) + { + // Integer store doesn't access high register bits + if (IrInst* src = function.asInstOp(inst.b); src && src->cmd == IrCmd::TRUNCATE_UINT) + replace(function, inst.b, src->a); + } + if (inst.a.kind == IrOpKind::VmReg) { if (inst.b.kind == IrOpKind::Constant) @@ -1106,16 +1652,35 @@ static void constPropInInst(ConstPropState& state, IrBuilder& build, IrFunction& break; } case IrCmd::GET_UPVALUE: - state.invalidate(inst.a); + if (FFlag::LuauCodegenUpvalueLoadProp) + { + state.substituteOrRecordVmUpvalueLoad(inst); + } + else + { + state.invalidate(inst.a); + } break; case IrCmd::SET_UPVALUE: - if (inst.b.kind == IrOpKind::VmReg) + if (FFlag::LuauCodegenUpvalueLoadProp) { + state.forwardVmUpvalueStoreToLoad(inst); + if (uint8_t tag = state.tryGetTag(inst.b); tag != 0xff) { replace(function, inst.c, build.constTag(tag)); } } + else + { + if (inst.b.kind == IrOpKind::VmReg) + { + if (uint8_t tag = state.tryGetTag(inst.b); tag != 0xff) + { + replace(function, inst.c, build.constTag(tag)); + } + } + } break; case IrCmd::CHECK_TAG: { @@ -1147,6 +1712,19 @@ static void constPropInInst(ConstPropState& state, IrBuilder& build, IrFunction& } else { + if (FFlag::LuauCodegenUpvalueLoadProp) + { + const IrInst& lhs = function.instOp(inst.a); + + // If we are loading a tag from a register which has previously been loaded as a full TValue + // We can associate a known tag with that instruction going forward + if (lhs.cmd == IrCmd::LOAD_TAG && lhs.a.kind == IrOpKind::VmReg) + { + if (uint32_t* prevIdx = state.getPreviousVersionedLoadIndex(IrCmd::LOAD_TVALUE, lhs.a)) + state.instTag[*prevIdx] = b; + } + } + state.updateTag(inst.a, b); // We can assume the tag value going forward } break; @@ -1286,17 +1864,84 @@ static void constPropInInst(ConstPropState& state, IrBuilder& build, IrFunction& break; } case IrCmd::BUFFER_READI8: + if (FFlag::LuauCodegenBufferLoadProp2) + state.substituteOrRecordBufferLoad(block, index, inst, 1); + break; case IrCmd::BUFFER_READU8: + if (FFlag::LuauCodegenBufferLoadProp2) + state.substituteOrRecordBufferLoad(block, index, inst, 1); + break; case IrCmd::BUFFER_WRITEI8: + if (FFlag::LuauCodegenBufferLoadProp2) + { + if (IrInst* src = function.asInstOp(inst.c)) + { + std::optional intSrcB = function.asIntOp(src->b); + + if (src->cmd == IrCmd::SEXTI8_INT) + replace(function, inst.c, src->a); + else if (src->cmd == IrCmd::BITAND_UINT && intSrcB && *intSrcB == 0xff) + replace(function, inst.c, src->a); + } + + state.forwardBufferStoreToLoad(inst, IrCmd::BUFFER_READI8, 1); + } + break; case IrCmd::BUFFER_READI16: + if (FFlag::LuauCodegenBufferLoadProp2) + state.substituteOrRecordBufferLoad(block, index, inst, 2); + break; case IrCmd::BUFFER_READU16: + if (FFlag::LuauCodegenBufferLoadProp2) + state.substituteOrRecordBufferLoad(block, index, inst, 2); + break; case IrCmd::BUFFER_WRITEI16: + if (FFlag::LuauCodegenBufferLoadProp2) + { + if (IrInst* src = function.asInstOp(inst.c)) + { + std::optional intSrcB = function.asIntOp(src->b); + + if (src->cmd == IrCmd::SEXTI16_INT) + replace(function, inst.c, src->a); + else if (src->cmd == IrCmd::BITAND_UINT && intSrcB && *intSrcB == 0xffff) + replace(function, inst.c, src->a); + } + + state.forwardBufferStoreToLoad(inst, IrCmd::BUFFER_READI16, 2); + } + break; case IrCmd::BUFFER_READI32: + if (FFlag::LuauCodegenBufferLoadProp2) + state.substituteOrRecordBufferLoad(block, index, inst, 4); + break; case IrCmd::BUFFER_WRITEI32: + if (FFlag::LuauCodegenBufferLoadProp2) + { + if (IrInst* src = function.asInstOp(inst.c)) + { + if (src->cmd == IrCmd::TRUNCATE_UINT) + replace(function, inst.c, src->a); + } + + state.forwardBufferStoreToLoad(inst, IrCmd::BUFFER_READI32, 4); + } + break; case IrCmd::BUFFER_READF32: + if (FFlag::LuauCodegenBufferLoadProp2) + state.substituteOrRecordBufferLoad(block, index, inst, 4); + break; case IrCmd::BUFFER_WRITEF32: + if (FFlag::LuauCodegenBufferLoadProp2) + state.forwardBufferStoreToLoad(inst, IrCmd::BUFFER_READF32, 4); + break; case IrCmd::BUFFER_READF64: + if (FFlag::LuauCodegenBufferLoadProp2) + state.substituteOrRecordBufferLoad(block, index, inst, 8); + break; case IrCmd::BUFFER_WRITEF64: + if (FFlag::LuauCodegenBufferLoadProp2) + state.forwardBufferStoreToLoad(inst, IrCmd::BUFFER_READF64, 8); break; case IrCmd::CHECK_GC: // It is enough to perform a GC check once in a block @@ -1405,6 +2050,8 @@ static void constPropInInst(ConstPropState& state, IrBuilder& build, IrFunction& break; case IrCmd::ADD_INT: case IrCmd::SUB_INT: + case IrCmd::SEXTI8_INT: + case IrCmd::SEXTI16_INT: state.substituteOrRecord(inst, index); break; case IrCmd::ADD_NUM: @@ -1466,9 +2113,19 @@ static void constPropInInst(ConstPropState& state, IrBuilder& build, IrFunction& case IrCmd::SELECT_NUM: case IrCmd::SELECT_VEC: case IrCmd::MULADD_VEC: + case IrCmd::EXTRACT_VEC: case IrCmd::NOT_ANY: state.substituteOrRecord(inst, index); break; + case IrCmd::SELECT_IF_TRUTHY: + if (uint8_t tag = state.tryGetTag(inst.a); tag != 0xff) + { + if (tag == LUA_TNIL) + substitute(function, inst, inst.c); + else if (tag != LUA_TBOOLEAN) + substitute(function, inst, inst.b); + } + break; case IrCmd::CMP_INT: break; case IrCmd::CMP_ANY: @@ -1581,10 +2238,35 @@ static void constPropInInst(ConstPropState& state, IrBuilder& build, IrFunction& state.substituteOrRecord(inst, index); break; case IrCmd::NUM_TO_INT: - if (IrInst* src = function.asInstOp(inst.a); src && src->cmd == IrCmd::INT_TO_NUM) - substitute(function, inst, src->a); - else + if (FFlag::LuauCodegenNumIntFolds2) + { + IrInst* src = function.asInstOp(inst.a); + + if (src && src->cmd == IrCmd::INT_TO_NUM) + { + substitute(function, inst, src->a); + break; + } + + // INT and UINT are stored in the same way and can be reinterpreted (constants are not and are handled in foldConstants) + if (src && src->cmd == IrCmd::UINT_TO_NUM && src->a.kind != IrOpKind::Constant) + { + if (IrInst* srcOfSrc = function.asInstOp(src->a); srcOfSrc && producesDirtyHighRegisterBits(srcOfSrc->cmd)) + replace(function, block, index, IrInst{IrCmd::TRUNCATE_UINT, src->a}); + else + substitute(function, inst, src->a); + break; + } + state.substituteOrRecord(inst, index); + } + else + { + if (IrInst* src = function.asInstOp(inst.a); src && src->cmd == IrCmd::INT_TO_NUM) + substitute(function, inst, src->a); + else + state.substituteOrRecord(inst, index); + } break; case IrCmd::NUM_TO_UINT: { @@ -1592,10 +2274,31 @@ static void constPropInInst(ConstPropState& state, IrBuilder& build, IrFunction& { IrInst* src = function.asInstOp(inst.a); - if (src && src->cmd == IrCmd::UINT_TO_NUM) + if (FFlag::LuauCodegenNumIntFolds2) { - substitute(function, inst, src->a); - break; + if (src && src->cmd == IrCmd::UINT_TO_NUM) + { + if (IrInst* srcOfSrc = function.asInstOp(src->a); srcOfSrc && producesDirtyHighRegisterBits(srcOfSrc->cmd)) + replace(function, block, index, IrInst{IrCmd::TRUNCATE_UINT, src->a}); + else + substitute(function, inst, src->a); + break; + } + + // INT and UINT are stored in the same way and can be reinterpreted (constants are not and are handled in foldConstants) + if (src && src->cmd == IrCmd::INT_TO_NUM && src->a.kind != IrOpKind::Constant) + { + substitute(function, inst, src->a); + break; + } + } + else + { + if (src && src->cmd == IrCmd::UINT_TO_NUM) + { + substitute(function, inst, src->a); + break; + } } if (src && src->cmd == IrCmd::ADD_NUM) @@ -1655,13 +2358,54 @@ static void constPropInInst(ConstPropState& state, IrBuilder& build, IrFunction& } else { - if (IrInst* src = function.asInstOp(inst.a); src && src->cmd == IrCmd::UINT_TO_NUM) - substitute(function, inst, src->a); + if (FFlag::LuauCodegenNumIntFolds2) + { + IrInst* src = function.asInstOp(inst.a); + + if (src && src->cmd == IrCmd::UINT_TO_NUM) + { + if (IrInst* srcOfSrc = function.asInstOp(src->a); srcOfSrc && producesDirtyHighRegisterBits(srcOfSrc->cmd)) + replace(function, block, index, IrInst{IrCmd::TRUNCATE_UINT, src->a}); + else + substitute(function, inst, src->a); + break; + } + + // INT and UINT are stored in the same way and can be reinterpreted (constants are not and are handled in foldConstants) + if (src && src->cmd == IrCmd::INT_TO_NUM && src->a.kind != IrOpKind::Constant) + { + substitute(function, inst, src->a); + break; + } + } else - state.substituteOrRecord(inst, index); + { + if (IrInst* src = function.asInstOp(inst.a); src && src->cmd == IrCmd::UINT_TO_NUM) + substitute(function, inst, src->a); + else + state.substituteOrRecord(inst, index); + } } break; } + case IrCmd::TRUNCATE_UINT: + // Truncation can be skipped if the source does not produce dirty high register bits: TRUNCATE_UINT(uint32) => uint32 + if (IrInst* src = function.asInstOp(inst.a); src && !producesDirtyHighRegisterBits(src->cmd)) + substitute(function, inst, inst.a); + else + state.substituteOrRecord(inst, index); + break; + case IrCmd::FLOAT_TO_NUM: + // double->float->double conversion cannot be skipped as it affects value precision + state.substituteOrRecord(inst, index); + break; + case IrCmd::NUM_TO_FLOAT: + // We can skip float->double->float conversion: NUM_TO_FLOAT(FLOAT_TO_NUM(value)) => value + if (IrInst* src = function.asInstOp(inst.a); src && src->cmd == IrCmd::FLOAT_TO_NUM) + substitute(function, inst, src->a); + else + state.substituteOrRecord(inst, index); + break; case IrCmd::CHECK_ARRAY_SIZE: { std::optional arrayIndex = function.asIntOp(inst.b.kind == IrOpKind::Constant ? inst.b : state.tryGetValue(inst.b)); @@ -1897,6 +2641,71 @@ static void constPropInInst(ConstPropState& state, IrBuilder& build, IrFunction& } } +static void setupBlockEntryState(IrBuilder& build, IrFunction& function, IrBlock& block, ConstPropState& state) +{ + // State starts with knowledge of entry registers, unless it's an entry block which establishes this + if (isEntryBlock(block)) + return; + + const BytecodeTypeInfo& typeInfo = build.function.bcOriginalTypeInfo; + + for (size_t i = 0; i < typeInfo.argumentTypes.size(); i++) + { + uint8_t et = typeInfo.argumentTypes[i]; + uint8_t tag = et & ~LBC_TYPE_OPTIONAL_BIT; + + if (tag == LBC_TYPE_ANY || (et & LBC_TYPE_OPTIONAL_BIT) != 0) + continue; + + if (function.cfg.written.regs[i]) + continue; + + if (function.cfg.written.varargSeq && i >= function.cfg.written.varargStart) + continue; + + if (function.cfg.captured.regs[i]) + continue; + + switch (tag) + { + case LBC_TYPE_NIL: + state.regs[i].tag = LUA_TNIL; + break; + case LBC_TYPE_BOOLEAN: + state.regs[i].tag = LUA_TBOOLEAN; + break; + case LBC_TYPE_NUMBER: + state.regs[i].tag = LUA_TNUMBER; + break; + case LBC_TYPE_STRING: + state.regs[i].tag = LUA_TSTRING; + break; + case LBC_TYPE_TABLE: + state.regs[i].tag = LUA_TTABLE; + break; + case LBC_TYPE_FUNCTION: + state.regs[i].tag = LUA_TFUNCTION; + break; + case LBC_TYPE_THREAD: + state.regs[i].tag = LUA_TTHREAD; + break; + case LBC_TYPE_USERDATA: + state.regs[i].tag = LUA_TUSERDATA; + break; + case LBC_TYPE_VECTOR: + state.regs[i].tag = LUA_TVECTOR; + break; + case LBC_TYPE_BUFFER: + state.regs[i].tag = LUA_TBUFFER; + break; + default: + if (tag >= LBC_TYPE_TAGGED_USERDATA_BASE && tag < LBC_TYPE_TAGGED_USERDATA_END) + state.regs[i].tag = LUA_TUSERDATA; + break; + } + } +} + static void constPropInBlock(IrBuilder& build, IrBlock& block, ConstPropState& state) { IrFunction& function = build.function; @@ -1918,6 +2727,10 @@ static void constPropInBlock(IrBuilder& build, IrBlock& block, ConstPropState& s foldConstants(build, function, block, index); constPropInInst(state, build, function, block, inst, index); + + // Optimizations might have killed the current block + if (FFlag::LuauCodegenBetterSccRemoval && block.kind == IrBlockKind::Dead) + break; } } @@ -1927,6 +2740,9 @@ static void constPropInBlockChain(IrBuilder& build, std::vector& visite state.clear(); + if (FFlag::LuauCodegenSetBlockEntryState) + setupBlockEntryState(build, function, *block, state); + const uint32_t startSortkey = block->sortkey; uint32_t chainPos = 0; @@ -1945,6 +2761,10 @@ static void constPropInBlockChain(IrBuilder& build, std::vector& visite constPropInBlock(build, *block, state); + // Optimizations might have killed the current block + if (FFlag::LuauCodegenBetterSccRemoval && block->kind == IrBlockKind::Dead) + break; + if (!FFlag::LuauCodegenChainLink) { // Value numbering and load/store propagation is not performed between blocks @@ -2179,7 +2999,7 @@ void constPropInBlockChains(IrBuilder& build) { IrFunction& function = build.function; - ConstPropState state{function}; + ConstPropState state{build, function}; std::vector visited(function.blocks.size(), false); @@ -2202,7 +3022,7 @@ void createLinearBlocks(IrBuilder& build) // new 'block' will only be reachable from a single one and all gathered information can be preserved. IrFunction& function = build.function; - ConstPropState state{function}; + ConstPropState state{build, function}; std::vector visited(function.blocks.size(), false); diff --git a/CodeGen/src/OptimizeDeadStore.cpp b/CodeGen/src/OptimizeDeadStore.cpp index d815ebf79..cfd99b19e 100644 --- a/CodeGen/src/OptimizeDeadStore.cpp +++ b/CodeGen/src/OptimizeDeadStore.cpp @@ -9,6 +9,8 @@ #include "lobject.h" +LUAU_FASTFLAGVARIABLE(LuauCodegenGcoDse) + // TODO: optimization can be improved by knowing which registers are live in at each VM exit namespace Luau @@ -40,8 +42,9 @@ struct StoreRegInfo struct RemoveDeadStoreState { - RemoveDeadStoreState(IrFunction& function) + RemoveDeadStoreState(IrFunction& function, std::vector& remainingUses) : function(function) + , remainingUses(remainingUses) { maxReg = function.proto ? function.proto->maxstacksize : 255; } @@ -270,6 +273,19 @@ struct RemoveDeadStoreState hasGcoToClear = false; } + bool hasRemainingUses(uint32_t instIdx) + { + IrInst& inst = function.instructions[instIdx]; + + return anyArgumentMatch( + inst, + [&](IrOp op) + { + return op.kind == IrOpKind::Inst && remainingUses[op.index] != 0; + } + ); + } + // Partial clear of information about registers that might contain a GC object // This is used by instructions that might perform a GC assist and GC needs all pointers to be pinned to stack void flushGcoRegs() @@ -283,11 +299,32 @@ struct RemoveDeadStoreState // If we happen to know the exact tag, it has to be a GCO, otherwise 'maybeGCO' should be false CODEGEN_ASSERT(regInfo.knownTag == kUnknownTag || isGCO(regInfo.knownTag)); - // Indirect register read by GC doesn't clear the known tag - regInfo.tagInstIdx = ~0u; - regInfo.valueInstIdx = ~0u; - regInfo.tvalueInstIdx = ~0u; - regInfo.maybeGco = false; + if (FFlag::LuauCodegenGcoDse) + { + // If the values stored are still used and might be a GCO object, we have to pin in to the stack + // And we have to pin all components of the register containing GCO + bool tagUsedAfter = regInfo.tagInstIdx != ~0u && hasRemainingUses(regInfo.tagInstIdx); + bool valueUsedAfter = regInfo.valueInstIdx != ~0u && hasRemainingUses(regInfo.valueInstIdx); + bool tvalueUsedAfter = regInfo.tvalueInstIdx != ~0u && hasRemainingUses(regInfo.tvalueInstIdx); + + if (tagUsedAfter || valueUsedAfter || tvalueUsedAfter) + { + regInfo.tagInstIdx = ~0u; + regInfo.valueInstIdx = ~0u; + regInfo.tvalueInstIdx = ~0u; + } + + // Indirect register read by GC doesn't clear the known tag + regInfo.maybeGco = false; + } + else + { + // Indirect register read by GC doesn't clear the known tag + regInfo.tagInstIdx = ~0u; + regInfo.valueInstIdx = ~0u; + regInfo.tvalueInstIdx = ~0u; + regInfo.maybeGco = false; + } } } @@ -295,12 +332,16 @@ struct RemoveDeadStoreState } IrFunction& function; + std::vector& remainingUses; std::array info; int maxReg = 255; // Some of the registers contain values which might be a GC object bool hasGcoToClear = false; + + // Have there been any object allocations which might remain unused + bool hasAllocations = false; }; static bool tryReplaceTagWithFullStore( @@ -536,8 +577,28 @@ static bool tryReplaceVectorValueWithFullStore( return false; } +static void updateRemainingUses(RemoveDeadStoreState& state, IrInst& inst, uint32_t index) +{ + state.remainingUses[index] = inst.useCount; + + visitArguments( + inst, + [&](IrOp op) + { + if (op.kind == IrOpKind::Inst) + { + CODEGEN_ASSERT(state.remainingUses[op.index] != 0); + state.remainingUses[op.index]--; + } + } + ); +} + static void markDeadStoresInInst(RemoveDeadStoreState& state, IrBuilder& build, IrFunction& function, IrBlock& block, IrInst& inst, uint32_t index) { + if (FFlag::LuauCodegenGcoDse) + updateRemainingUses(state, inst, index); + switch (inst.cmd) { case IrCmd::STORE_TAG: @@ -785,6 +846,11 @@ static void markDeadStoresInInst(RemoveDeadStoreState& state, IrBuilder& build, visitVmRegDefsUses(state, function, inst); break; + case IrCmd::NEW_USERDATA: + if (FFlag::LuauCodegenGcoDse) + state.hasAllocations = true; + break; + default: // Guards have to be covered explicitly CODEGEN_ASSERT(!isNonTerminatingJump(inst.cmd)); @@ -807,11 +873,24 @@ static void markDeadStoresInBlock(IrBuilder& build, IrBlock& block, RemoveDeadSt } } -static void markDeadStoresInBlockChain(IrBuilder& build, std::vector& visited, IrBlock* block) +static void markDeadStoresInBlockChain( + IrBuilder& build, + std::vector& visited, + std::vector& remainingUses, + std::vector& blockIdxChain, + IrBlock* block +) { IrFunction& function = build.function; - RemoveDeadStoreState state{function}; + RemoveDeadStoreState state{function, remainingUses}; + + if (FFlag::LuauCodegenGcoDse) + { + // We will be visiting this chain a few times to clean unreferenced temporaries + // Clear the storage we reuse + blockIdxChain.clear(); + } while (block) { @@ -819,6 +898,9 @@ static void markDeadStoresInBlockChain(IrBuilder& build, std::vector& v CODEGEN_ASSERT(!visited[blockIdx]); visited[blockIdx] = true; + if (FFlag::LuauCodegenGcoDse) + blockIdxChain.push_back(blockIdx); + markDeadStoresInBlock(build, *block, state); IrInst& termInst = function.instructions[block->finish]; @@ -838,6 +920,74 @@ static void markDeadStoresInBlockChain(IrBuilder& build, std::vector& v block = nextBlock; } + + // If there are allocating instructions, check if they have 'read' uses after DSE + if (FFlag::LuauCodegenGcoDse && state.hasAllocations) + { + bool foundUnused = false; + + // Remove uses in instructions writing to the allocations + for (uint32_t blockIdx : blockIdxChain) + { + IrBlock& block = function.blocks[blockIdx]; + + for (uint32_t index = block.start; index <= block.finish; index++) + { + IrInst& inst = function.instructions[index]; + + state.remainingUses[index] = inst.useCount; + + switch (inst.cmd) + { + case IrCmd::BUFFER_WRITEI8: + case IrCmd::BUFFER_WRITEI16: + case IrCmd::BUFFER_WRITEI32: + case IrCmd::BUFFER_WRITEF32: + case IrCmd::BUFFER_WRITEF64: + state.remainingUses[inst.a.index]--; + + if (state.remainingUses[inst.a.index] == 0) + foundUnused = true; + break; + default: + break; + } + } + } + + // Remove those write instructions if they were the only users of the allocation + if (foundUnused) + { + for (uint32_t blockIdx : blockIdxChain) + { + IrBlock& block = function.blocks[blockIdx]; + + for (uint32_t index = block.start; index <= block.finish; index++) + { + IrInst& inst = function.instructions[index]; + + switch (inst.cmd) + { + case IrCmd::BUFFER_WRITEI8: + case IrCmd::BUFFER_WRITEI16: + case IrCmd::BUFFER_WRITEI32: + case IrCmd::BUFFER_WRITEF32: + case IrCmd::BUFFER_WRITEF64: + if (state.remainingUses[inst.a.index] == 0) + { + IrInst& pointer = function.instOp(inst.a); + + if (pointer.cmd == IrCmd::NEW_USERDATA) + kill(function, inst); + } + break; + default: + break; + } + } + } + } + } } void markDeadStoresInBlockChains(IrBuilder& build) @@ -845,6 +995,8 @@ void markDeadStoresInBlockChains(IrBuilder& build) IrFunction& function = build.function; std::vector visited(function.blocks.size(), false); + std::vector remainingUses(function.instructions.size(), 0u); + std::vector blockIdxChain; for (IrBlock& block : function.blocks) { @@ -854,7 +1006,7 @@ void markDeadStoresInBlockChains(IrBuilder& build) if (visited[function.getBlockIndex(block)]) continue; - markDeadStoresInBlockChain(build, visited, &block); + markDeadStoresInBlockChain(build, visited, remainingUses, blockIdxChain, &block); } } diff --git a/Common/include/Luau/Variant.h b/Common/include/Luau/Variant.h index 5d6542dd3..67ca34f98 100644 --- a/Common/include/Luau/Variant.h +++ b/Common/include/Luau/Variant.h @@ -73,7 +73,7 @@ class Variant table[typeId](&storage, &other.storage); } - Variant(Variant&& other) + Variant(Variant&& other) noexcept { typeId = other.typeId; tableMove[typeId](&storage, &other.storage); @@ -91,7 +91,7 @@ class Variant return *this = static_cast(copy); } - Variant& operator=(Variant&& other) + Variant& operator=(Variant&& other) noexcept { if (this != &other) { @@ -169,7 +169,7 @@ class Variant static constexpr size_t storageAlign = cmax({alignof(Ts)...}); using FnCopy = void (*)(void*, const void*); - using FnMove = void (*)(void*, void*); + using FnMove = void (*)(void*, void*) noexcept; using FnDtor = void (*)(void*); using FnPred = bool (*)(const void*, const void*); @@ -180,7 +180,7 @@ class Variant } template - static void fnMove(void* dst, void* src) + static void fnMove(void* dst, void* src) noexcept { // static_cast is equivalent to std::move() but faster in Debug new (dst) T(static_cast(*static_cast(src))); diff --git a/Compiler/src/Builtins.cpp b/Compiler/src/Builtins.cpp index f3f41ef45..74e07a0ac 100644 --- a/Compiler/src/Builtins.cpp +++ b/Compiler/src/Builtins.cpp @@ -7,7 +7,6 @@ #include -LUAU_FASTFLAGVARIABLE(LuauCompileVectorLerp) LUAU_FASTFLAGVARIABLE(LuauCompileMathIsNanInfFinite) namespace Luau @@ -264,7 +263,7 @@ static int getBuiltinFunctionId(const Builtin& builtin, const CompileOptions& op return LBF_VECTOR_MIN; if (builtin.method == "max") return LBF_VECTOR_MAX; - if (FFlag::LuauCompileVectorLerp && builtin.method == "lerp") + if (builtin.method == "lerp") return LBF_VECTOR_LERP; } diff --git a/Compiler/src/Compiler.cpp b/Compiler/src/Compiler.cpp index 660d2c964..b13cc2a03 100644 --- a/Compiler/src/Compiler.cpp +++ b/Compiler/src/Compiler.cpp @@ -26,7 +26,6 @@ LUAU_FASTINTVARIABLE(LuauCompileLoopUnrollThresholdMaxBoost, 300) LUAU_FASTINTVARIABLE(LuauCompileInlineThreshold, 25) LUAU_FASTINTVARIABLE(LuauCompileInlineThresholdMaxBoost, 300) LUAU_FASTINTVARIABLE(LuauCompileInlineDepth, 5) -LUAU_FASTFLAG(LuauInterpStringConstFolding) LUAU_FASTFLAG(LuauExplicitTypeExpressionInstantiation) LUAU_FASTFLAGVARIABLE(LuauCompileCallCostModel) @@ -1947,65 +1946,33 @@ struct Compiler } size_t skippedSubExpr = 0; - if (FFlag::LuauInterpStringConstFolding) + for (size_t index = 0; index < expr->expressions.size; ++index) { - for (size_t index = 0; index < expr->expressions.size; ++index) + const Constant* c = constants.find(expr->expressions.data[index]); + if (c && c->type == Constant::Type::Type_String) { - const Constant* c = constants.find(expr->expressions.data[index]); - if (c && c->type == Constant::Type::Type_String) - { - formatCapacity += c->stringLength + std::count(c->valueString, c->valueString + c->stringLength, '%'); - skippedSubExpr++; - } - else - formatCapacity += 2; // "%*" + formatCapacity += c->stringLength + std::count(c->valueString, c->valueString + c->stringLength, '%'); + skippedSubExpr++; } + else + formatCapacity += 2; // "%*" } std::string formatString; formatString.reserve(formatCapacity); - if (FFlag::LuauInterpStringConstFolding) - { - LUAU_ASSERT(expr->strings.size == expr->expressions.size + 1); - for (size_t idx = 0; idx < expr->strings.size; idx++) - { - AstArray string = expr->strings.data[idx]; - escapeAndAppend(formatString, string.data, string.size); - - if (idx < expr->expressions.size) - { - const Constant* c = constants.find(expr->expressions.data[idx]); - if (c && c->type == Constant::Type::Type_String) - escapeAndAppend(formatString, c->valueString, c->stringLength); - else - formatString += "%*"; - } - } - } - else + LUAU_ASSERT(expr->strings.size == expr->expressions.size + 1); + for (size_t idx = 0; idx < expr->strings.size; idx++) { - size_t stringsLeft = expr->strings.size; + AstArray string = expr->strings.data[idx]; + escapeAndAppend(formatString, string.data, string.size); - for (AstArray string : expr->strings) + if (idx < expr->expressions.size) { - if (memchr(string.data, '%', string.size)) - { - for (size_t characterIndex = 0; characterIndex < string.size; ++characterIndex) - { - char character = string.data[characterIndex]; - formatString.push_back(character); - - if (character == '%') - formatString.push_back('%'); - } - } + const Constant* c = constants.find(expr->expressions.data[idx]); + if (c && c->type == Constant::Type::Type_String) + escapeAndAppend(formatString, c->valueString, c->stringLength); else - formatString.append(string.data, string.size); - - stringsLeft--; - - if (stringsLeft > 0) formatString += "%*"; } } @@ -2030,22 +1997,16 @@ struct Compiler emitLoadK(baseReg, formatStringIndex); - if (FFlag::LuauInterpStringConstFolding) + size_t skipped = 0; + for (size_t index = 0; index < expr->expressions.size; ++index) { - size_t skipped = 0; - for (size_t index = 0; index < expr->expressions.size; ++index) - { - AstExpr* subExpr = expr->expressions.data[index]; - const Constant* c = constants.find(subExpr); - if (!c || c->type != Constant::Type::Type_String) - compileExprTempTop(subExpr, uint8_t(baseReg + 2 + index - skipped)); - else - skipped++; - } + AstExpr* subExpr = expr->expressions.data[index]; + const Constant* c = constants.find(subExpr); + if (!c || c->type != Constant::Type::Type_String) + compileExprTempTop(subExpr, uint8_t(baseReg + 2 + index - skipped)); + else + skipped++; } - else - for (size_t index = 0; index < expr->expressions.size; ++index) - compileExprTempTop(expr->expressions.data[index], uint8_t(baseReg + 2 + index)); BytecodeBuilder::StringRef formatMethod = sref(AstName("format")); diff --git a/Compiler/src/ConstantFolding.cpp b/Compiler/src/ConstantFolding.cpp index c6e36dbd5..42e38bcb3 100644 --- a/Compiler/src/ConstantFolding.cpp +++ b/Compiler/src/ConstantFolding.cpp @@ -8,8 +8,6 @@ #include LUAU_FASTFLAG(LuauExplicitTypeExpressionInstantiation) -LUAU_FASTFLAGVARIABLE(LuauStringConstFolding2) -LUAU_FASTFLAGVARIABLE(LuauInterpStringConstFolding) namespace Luau { @@ -288,25 +286,24 @@ static void foldBinary(Constant& result, AstExprBinary::Op op, const Constant& l break; case AstExprBinary::Concat: - if (FFlag::LuauStringConstFolding2) - if (la.type == Constant::Type_String && ra.type == Constant::Type_String) + if (la.type == Constant::Type_String && ra.type == Constant::Type_String) + { + result.type = Constant::Type_String; + result.stringLength = la.stringLength + ra.stringLength; + if (la.stringLength == 0) + result.valueString = ra.valueString; + else if (ra.stringLength == 0) + result.valueString = la.valueString; + else { - result.type = Constant::Type_String; - result.stringLength = la.stringLength + ra.stringLength; - if (la.stringLength == 0) - result.valueString = ra.valueString; - else if (ra.stringLength == 0) - result.valueString = la.valueString; - else - { - std::string tmp; - tmp.reserve(result.stringLength + 1); - tmp.append(la.valueString, la.stringLength); - tmp.append(ra.valueString, ra.stringLength); - AstName name = stringTable.getOrAdd(tmp.c_str(), result.stringLength); - result.valueString = name.value; - } + std::string tmp; + tmp.reserve(result.stringLength + 1); + tmp.append(la.valueString, la.stringLength); + tmp.append(ra.valueString, ra.stringLength); + AstName name = stringTable.getOrAdd(tmp.c_str(), result.stringLength); + result.valueString = name.value; } + } break; case AstExprBinary::CompareNe: @@ -606,21 +603,13 @@ struct ConstantVisitor : AstVisitor } else if (AstExprInterpString* expr = node->as()) { - if (FFlag::LuauInterpStringConstFolding) - { - bool onlyConstantSubExpr = true; - for (AstExpr* expression : expr->expressions) - if (analyze(expression).type != Constant::Type_String) - onlyConstantSubExpr = false; + bool onlyConstantSubExpr = true; + for (AstExpr* expression : expr->expressions) + if (analyze(expression).type != Constant::Type_String) + onlyConstantSubExpr = false; - if (onlyConstantSubExpr) - foldInterpString(result, expr, constants, stringTable); - } - else - { - for (AstExpr* expression : expr->expressions) - analyze(expression); - } + if (onlyConstantSubExpr) + foldInterpString(result, expr, constants, stringTable); } else if (AstExprInstantiate* expr = node->as()) { diff --git a/Require/include/Luau/Require.h b/Require/include/Luau/Require.h index 584d201f2..5ba274d23 100644 --- a/Require/include/Luau/Require.h +++ b/Require/include/Luau/Require.h @@ -86,9 +86,14 @@ typedef struct luarequire_Configuration // alias's path cannot be resolved relative to its configuration file. luarequire_NavigateResult (*jump_to_alias)(lua_State* L, void* ctx, const char* path); - // Provides a final override opportunity if an alias cannot be found in - // configuration files. If NAVIGATE_SUCCESS is returned, this must update - // the internal state to point at the aliased module. Can be left undefined. + // Provides an initial alias override opportunity prior to searching for + // configuration files. If NAVIGATE_SUCCESS is returned, the internal state + // must be updated to point at the aliased location. Can be left undefined. + luarequire_NavigateResult (*to_alias_override)(lua_State* L, void* ctx, const char* alias_unprefixed); + + // Provides a final opportunity to resolve an alias if it cannot be found in + // configuration files. If NAVIGATE_SUCCESS is returned, the internal state + // must be updated to point at the aliased location. Can be left undefined. luarequire_NavigateResult (*to_alias_fallback)(lua_State* L, void* ctx, const char* alias_unprefixed); // Navigates through the context by making mutations to the internal state. diff --git a/Require/include/Luau/RequireNavigator.h b/Require/include/Luau/RequireNavigator.h index bf1acad6a..ceade9389 100644 --- a/Require/include/Luau/RequireNavigator.h +++ b/Require/include/Luau/RequireNavigator.h @@ -13,14 +13,13 @@ struct lua_State; //////////////////////////////////////////////////////////////////////////////// // -// The RequireNavigator library provides a C++ interface for navigating the -// context in which require-by-string operates. This is used internally by the -// require-by-string runtime library to resolve paths based on the rules defined -// by its consumers. +// This file provides a C++ interface for navigating the context in which +// require-by-string operates. This is used internally by the require-by-string +// runtime library to resolve paths based on the rules defined by its consumers. // -// Directly linking against this library allows for inspection of the -// require-by-string path resolution algorithm's behavior without enabling the -// runtime library, which is useful for static tooling as well. +// Including this file directly allows for inspection of the require-by-string +// path resolution algorithm's behavior without enabling the runtime library, +// which can be useful for static tooling. // //////////////////////////////////////////////////////////////////////////////// @@ -61,6 +60,11 @@ class NavigationContext virtual NavigateResult reset(const std::string& identifier) = 0; virtual NavigateResult jumpToAlias(const std::string& path) = 0; + + virtual NavigateResult toAliasOverride(const std::string& aliasUnprefixed) + { + return NavigateResult::NotFound; + }; virtual NavigateResult toAliasFallback(const std::string& aliasUnprefixed) { return NavigateResult::NotFound; @@ -123,6 +127,8 @@ class Navigator [[nodiscard]] Error jumpToAlias(const std::string& aliasPath); [[nodiscard]] Error navigateToParent(std::optional previousComponent); [[nodiscard]] Error navigateToChild(const std::string& component); + + [[nodiscard]] std::pair toAliasOverride(const std::string& aliasUnprefixed); [[nodiscard]] Error toAliasFallback(const std::string& aliasUnprefixed); NavigationContext& navigationContext; diff --git a/Require/src/Navigation.cpp b/Require/src/Navigation.cpp index 6bd9498c2..a6d502cdc 100644 --- a/Require/src/Navigation.cpp +++ b/Require/src/Navigation.cpp @@ -74,6 +74,13 @@ NavigationContext::NavigateResult RuntimeNavigationContext::jumpToAlias(const st return convertNavigateResult(config->jump_to_alias(L, ctx, path.c_str())); } +NavigationContext::NavigateResult RuntimeNavigationContext::toAliasOverride(const std::string& aliasUnprefixed) +{ + if (!config->to_alias_override) + return NavigationContext::NavigateResult::NotFound; + return convertNavigateResult(config->to_alias_override(L, ctx, aliasUnprefixed.c_str())); +} + NavigationContext::NavigateResult RuntimeNavigationContext::toAliasFallback(const std::string& aliasUnprefixed) { if (!config->to_alias_fallback) diff --git a/Require/src/Navigation.h b/Require/src/Navigation.h index 6136f29b5..00a478035 100644 --- a/Require/src/Navigation.h +++ b/Require/src/Navigation.h @@ -34,6 +34,8 @@ class RuntimeNavigationContext : public NavigationContext // Navigation interface NavigateResult reset(const std::string& requirerChunkname) override; NavigateResult jumpToAlias(const std::string& path) override; + + NavigateResult toAliasOverride(const std::string& aliasUnprefixed) override; NavigateResult toAliasFallback(const std::string& aliasUnprefixed) override; NavigateResult toParent() override; diff --git a/Require/src/RequireNavigator.cpp b/Require/src/RequireNavigator.cpp index a8c912a78..4523ba843 100644 --- a/Require/src/RequireNavigator.cpp +++ b/Require/src/RequireNavigator.cpp @@ -77,6 +77,18 @@ Error Navigator::navigateImpl(std::string_view path) } ); + if (auto [error, wasOverridden] = toAliasOverride(alias); error) + { + return error; + } + else if (wasOverridden) + { + if (Error error = navigateThroughPath(path)) + return error; + + return std::nullopt; + } + Config config; if (Error error = navigateToAndPopulateConfig(alias, config)) return error; @@ -176,6 +188,19 @@ Error Navigator::navigateToAlias(const std::string& alias, const Config& config, return error; std::string nextAlias = extractAlias(value); + + if (auto [error, wasOverridden] = toAliasOverride(nextAlias); error) + { + return error; + } + else if (wasOverridden) + { + if (Error error = navigateThroughPath(value)) + return error; + + return std::nullopt; + } + if (config.aliases.contains(nextAlias)) { if (Error error = navigateToAlias(nextAlias, config, std::move(cycleTracker))) @@ -322,6 +347,24 @@ Error Navigator::navigateToChild(const std::string& component) return errorMessage; } +std::pair Navigator::toAliasOverride(const std::string& aliasUnprefixed) +{ + std::pair result; + switch (navigationContext.toAliasOverride(aliasUnprefixed)) + { + case NavigationContext::NavigateResult::Success: + result = {std::nullopt, true}; + break; + case NavigationContext::NavigateResult::NotFound: + result = {std::nullopt, false}; + break; + case NavigationContext::NavigateResult::Ambiguous: + result = {"@" + aliasUnprefixed + " is not a valid alias (ambiguous)", false}; + break; + } + return result; +} + Error Navigator::toAliasFallback(const std::string& aliasUnprefixed) { NavigationContext::NavigateResult result = navigationContext.toAliasFallback(aliasUnprefixed); diff --git a/Sources.cmake b/Sources.cmake index 2c4336644..c735114e1 100644 --- a/Sources.cmake +++ b/Sources.cmake @@ -205,6 +205,7 @@ target_sources(Luau.Analysis PRIVATE Analysis/include/Luau/Instantiation.h Analysis/include/Luau/Instantiation2.h Analysis/include/Luau/IostreamHelpers.h + Analysis/include/Luau/IterativeTypeVisitor.h Analysis/include/Luau/JsonEmitter.h Analysis/include/Luau/Linter.h Analysis/include/Luau/LValue.h @@ -256,6 +257,7 @@ target_sources(Luau.Analysis PRIVATE Analysis/include/Luau/UnifierSharedState.h Analysis/include/Luau/UserDefinedTypeFunction.h Analysis/include/Luau/VisitType.h + Analysis/include/Luau/IterativeTypeVisitor.h Analysis/src/Anyification.cpp Analysis/src/ApplyTypeFunction.cpp @@ -286,6 +288,7 @@ target_sources(Luau.Analysis PRIVATE Analysis/src/Instantiation.cpp Analysis/src/Instantiation2.cpp Analysis/src/IostreamHelpers.cpp + Analysis/src/IterativeTypeVisitor.cpp Analysis/src/JsonEmitter.cpp Analysis/src/Linter.cpp Analysis/src/LValue.cpp diff --git a/VM/src/lbuiltins.cpp b/VM/src/lbuiltins.cpp index bcd217470..88ff7726d 100644 --- a/VM/src/lbuiltins.cpp +++ b/VM/src/lbuiltins.cpp @@ -25,8 +25,6 @@ #endif #endif -LUAU_FASTFLAG(LuauVectorLerp) - // luauF functions implement FASTCALL instruction that performs a direct execution of some builtin functions from the VM // The rule of thumb is that FASTCALL functions can not call user code, yield, fail, or reallocate stack. // If types of the arguments mismatch, luauF_* needs to return -1 and the execution will fall back to the usual call path @@ -1705,7 +1703,7 @@ static int luauF_vectormax(lua_State* L, StkId res, TValue* arg0, int nresults, static int luauF_vectorlerp(lua_State* L, StkId res, TValue* arg0, int nresults, StkId args, int nparams) { - if (FFlag::LuauVectorLerp && nparams >= 3 && nresults <= 1 && ttisvector(arg0) && ttisvector(args) && ttisnumber(args + 1)) + if (nparams >= 3 && nresults <= 1 && ttisvector(arg0) && ttisvector(args) && ttisnumber(args + 1)) { const float* a = vvalue(arg0); const float* b = vvalue(args); diff --git a/VM/src/lveclib.cpp b/VM/src/lveclib.cpp index 13cfb8473..89a24379c 100644 --- a/VM/src/lveclib.cpp +++ b/VM/src/lveclib.cpp @@ -6,8 +6,6 @@ #include -LUAU_FASTFLAGVARIABLE(LuauVectorLerp) - static int vector_create(lua_State* L) { // checking argument count to avoid accepting 'nil' as a valid value @@ -314,6 +312,7 @@ static const luaL_Reg vectorlib[] = { {"clamp", vector_clamp}, {"max", vector_max}, {"min", vector_min}, + {"lerp", vector_lerp}, {NULL, NULL}, }; @@ -343,13 +342,6 @@ int luaopen_vector(lua_State* L) { luaL_register(L, LUA_VECLIBNAME, vectorlib); - if (FFlag::LuauVectorLerp) - { - // To unflag put {"lerp", vector_lerp} in the `vectorlib` table - lua_pushcfunction(L, vector_lerp, "lerp"); - lua_setfield(L, -2, "lerp"); - } - #if LUA_VECTOR_SIZE == 4 lua_pushvector(L, 0.0f, 0.0f, 0.0f, 0.0f); lua_setfield(L, -2, "zero"); diff --git a/bench/tests/shootout/n-body-vector.lua b/bench/tests/shootout/n-body-vector.lua new file mode 100644 index 000000000..13a569f9b --- /dev/null +++ b/bench/tests/shootout/n-body-vector.lua @@ -0,0 +1,94 @@ +local function prequire(name) local success, result = pcall(require, name); return success and result end +local bench = script and require(script.Parent.bench_support) or prequire("bench_support") or require("../../bench_support") + +function test() + + --The Computer Language Benchmarks Game + -- https://salsa.debian.org/benchmarksgame-team/benchmarksgame/ + --contributed by Mike Pall + + local PI = 3.141592653589793 + local SOLAR_MASS = 4 * PI * PI + local DAYS_PER_YEAR = 365.24 + + type Body = { pos: vector, vel: vector, mass: number } + + local bodies: {Body} = { + { --Sun + pos = vector.create(0, 0, 0), + vel = vector.create(0, 0, 0), + mass = SOLAR_MASS + }, + { --Jupiter + pos = vector.create(4.84143144246472090e+00, -1.16032004402742839e+00, -1.03622044471123109e-01), + vel = vector.create(1.66007664274403694e-03 * DAYS_PER_YEAR, 7.69901118419740425e-03 * DAYS_PER_YEAR, -6.90460016972063023e-05 * DAYS_PER_YEAR), + mass = 9.54791938424326609e-04 * SOLAR_MASS + }, + { --Saturn + pos = vector.create(8.34336671824457987e+00, 4.12479856412430479e+00, -4.03523417114321381e-01), + vel = vector.create(-2.76742510726862411e-03 * DAYS_PER_YEAR, 4.99852801234917238e-03 * DAYS_PER_YEAR, 2.30417297573763929e-05 * DAYS_PER_YEAR), + mass = 2.85885980666130812e-04 * SOLAR_MASS + }, + { --Uranus + pos = vector.create(1.28943695621391310e+01, -1.51111514016986312e+01, -2.23307578892655734e-01), + vel = vector.create(2.96460137564761618e-03 * DAYS_PER_YEAR, 2.37847173959480950e-03 * DAYS_PER_YEAR, -2.96589568540237556e-05 * DAYS_PER_YEAR), + mass = 4.36624404335156298e-05 * SOLAR_MASS + }, + { --Neptune + pos = vector.create(1.53796971148509165e+01, -2.59193146099879641e+01, 1.79258772950371181e-01), + vel = vector.create(2.68067772490389322e-03 * DAYS_PER_YEAR, 1.62824170038242295e-03 * DAYS_PER_YEAR, -9.51592254519715870e-05 * DAYS_PER_YEAR), + mass = 5.15138902046611451e-05 * SOLAR_MASS + } + } + + local function advance(bodies: {Body}, nbody: number, dt: number) + for i = 1, nbody do + local bi = bodies[i] + local bipos, bimass = bi.pos, bi.mass + local bivel = bi.vel + + for j = i + 1, nbody do + local bj = bodies[j] + + local dpos = bipos - bj.pos + local distance = vector.magnitude(dpos) + + local mag = dt / (distance * distance * distance) + local bim, bjm = bimass * mag, bj.mass * mag + + bivel -= dpos * bjm + bj.vel += dpos * bim + end + + bi.vel = bivel + end + + for i = 1, nbody do + local bi = bodies[i] + bi.pos += dt * bi.vel + end + end + + local function offsetMomentum(bodies: {Body}, nbody: number) + local p = vector.create(0, 0, 0) + + for i = 1, nbody do + local bi = bodies[i] + p += bi.vel * bi.mass + end + + bodies[1].vel = -p / SOLAR_MASS + end + + local N = 20000 + local nbody = #bodies + + local ts0 = os.clock() + offsetMomentum(bodies, nbody) + for i = 1, N do advance(bodies, nbody, 0.01) end + local ts1 = os.clock() + + return ts1 - ts0 +end + +bench.runCode(test, "n-body-vec") \ No newline at end of file diff --git a/tests/AssemblyBuilderA64.test.cpp b/tests/AssemblyBuilderA64.test.cpp index f2107fa81..5ca814911 100644 --- a/tests/AssemblyBuilderA64.test.cpp +++ b/tests/AssemblyBuilderA64.test.cpp @@ -7,6 +7,8 @@ #include +LUAU_FASTFLAG(LuauCodegenUpvalueLoadProp) + using namespace Luau::CodeGen; using namespace Luau::CodeGen::A64; @@ -381,8 +383,11 @@ TEST_CASE_FIXTURE(AssemblyBuilderA64Fixture, "AddressOfLabel") TEST_CASE_FIXTURE(AssemblyBuilderA64Fixture, "FPBasic") { + ScopedFastFlag luauCodegenUpvalueLoadProp{FFlag::LuauCodegenUpvalueLoadProp, true}; + SINGLE_COMPARE(fmov(d0, d1), 0x1E604020); SINGLE_COMPARE(fmov(d0, x1), 0x9E670020); + SINGLE_COMPARE(fmov(x3, d2), 0x9E660043); } TEST_CASE_FIXTURE(AssemblyBuilderA64Fixture, "FPMath") @@ -466,6 +471,8 @@ TEST_CASE_FIXTURE(AssemblyBuilderA64Fixture, "FPInsertExtract") SINGLE_COMPARE(ins_4s(q31, 0, q29, 0), 0x6E0407BF); SINGLE_COMPARE(dup_4s(s29, q31, 2), 0x5E1407FD); SINGLE_COMPARE(dup_4s(q29, q30, 0), 0x4E0407DD); + SINGLE_COMPARE(umov_4s(w1, q30, 3), 0x0E1C3FC1); + SINGLE_COMPARE(umov_4s(w13, q1, 1), 0x0E0C3C2D); } TEST_CASE_FIXTURE(AssemblyBuilderA64Fixture, "FPCompare") @@ -476,18 +483,18 @@ TEST_CASE_FIXTURE(AssemblyBuilderA64Fixture, "FPCompare") TEST_CASE_FIXTURE(AssemblyBuilderA64Fixture, "FPImm") { - SINGLE_COMPARE(fmov(d0, 0), 0x2F00E400); + SINGLE_COMPARE(fmov(d0, 0.0), 0x2F00E400); SINGLE_COMPARE(fmov(d0, 0.125), 0x1E681000); SINGLE_COMPARE(fmov(d0, -0.125), 0x1E781000); SINGLE_COMPARE(fmov(d0, 1.9375), 0x1E6FF000); - SINGLE_COMPARE(fmov(q0, 0), 0x4F000400); + SINGLE_COMPARE(fmov(q0, 0.0), 0x4F000400); SINGLE_COMPARE(fmov(q0, 0.125), 0x4F02F400); SINGLE_COMPARE(fmov(q0, -0.125), 0x4F06F400); SINGLE_COMPARE(fmov(q0, 1.9375), 0x4F03F7E0); - CHECK(!AssemblyBuilderA64::isFmovSupported(-0.0)); - CHECK(!AssemblyBuilderA64::isFmovSupported(0.12389)); + CHECK(!AssemblyBuilderA64::isFmovSupportedFp64(-0.0)); + CHECK(!AssemblyBuilderA64::isFmovSupportedFp64(0.12389)); } TEST_CASE_FIXTURE(AssemblyBuilderA64Fixture, "AddressOffsetSize") @@ -596,6 +603,7 @@ TEST_CASE("LogTest") build.ins_4s(q31, 1, q29, 2); build.dup_4s(s29, q31, 2); build.dup_4s(q29, q30, 0); + build.umov_4s(w1, q30, 3); build.fmul(q0, q1, q2); build.fcmeq_4s(q2, q0, q1); @@ -642,6 +650,7 @@ TEST_CASE("LogTest") ins v31.s[1],v29.s[2] dup s29,v31.s[2] dup v29.4s,v30.s[0] + umov w1,v30.s[3] fmul v0.4s,v1.4s,v2.4s fcmeq v2.4s,v0.4s,v1.4s bit v1.16b,v0.16b,v2.16b diff --git a/tests/AssemblyBuilderX64.test.cpp b/tests/AssemblyBuilderX64.test.cpp index 5700307c2..05dbb8f01 100644 --- a/tests/AssemblyBuilderX64.test.cpp +++ b/tests/AssemblyBuilderX64.test.cpp @@ -7,6 +7,8 @@ #include +LUAU_FASTFLAG(LuauCodegenBufferLoadProp2) + using namespace Luau::CodeGen; using namespace Luau::CodeGen::X64; @@ -220,14 +222,24 @@ TEST_CASE_FIXTURE(AssemblyBuilderX64Fixture, "FormsOfMov") TEST_CASE_FIXTURE(AssemblyBuilderX64Fixture, "FormsOfMovExtended") { + ScopedFastFlag luauCodegenBufferLoadProp{FFlag::LuauCodegenBufferLoadProp2, true}; + SINGLE_COMPARE(movsx(eax, byte[rcx]), 0x0f, 0xbe, 0x01); SINGLE_COMPARE(movsx(r12, byte[r10]), 0x4d, 0x0f, 0xbe, 0x22); SINGLE_COMPARE(movsx(ebx, word[r11]), 0x41, 0x0f, 0xbf, 0x1b); SINGLE_COMPARE(movsx(rdx, word[rcx]), 0x48, 0x0f, 0xbf, 0x11); + SINGLE_COMPARE(movsx(edx, cl), 0x0f, 0xbe, 0xd1); + SINGLE_COMPARE(movsx(edx, r12b), 0x41, 0x0f, 0xbe, 0xd4); + SINGLE_COMPARE(movsx(edx, wordReg(ecx)), 0x0f, 0xbf, 0xd1); + SINGLE_COMPARE(movsx(edx, wordReg(r12d)), 0x41, 0x0f, 0xbf, 0xd4); SINGLE_COMPARE(movzx(eax, byte[rcx]), 0x0f, 0xb6, 0x01); SINGLE_COMPARE(movzx(r12, byte[r10]), 0x4d, 0x0f, 0xb6, 0x22); SINGLE_COMPARE(movzx(ebx, word[r11]), 0x41, 0x0f, 0xb7, 0x1b); SINGLE_COMPARE(movzx(rdx, word[rcx]), 0x48, 0x0f, 0xb7, 0x11); + SINGLE_COMPARE(movzx(edx, cl), 0x0f, 0xb6, 0xd1); + SINGLE_COMPARE(movzx(edx, r12b), 0x41, 0x0f, 0xb6, 0xd4); + SINGLE_COMPARE(movzx(edx, wordReg(ecx)), 0x0f, 0xb7, 0xd1); + SINGLE_COMPARE(movzx(edx, wordReg(r12d)), 0x41, 0x0f, 0xb7, 0xd4); } TEST_CASE_FIXTURE(AssemblyBuilderX64Fixture, "FormsOfTest") @@ -583,6 +595,9 @@ TEST_CASE_FIXTURE(AssemblyBuilderX64Fixture, "AVXTernaryInstructionForms") SINGLE_COMPARE(vpshufps(xmm7, xmm12, xmmword[rcx + r10], 0b11010100), 0xc4, 0xa1, 0x18, 0xc6, 0x3c, 0x11, 0xd4); SINGLE_COMPARE(vpinsrd(xmm7, xmm12, xmmword[rcx + r10], 2), 0xc4, 0xa3, 0x19, 0x22, 0x3c, 0x11, 0x02); + SINGLE_COMPARE(vpextrd(ecx, xmm5, 2), 0xc4, 0xe3, 0x79, 0x16, 0xe9, 0x02); + SINGLE_COMPARE(vpextrd(r10d, xmm9, 1), 0xc4, 0x43, 0x79, 0x16, 0xca, 0x01); + SINGLE_COMPARE(vdpps(xmm7, xmm12, xmmword[rcx + r10], 2), 0xc4, 0xa3, 0x19, 0x40, 0x3c, 0x11, 0x02); } @@ -650,6 +665,7 @@ TEST_CASE("LogTest") build.add(rdx, qword[rcx - 12]); build.pop(r12); build.cmov(ConditionX64::AboveEqual, rax, rbx); + build.vpextrd(ecx, xmm5, 2); build.ret(); build.int3(); @@ -696,6 +712,7 @@ TEST_CASE("LogTest") add rdx,qword ptr [rcx-0Ch] pop r12 cmovae rax,rbx + vpextrd ecx,xmm5,2 ret int3 nop diff --git a/tests/Autocomplete.test.cpp b/tests/Autocomplete.test.cpp index 7a75f8b6e..9007d7f0d 100644 --- a/tests/Autocomplete.test.cpp +++ b/tests/Autocomplete.test.cpp @@ -22,7 +22,6 @@ LUAU_FASTFLAG(LuauTraceTypesInNonstrictMode2) LUAU_FASTFLAG(LuauSetMetatableDoesNotTimeTravel) LUAU_FASTINT(LuauTypeInferRecursionLimit) LUAU_FASTFLAG(LuauDoNotSuggestGenericsInAnonFuncs) -LUAU_FASTFLAG(LuauAutocompleteAttributes) LUAU_FASTFLAG(LuauAutocompleteSingletonsInIndexer) using namespace Luau; @@ -4958,8 +4957,6 @@ TEST_CASE_FIXTURE(ACFixture, "autocomplete_method_in_unfinished_while_body") TEST_CASE_FIXTURE(ACBuiltinsFixture, "autocomplete_empty_attribute") { - ScopedFastFlag sff[]{{FFlag::LuauAutocompleteAttributes, true}}; - check(R"( \@@1 function foo() return 42 end @@ -4973,8 +4970,6 @@ TEST_CASE_FIXTURE(ACBuiltinsFixture, "autocomplete_empty_attribute") TEST_CASE_FIXTURE(ACBuiltinsFixture, "autocomplete_deprecated_attribute") { - ScopedFastFlag sff[]{{FFlag::LuauAutocompleteAttributes, true}}; - check(R"( \@dep@1 function foo() return 42 end @@ -4988,8 +4983,6 @@ TEST_CASE_FIXTURE(ACBuiltinsFixture, "autocomplete_deprecated_attribute") TEST_CASE_FIXTURE(ACBuiltinsFixture, "autocomplete_empty_braced_attribute") { - ScopedFastFlag sff[]{{FFlag::LuauAutocompleteAttributes, true}}; - check(R"( \@[@1] function foo() return 42 end @@ -5003,8 +4996,6 @@ TEST_CASE_FIXTURE(ACBuiltinsFixture, "autocomplete_empty_braced_attribute") TEST_CASE_FIXTURE(ACBuiltinsFixture, "autocomplete_deprecated_braced_attribute") { - ScopedFastFlag sff[]{{FFlag::LuauAutocompleteAttributes, true}}; - check(R"( \@[dep@1] function foo() return 42 end diff --git a/tests/Compiler.test.cpp b/tests/Compiler.test.cpp index 1ce4499ed..083097de7 100644 --- a/tests/Compiler.test.cpp +++ b/tests/Compiler.test.cpp @@ -23,10 +23,8 @@ LUAU_FASTINT(LuauCompileInlineThresholdMaxBoost) LUAU_FASTINT(LuauCompileLoopUnrollThreshold) LUAU_FASTINT(LuauCompileLoopUnrollThresholdMaxBoost) LUAU_FASTINT(LuauRecursionLimit) -LUAU_FASTFLAG(LuauStringConstFolding2) LUAU_FASTFLAG(LuauCompileStringCharSubFold) LUAU_FASTFLAG(LuauCompileTypeofFold) -LUAU_FASTFLAG(LuauInterpStringConstFolding) LUAU_FASTFLAG(LuauCompileMathIsNanInfFinite) LUAU_FASTFLAG(LuauCompileCallCostModel) @@ -1513,8 +1511,6 @@ TEST_CASE("InterpStringRegisterLimit") TEST_CASE("InterpStringConstFold") { - ScopedFastFlag sff{FFlag::LuauInterpStringConstFolding, true}; - CHECK_EQ( "\n" + compileFunction0(R"(local empty = ""; return `{empty}`)"), R"( @@ -7950,7 +7946,6 @@ TEST_CASE("InlineConstConditionals") { ScopedFastFlag luauCompileCallCostModel{FFlag::LuauCompileCallCostModel, true}; ScopedFastFlag luauCompileStringCharSubFold{FFlag::LuauCompileStringCharSubFold, true}; - ScopedFastFlag luauInterpStringConstFolding{FFlag::LuauInterpStringConstFolding, true}; // the most expensive part does not participate in cost model if branches are const CHECK_EQ( @@ -9952,8 +9947,6 @@ RETURN R1 7 TEST_CASE("ConstStringFolding") { - ScopedFastFlag sff{FFlag::LuauStringConstFolding2, true}; - CHECK_EQ( "\n" + compileFunction(R"(return "" .. "")", 0, 2), R"( diff --git a/tests/Conformance.test.cpp b/tests/Conformance.test.cpp index 04a996f6b..176ecd9c1 100644 --- a/tests/Conformance.test.cpp +++ b/tests/Conformance.test.cpp @@ -37,15 +37,13 @@ void luau_callhook(lua_State* L, lua_Hook hook, void* userdata); LUAU_FASTFLAG(DebugLuauAbortingChecks) LUAU_FASTFLAG(LuauExplicitTypeExpressionInstantiation) LUAU_FASTINT(CodegenHeuristicsInstructionLimit) -LUAU_FASTFLAG(LuauVectorLerp) -LUAU_FASTFLAG(LuauCompileVectorLerp) -LUAU_FASTFLAG(LuauTypeCheckerVectorLerp2) -LUAU_FASTFLAG(LuauCodeGenVectorLerp2) LUAU_FASTFLAG(LuauStacklessPcall) LUAU_FASTFLAG(LuauMathIsNanInfFinite) LUAU_FASTFLAG(LuauCompileMathIsNanInfFinite) LUAU_FASTFLAG(LuauTypeCheckerMathIsNanInfFinite) LUAU_FASTFLAG(LuauCodegenChainedSpills) +LUAU_FASTFLAG(LuauCodegenSpillRestoreFreeTemp) +LUAU_FASTFLAG(LuauCodegenDwordSpillSlots) static lua_CompileOptions defaultOptions() { @@ -554,6 +552,74 @@ static int lua_vec2_namecall(lua_State* L) luaL_error(L, "%s is not a valid method of vector", luaL_checkstring(L, 1)); } +Vertex* lua_vertex_push(lua_State* L) +{ + Vertex* data = (Vertex*)lua_newuserdatatagged(L, sizeof(Vertex), kTagVertex); + + lua_getuserdatametatable(L, kTagVertex); + lua_setmetatable(L, -2); + + return data; +} + +Vertex* lua_vertex_get(lua_State* L, int idx) +{ + Vertex* a = (Vertex*)lua_touserdatatagged(L, idx, kTagVertex); + + if (a) + return a; + + luaL_typeerror(L, idx, "vertex"); +} + +static int lua_vertex(lua_State* L) +{ + const float* pos = luaL_checkvector(L, 1); + const float* normal = luaL_checkvector(L, 2); + Vec2* uv = lua_vec2_get(L, 3); + + Vertex* data = lua_vertex_push(L); + + data->pos[0] = pos[0]; + data->pos[1] = pos[1]; + data->pos[2] = pos[2]; + data->normal[0] = normal[0]; + data->normal[1] = normal[1]; + data->normal[2] = normal[2]; + data->uv[0] = uv->x; + data->uv[1] = uv->y; + + return 1; +} + +static int lua_vertex_index(lua_State* L) +{ + Vertex* v = lua_vertex_get(L, 1); + const char* name = luaL_checkstring(L, 2); + + if (strcmp(name, "pos") == 0) + { + lua_pushvector(L, v->pos[0], v->pos[1], v->pos[2]); + return 1; + } + + if (strcmp(name, "normal") == 0) + { + lua_pushvector(L, v->normal[0], v->normal[1], v->normal[2]); + return 1; + } + + if (strcmp(name, "uv") == 0) + { + Vec2* uv = lua_vec2_push(L); + uv->x = v->uv[0]; + uv->y = v->uv[1]; + return 1; + } + + luaL_error(L, "%s is not a valid member of vertex", name); +} + void setupUserdataHelpers(lua_State* L) { // create metatable with all the metamethods @@ -668,6 +734,22 @@ void setupUserdataHelpers(lua_State* L) lua_setglobal(L, "vec2"); lua_pop(L, 1); + + // register vertex as well + luaL_newmetatable(L, "vertex"); + lua_pushvalue(L, -1); + lua_setuserdatametatable(L, kTagVertex); + + lua_pushcfunction(L, lua_vertex_index, nullptr); + lua_setfield(L, -2, "__index"); + + lua_setreadonly(L, -1, true); + + // ctor + lua_pushcfunction(L, lua_vertex, "vertex"); + lua_setglobal(L, "vertex"); + + lua_pop(L, 1); } static void setupNativeHelpers(lua_State* L) @@ -1256,13 +1338,6 @@ TEST_CASE("Vector") TEST_CASE("VectorLibrary") { - ScopedFastFlag _[]{ - {FFlag::LuauCompileVectorLerp, true}, - {FFlag::LuauTypeCheckerVectorLerp2, true}, - {FFlag::LuauVectorLerp, true}, - {FFlag::LuauCodeGenVectorLerp2, true} - }; - lua_CompileOptions copts = defaultOptions(); SUBCASE("O0") @@ -2857,16 +2932,17 @@ TEST_CASE("Interrupt") } }; - for (int test = 1; test <= 5; ++test) + for (int test = 1; test <= 6; ++test) { lua_State* T = lua_newthread(L); - std::string name = "strhang" + std::to_string(test); + std::string name = "hang" + std::to_string(test); lua_getglobal(T, name.c_str()); index = 0; int status = lua_resume(T, nullptr, 0); CHECK(status == LUA_ERRRUN); + CHECK(strstr(luaL_checkstring(T, -1), "timeout") != nullptr); lua_pop(L, 1); } @@ -2874,7 +2950,7 @@ TEST_CASE("Interrupt") { lua_State* T = lua_newthread(L); - lua_getglobal(T, "strhangpcall"); + lua_getglobal(T, "hangpcall"); index = 0; int status = lua_resume(T, nullptr, 0); @@ -3364,6 +3440,8 @@ TEST_CASE("SafeEnv") TEST_CASE("Native") { + ScopedFastFlag luauCodegenSpillRestoreFreeTemp{FFlag::LuauCodegenSpillRestoreFreeTemp, true}; + // This tests requires code to run natively, otherwise all 'is_native' checks will fail if (!codegen || !luau_codegen_supported()) return; @@ -3421,6 +3499,7 @@ TEST_CASE("Native") TEST_CASE("NativeIntegerSpills") { ScopedFastFlag luauCodegenChainedSpills{FFlag::LuauCodegenChainedSpills, true}; + ScopedFastFlag luauCodegenDwordSpillSlots{FFlag::LuauCodegenDwordSpillSlots, true}; lua_CompileOptions copts = defaultOptions(); @@ -3461,7 +3540,7 @@ TEST_CASE("NativeUserdata") lua_CompileOptions copts = defaultOptions(); Luau::CodeGen::CompilationOptions nativeOpts = defaultCodegenOptions(); - static const char* kUserdataCompileTypes[] = {"vec2", "color", "mat3", nullptr}; + static const char* kUserdataCompileTypes[] = {"vec2", "color", "mat3", "vertex", nullptr}; copts.userdataTypes = kUserdataCompileTypes; SUBCASE("NoIrHooks") diff --git a/tests/ConformanceIrHooks.h b/tests/ConformanceIrHooks.h index f3bfca8c9..7df0d8939 100644 --- a/tests/ConformanceIrHooks.h +++ b/tests/ConformanceIrHooks.h @@ -3,6 +3,8 @@ #include "Luau/IrBuilder.h" +LUAU_FASTFLAG(LuauCodegenSplitFloat) + static const char* kUserdataRunTypes[] = {"extra", "color", "vec2", "mat3", "vertex", nullptr}; constexpr uint8_t kUserdataExtra = 0; @@ -67,6 +69,13 @@ inline bool vectorAccess(Luau::CodeGen::IrBuilder& build, const char* member, si IrOp y = build.inst(IrCmd::LOAD_FLOAT, build.vmReg(sourceReg), build.constInt(4)); IrOp z = build.inst(IrCmd::LOAD_FLOAT, build.vmReg(sourceReg), build.constInt(8)); + if (FFlag::LuauCodegenSplitFloat) + { + x = build.inst(IrCmd::FLOAT_TO_NUM, x); + y = build.inst(IrCmd::FLOAT_TO_NUM, y); + z = build.inst(IrCmd::FLOAT_TO_NUM, z); + } + IrOp x2 = build.inst(IrCmd::MUL_NUM, x, x); IrOp y2 = build.inst(IrCmd::MUL_NUM, y, y); IrOp z2 = build.inst(IrCmd::MUL_NUM, z, z); @@ -87,6 +96,13 @@ inline bool vectorAccess(Luau::CodeGen::IrBuilder& build, const char* member, si IrOp y = build.inst(IrCmd::LOAD_FLOAT, build.vmReg(sourceReg), build.constInt(4)); IrOp z = build.inst(IrCmd::LOAD_FLOAT, build.vmReg(sourceReg), build.constInt(8)); + if (FFlag::LuauCodegenSplitFloat) + { + x = build.inst(IrCmd::FLOAT_TO_NUM, x); + y = build.inst(IrCmd::FLOAT_TO_NUM, y); + z = build.inst(IrCmd::FLOAT_TO_NUM, z); + } + IrOp x2 = build.inst(IrCmd::MUL_NUM, x, x); IrOp y2 = build.inst(IrCmd::MUL_NUM, y, y); IrOp z2 = build.inst(IrCmd::MUL_NUM, z, z); @@ -100,6 +116,13 @@ inline bool vectorAccess(Luau::CodeGen::IrBuilder& build, const char* member, si IrOp yr = build.inst(IrCmd::MUL_NUM, y, inv); IrOp zr = build.inst(IrCmd::MUL_NUM, z, inv); + if (FFlag::LuauCodegenSplitFloat) + { + xr = build.inst(IrCmd::NUM_TO_FLOAT, xr); + yr = build.inst(IrCmd::NUM_TO_FLOAT, yr); + zr = build.inst(IrCmd::NUM_TO_FLOAT, zr); + } + build.inst(IrCmd::STORE_VECTOR, build.vmReg(resultReg), xr, yr, zr); build.inst(IrCmd::STORE_TAG, build.vmReg(resultReg), build.constTag(LUA_TVECTOR)); @@ -139,14 +162,35 @@ inline bool vectorNamecall( IrOp x1 = build.inst(IrCmd::LOAD_FLOAT, build.vmReg(sourceReg), build.constInt(0)); IrOp x2 = build.inst(IrCmd::LOAD_FLOAT, build.vmReg(argResReg + 2), build.constInt(0)); + + if (FFlag::LuauCodegenSplitFloat) + { + x1 = build.inst(IrCmd::FLOAT_TO_NUM, x1); + x2 = build.inst(IrCmd::FLOAT_TO_NUM, x2); + } + IrOp xx = build.inst(IrCmd::MUL_NUM, x1, x2); IrOp y1 = build.inst(IrCmd::LOAD_FLOAT, build.vmReg(sourceReg), build.constInt(4)); IrOp y2 = build.inst(IrCmd::LOAD_FLOAT, build.vmReg(argResReg + 2), build.constInt(4)); + + if (FFlag::LuauCodegenSplitFloat) + { + y1 = build.inst(IrCmd::FLOAT_TO_NUM, y1); + y2 = build.inst(IrCmd::FLOAT_TO_NUM, y2); + } + IrOp yy = build.inst(IrCmd::MUL_NUM, y1, y2); IrOp z1 = build.inst(IrCmd::LOAD_FLOAT, build.vmReg(sourceReg), build.constInt(8)); IrOp z2 = build.inst(IrCmd::LOAD_FLOAT, build.vmReg(argResReg + 2), build.constInt(8)); + + if (FFlag::LuauCodegenSplitFloat) + { + z1 = build.inst(IrCmd::FLOAT_TO_NUM, z1); + z2 = build.inst(IrCmd::FLOAT_TO_NUM, z2); + } + IrOp zz = build.inst(IrCmd::MUL_NUM, z1, z2); IrOp sum = build.inst(IrCmd::ADD_NUM, build.inst(IrCmd::ADD_NUM, xx, yy), zz); @@ -174,6 +218,18 @@ inline bool vectorNamecall( IrOp z1 = build.inst(IrCmd::LOAD_FLOAT, build.vmReg(sourceReg), build.constInt(8)); IrOp z2 = build.inst(IrCmd::LOAD_FLOAT, build.vmReg(argResReg + 2), build.constInt(8)); + if (FFlag::LuauCodegenSplitFloat) + { + x1 = build.inst(IrCmd::FLOAT_TO_NUM, x1); + x2 = build.inst(IrCmd::FLOAT_TO_NUM, x2); + + y1 = build.inst(IrCmd::FLOAT_TO_NUM, y1); + y2 = build.inst(IrCmd::FLOAT_TO_NUM, y2); + + z1 = build.inst(IrCmd::FLOAT_TO_NUM, z1); + z2 = build.inst(IrCmd::FLOAT_TO_NUM, z2); + } + IrOp y1z2 = build.inst(IrCmd::MUL_NUM, y1, z2); IrOp z1y2 = build.inst(IrCmd::MUL_NUM, z1, y2); IrOp xr = build.inst(IrCmd::SUB_NUM, y1z2, z1y2); @@ -186,6 +242,13 @@ inline bool vectorNamecall( IrOp y1x2 = build.inst(IrCmd::MUL_NUM, y1, x2); IrOp zr = build.inst(IrCmd::SUB_NUM, x1y2, y1x2); + if (FFlag::LuauCodegenSplitFloat) + { + xr = build.inst(IrCmd::NUM_TO_FLOAT, xr); + yr = build.inst(IrCmd::NUM_TO_FLOAT, yr); + zr = build.inst(IrCmd::NUM_TO_FLOAT, zr); + } + build.inst(IrCmd::STORE_VECTOR, build.vmReg(argResReg), xr, yr, zr); build.inst(IrCmd::STORE_TAG, build.vmReg(argResReg), build.constTag(LUA_TVECTOR)); @@ -275,6 +338,9 @@ inline bool userdataAccess( IrOp value = build.inst(IrCmd::BUFFER_READF32, udata, build.constInt(offsetof(Vec2, x)), build.constTag(LUA_TUSERDATA)); + if (FFlag::LuauCodegenSplitFloat) + value = build.inst(IrCmd::FLOAT_TO_NUM, value); + build.inst(IrCmd::STORE_DOUBLE, build.vmReg(resultReg), value); build.inst(IrCmd::STORE_TAG, build.vmReg(resultReg), build.constTag(LUA_TNUMBER)); return true; @@ -287,6 +353,9 @@ inline bool userdataAccess( IrOp value = build.inst(IrCmd::BUFFER_READF32, udata, build.constInt(offsetof(Vec2, y)), build.constTag(LUA_TUSERDATA)); + if (FFlag::LuauCodegenSplitFloat) + value = build.inst(IrCmd::FLOAT_TO_NUM, value); + build.inst(IrCmd::STORE_DOUBLE, build.vmReg(resultReg), value); build.inst(IrCmd::STORE_TAG, build.vmReg(resultReg), build.constTag(LUA_TNUMBER)); return true; @@ -300,6 +369,12 @@ inline bool userdataAccess( IrOp x = build.inst(IrCmd::BUFFER_READF32, udata, build.constInt(offsetof(Vec2, x)), build.constTag(LUA_TUSERDATA)); IrOp y = build.inst(IrCmd::BUFFER_READF32, udata, build.constInt(offsetof(Vec2, y)), build.constTag(LUA_TUSERDATA)); + if (FFlag::LuauCodegenSplitFloat) + { + x = build.inst(IrCmd::FLOAT_TO_NUM, x); + y = build.inst(IrCmd::FLOAT_TO_NUM, y); + } + IrOp x2 = build.inst(IrCmd::MUL_NUM, x, x); IrOp y2 = build.inst(IrCmd::MUL_NUM, y, y); @@ -320,6 +395,12 @@ inline bool userdataAccess( IrOp x = build.inst(IrCmd::BUFFER_READF32, udata, build.constInt(offsetof(Vec2, x)), build.constTag(LUA_TUSERDATA)); IrOp y = build.inst(IrCmd::BUFFER_READF32, udata, build.constInt(offsetof(Vec2, y)), build.constTag(LUA_TUSERDATA)); + if (FFlag::LuauCodegenSplitFloat) + { + x = build.inst(IrCmd::FLOAT_TO_NUM, x); + y = build.inst(IrCmd::FLOAT_TO_NUM, y); + } + IrOp x2 = build.inst(IrCmd::MUL_NUM, x, x); IrOp y2 = build.inst(IrCmd::MUL_NUM, y, y); @@ -331,6 +412,12 @@ inline bool userdataAccess( IrOp xr = build.inst(IrCmd::MUL_NUM, x, inv); IrOp yr = build.inst(IrCmd::MUL_NUM, y, inv); + if (FFlag::LuauCodegenSplitFloat) + { + xr = build.inst(IrCmd::NUM_TO_FLOAT, xr); + yr = build.inst(IrCmd::NUM_TO_FLOAT, yr); + } + build.inst(IrCmd::CHECK_GC); IrOp udatar = build.inst(IrCmd::NEW_USERDATA, build.constInt(sizeof(Vec2)), build.constInt(kTagVec2)); @@ -448,12 +535,32 @@ inline bool userdataMetamethod( IrOp x1 = build.inst(IrCmd::BUFFER_READF32, udata1, build.constInt(offsetof(Vec2, x)), build.constTag(LUA_TUSERDATA)); IrOp x2 = build.inst(IrCmd::BUFFER_READF32, udata2, build.constInt(offsetof(Vec2, x)), build.constTag(LUA_TUSERDATA)); + + if (FFlag::LuauCodegenSplitFloat) + { + x1 = build.inst(IrCmd::FLOAT_TO_NUM, x1); + x2 = build.inst(IrCmd::FLOAT_TO_NUM, x2); + } + IrOp mx = build.inst(IrCmd::ADD_NUM, x1, x2); IrOp y1 = build.inst(IrCmd::BUFFER_READF32, udata1, build.constInt(offsetof(Vec2, y)), build.constTag(LUA_TUSERDATA)); IrOp y2 = build.inst(IrCmd::BUFFER_READF32, udata2, build.constInt(offsetof(Vec2, y)), build.constTag(LUA_TUSERDATA)); + + if (FFlag::LuauCodegenSplitFloat) + { + y1 = build.inst(IrCmd::FLOAT_TO_NUM, y1); + y2 = build.inst(IrCmd::FLOAT_TO_NUM, y2); + } + IrOp my = build.inst(IrCmd::ADD_NUM, y1, y2); + if (FFlag::LuauCodegenSplitFloat) + { + mx = build.inst(IrCmd::NUM_TO_FLOAT, mx); + my = build.inst(IrCmd::NUM_TO_FLOAT, my); + } + build.inst(IrCmd::CHECK_GC); IrOp udatar = build.inst(IrCmd::NEW_USERDATA, build.constInt(sizeof(Vec2)), build.constInt(kTagVec2)); @@ -480,12 +587,32 @@ inline bool userdataMetamethod( IrOp x1 = build.inst(IrCmd::BUFFER_READF32, udata1, build.constInt(offsetof(Vec2, x)), build.constTag(LUA_TUSERDATA)); IrOp x2 = build.inst(IrCmd::BUFFER_READF32, udata2, build.constInt(offsetof(Vec2, x)), build.constTag(LUA_TUSERDATA)); + + if (FFlag::LuauCodegenSplitFloat) + { + x1 = build.inst(IrCmd::FLOAT_TO_NUM, x1); + x2 = build.inst(IrCmd::FLOAT_TO_NUM, x2); + } + IrOp mx = build.inst(IrCmd::MUL_NUM, x1, x2); IrOp y1 = build.inst(IrCmd::BUFFER_READF32, udata1, build.constInt(offsetof(Vec2, y)), build.constTag(LUA_TUSERDATA)); IrOp y2 = build.inst(IrCmd::BUFFER_READF32, udata2, build.constInt(offsetof(Vec2, y)), build.constTag(LUA_TUSERDATA)); + + if (FFlag::LuauCodegenSplitFloat) + { + y1 = build.inst(IrCmd::FLOAT_TO_NUM, y1); + y2 = build.inst(IrCmd::FLOAT_TO_NUM, y2); + } + IrOp my = build.inst(IrCmd::MUL_NUM, y1, y2); + if (FFlag::LuauCodegenSplitFloat) + { + mx = build.inst(IrCmd::NUM_TO_FLOAT, mx); + my = build.inst(IrCmd::NUM_TO_FLOAT, my); + } + build.inst(IrCmd::CHECK_GC); IrOp udatar = build.inst(IrCmd::NEW_USERDATA, build.constInt(sizeof(Vec2)), build.constInt(kTagVec2)); @@ -508,9 +635,22 @@ inline bool userdataMetamethod( IrOp x = build.inst(IrCmd::BUFFER_READF32, udata1, build.constInt(offsetof(Vec2, x)), build.constTag(LUA_TUSERDATA)); IrOp y = build.inst(IrCmd::BUFFER_READF32, udata1, build.constInt(offsetof(Vec2, y)), build.constTag(LUA_TUSERDATA)); + + if (FFlag::LuauCodegenSplitFloat) + { + x = build.inst(IrCmd::FLOAT_TO_NUM, x); + y = build.inst(IrCmd::FLOAT_TO_NUM, y); + } + IrOp mx = build.inst(IrCmd::UNM_NUM, x); IrOp my = build.inst(IrCmd::UNM_NUM, y); + if (FFlag::LuauCodegenSplitFloat) + { + mx = build.inst(IrCmd::NUM_TO_FLOAT, mx); + my = build.inst(IrCmd::NUM_TO_FLOAT, my); + } + build.inst(IrCmd::CHECK_GC); IrOp udatar = build.inst(IrCmd::NEW_USERDATA, build.constInt(sizeof(Vec2)), build.constInt(kTagVec2)); @@ -581,10 +721,24 @@ inline bool userdataNamecall( IrOp x1 = build.inst(IrCmd::BUFFER_READF32, udata1, build.constInt(offsetof(Vec2, x)), build.constTag(LUA_TUSERDATA)); IrOp x2 = build.inst(IrCmd::BUFFER_READF32, udata2, build.constInt(offsetof(Vec2, x)), build.constTag(LUA_TUSERDATA)); + + if (FFlag::LuauCodegenSplitFloat) + { + x1 = build.inst(IrCmd::FLOAT_TO_NUM, x1); + x2 = build.inst(IrCmd::FLOAT_TO_NUM, x2); + } + IrOp xx = build.inst(IrCmd::MUL_NUM, x1, x2); IrOp y1 = build.inst(IrCmd::BUFFER_READF32, udata1, build.constInt(offsetof(Vec2, y)), build.constTag(LUA_TUSERDATA)); IrOp y2 = build.inst(IrCmd::BUFFER_READF32, udata2, build.constInt(offsetof(Vec2, y)), build.constTag(LUA_TUSERDATA)); + + if (FFlag::LuauCodegenSplitFloat) + { + y1 = build.inst(IrCmd::FLOAT_TO_NUM, y1); + y2 = build.inst(IrCmd::FLOAT_TO_NUM, y2); + } + IrOp yy = build.inst(IrCmd::MUL_NUM, y1, y2); IrOp sum = build.inst(IrCmd::ADD_NUM, xx, yy); @@ -611,12 +765,32 @@ inline bool userdataNamecall( IrOp x1 = build.inst(IrCmd::BUFFER_READF32, udata1, build.constInt(offsetof(Vec2, x)), build.constTag(LUA_TUSERDATA)); IrOp x2 = build.inst(IrCmd::BUFFER_READF32, udata2, build.constInt(offsetof(Vec2, x)), build.constTag(LUA_TUSERDATA)); + + if (FFlag::LuauCodegenSplitFloat) + { + x1 = build.inst(IrCmd::FLOAT_TO_NUM, x1); + x2 = build.inst(IrCmd::FLOAT_TO_NUM, x2); + } + IrOp mx = build.inst(IrCmd::MIN_NUM, x1, x2); IrOp y1 = build.inst(IrCmd::BUFFER_READF32, udata1, build.constInt(offsetof(Vec2, y)), build.constTag(LUA_TUSERDATA)); IrOp y2 = build.inst(IrCmd::BUFFER_READF32, udata2, build.constInt(offsetof(Vec2, y)), build.constTag(LUA_TUSERDATA)); + + if (FFlag::LuauCodegenSplitFloat) + { + y1 = build.inst(IrCmd::FLOAT_TO_NUM, y1); + y2 = build.inst(IrCmd::FLOAT_TO_NUM, y2); + } + IrOp my = build.inst(IrCmd::MIN_NUM, y1, y2); + if (FFlag::LuauCodegenSplitFloat) + { + mx = build.inst(IrCmd::NUM_TO_FLOAT, mx); + my = build.inst(IrCmd::NUM_TO_FLOAT, my); + } + build.inst(IrCmd::CHECK_GC); IrOp udatar = build.inst(IrCmd::NEW_USERDATA, build.constInt(sizeof(Vec2)), build.constInt(kTagVec2)); diff --git a/tests/Error.test.cpp b/tests/Error.test.cpp index 87a07b203..f7581c885 100644 --- a/tests/Error.test.cpp +++ b/tests/Error.test.cpp @@ -7,6 +7,7 @@ using namespace Luau; LUAU_FASTFLAG(LuauSolverV2) +LUAU_FASTFLAG(LuauBetterTypeMismatchErrors) TEST_SUITE_BEGIN("ErrorTests"); @@ -33,7 +34,10 @@ local x: Account = 5 LUAU_REQUIRE_ERROR_COUNT(1, result); - CHECK_EQ("Type 'number' could not be converted into 'Account'", toString(result.errors[0])); + if (FFlag::LuauBetterTypeMismatchErrors) + CHECK_EQ("Expected this to be 'Account', but got 'number'", toString(result.errors[0])); + else + CHECK_EQ("Type 'number' could not be converted into 'Account'", toString(result.errors[0])); } TEST_CASE_FIXTURE(BuiltinsFixture, "binary_op_type_function_errors") @@ -52,6 +56,8 @@ TEST_CASE_FIXTURE(BuiltinsFixture, "binary_op_type_function_errors") "Operator '+' could not be applied to operands of types number and string; there is no corresponding overload for __add", toString(result.errors[0]) ); + else if (FFlag::LuauBetterTypeMismatchErrors) + CHECK_EQ("Expected this to be 'number', but got 'string'", toString(result.errors[0])); else CHECK_EQ("Type 'string' could not be converted into 'number'", toString(result.errors[0])); } @@ -72,12 +78,19 @@ TEST_CASE_FIXTURE(BuiltinsFixture, "unary_op_type_function_errors") CHECK_EQ( "Operator '-' could not be applied to operand of type string; there is no corresponding overload for __unm", toString(result.errors[0]) ); - CHECK_EQ("Type 'string' could not be converted into 'number'", toString(result.errors[1])); + + if (FFlag::LuauBetterTypeMismatchErrors) + CHECK_EQ("Expected this to be 'number', but got 'string'", toString(result.errors[1])); + else + CHECK_EQ("Type 'string' could not be converted into 'number'", toString(result.errors[1])); } else { LUAU_REQUIRE_ERROR_COUNT(1, result); - CHECK_EQ("Type 'string' could not be converted into 'number'", toString(result.errors[0])); + if (FFlag::LuauBetterTypeMismatchErrors) + CHECK_EQ("Expected this to be 'number', but got 'string'", toString(result.errors[0])); + else + CHECK_EQ("Type 'string' could not be converted into 'number'", toString(result.errors[0])); } } diff --git a/tests/Fixture.cpp b/tests/Fixture.cpp index 2aa79db89..af8a0a6fa 100644 --- a/tests/Fixture.cpp +++ b/tests/Fixture.cpp @@ -133,7 +133,7 @@ std::vector> TestRequireNode::getChildren() const std::vector TestRequireNode::getAvailableAliases() const { - return {{"defaultalias"}}; + return {RequireAlias("defaultalias")}; } std::unique_ptr TestRequireSuggester::getNode(const ModuleName& name) const @@ -559,6 +559,18 @@ TypeId Fixture::requireExportedType(const ModuleName& moduleName, const std::str return it->second.type; } +TypeId Fixture::parseType(std::string_view src) +{ + return getFrontend().parseType( + NotNull{&allocator}, + NotNull{&nameTable}, + NotNull{&getFrontend().iceHandler}, + TypeCheckLimits{}, + NotNull{&arena}, + src + ); +} + std::string Fixture::decorateWithTypes(const std::string& code) { fileResolver.source[mainModuleName] = code; diff --git a/tests/Fixture.h b/tests/Fixture.h index 8452ea81e..3155d4edf 100644 --- a/tests/Fixture.h +++ b/tests/Fixture.h @@ -28,7 +28,6 @@ LUAU_FASTFLAG(DebugLuauFreezeArena) LUAU_FASTFLAG(DebugLuauForceAllNewSolverTests) -LUAU_FASTFLAG(LuauTidyTypeUtils) LUAU_FASTFLAG(DebugLuauAlwaysShowConstraintSolvingIncomplete); #define DOES_NOT_PASS_NEW_SOLVER_GUARD_IMPL(line) ScopedFastFlag sff_##line{FFlag::LuauSolverV2, FFlag::DebugLuauForceAllNewSolverTests}; @@ -109,6 +108,10 @@ struct TestConfigResolver : ConfigResolver struct Fixture { explicit Fixture(bool prepareAutocomplete = false); + + explicit Fixture(const Fixture&) = delete; + Fixture& operator=(const Fixture&) = delete; + ~Fixture(); // Throws Luau::ParseErrors if the parse fails. @@ -145,13 +148,13 @@ struct Fixture TypeId requireTypeAlias(const std::string& name); TypeId requireExportedType(const ModuleName& moduleName, const std::string& name); + TypeId parseType(std::string_view src); + // While most flags can be flipped inside the unit test, some code changes affect the state that is part of Fixture initialization // Most often those are changes related to builtin type definitions. // In that case, flag can be forced to 'true' using the example below: // ScopedFastFlag sff_LuauExampleFlagDefinition{FFlag::LuauExampleFlagDefinition, true}; - ScopedFastFlag sff_TypeUtilTidy{FFlag::LuauTidyTypeUtils, 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. ScopedFastFlag sff_DebugLuauFreezeArena{FFlag::DebugLuauFreezeArena, true}; @@ -164,7 +167,9 @@ struct Fixture NullModuleResolver moduleResolver; std::unique_ptr sourceModule; InternalErrorReporter ice; - + Allocator allocator; + AstNameTable nameTable{allocator}; + TypeArena arena; std::string decorateWithTypes(const std::string& code); diff --git a/tests/Frontend.test.cpp b/tests/Frontend.test.cpp index c6511ed2b..2bce94e5e 100644 --- a/tests/Frontend.test.cpp +++ b/tests/Frontend.test.cpp @@ -18,6 +18,7 @@ LUAU_FASTFLAG(LuauSolverV2); LUAU_FASTFLAG(LuauStandaloneParseType) LUAU_FASTFLAG(DebugLuauFreezeArena) LUAU_FASTFLAG(DebugLuauMagicTypes) +LUAU_FASTFLAG(LuauBetterTypeMismatchErrors) namespace { @@ -1258,7 +1259,10 @@ TEST_CASE_FIXTURE(FrontendFixture, "parse_only") LUAU_REQUIRE_ERROR_COUNT(1, result); CHECK_EQ("game/Gui/Modules/A", result.errors[0].moduleName); - CHECK_EQ("Type 'string' could not be converted into 'number'", toString(result.errors[0])); + if (FFlag::LuauBetterTypeMismatchErrors) + CHECK_EQ("Expected this to be 'number', but got 'string'", toString(result.errors[0])); + else + CHECK_EQ("Type 'string' could not be converted into 'number'", toString(result.errors[0])); } TEST_CASE_FIXTURE(FrontendFixture, "markdirty_early_return") diff --git a/tests/IrBuilder.test.cpp b/tests/IrBuilder.test.cpp index 854fcac9f..36bbdaac6 100644 --- a/tests/IrBuilder.test.cpp +++ b/tests/IrBuilder.test.cpp @@ -14,6 +14,13 @@ LUAU_FASTFLAG(DebugLuauAbortingChecks) LUAU_FASTFLAG(LuauCodegenStorePriority) +LUAU_FASTFLAG(LuauCodegenBetterSccRemoval) +LUAU_FASTFLAG(LuauCodegenLinearAndOr) +LUAU_FASTFLAG(LuauCodegenHydrateLoadWithTag) +LUAU_FASTFLAG(LuauCodegenNumToUintFoldRange) +LUAU_FASTFLAG(LuauCodegenNumIntFolds2) +LUAU_FASTFLAG(LuauCodegenBufferLoadProp2) +LUAU_FASTFLAG(LuauCodegenGcoDse) using namespace Luau::CodeGen; @@ -367,6 +374,11 @@ TEST_CASE_FIXTURE(IrBuilderFixture, "Numeric") build.inst(IrCmd::STORE_DOUBLE, build.vmReg(22), build.inst(IrCmd::SIGN_NUM, build.constDouble(-4))); + build.inst(IrCmd::STORE_INT, build.vmReg(23), build.inst(IrCmd::SEXTI8_INT, build.constInt(0x7f))); + build.inst(IrCmd::STORE_INT, build.vmReg(24), build.inst(IrCmd::SEXTI8_INT, build.constInt(0xf1))); + build.inst(IrCmd::STORE_INT, build.vmReg(25), build.inst(IrCmd::SEXTI16_INT, build.constInt(0x7fff))); + build.inst(IrCmd::STORE_INT, build.vmReg(26), build.inst(IrCmd::SEXTI16_INT, build.constInt(0xf111))); + build.inst(IrCmd::RETURN, build.constUint(0)); updateUseCounts(build.function); @@ -396,6 +408,10 @@ TEST_CASE_FIXTURE(IrBuilderFixture, "Numeric") STORE_INT R20, 1i STORE_INT R21, 0i STORE_DOUBLE R22, -1 + STORE_INT R23, 127i + STORE_INT R24, -15i + STORE_INT R25, 32767i + STORE_INT R26, -3823i RETURN 0u )"); @@ -403,6 +419,8 @@ TEST_CASE_FIXTURE(IrBuilderFixture, "Numeric") TEST_CASE_FIXTURE(IrBuilderFixture, "NumericConversions") { + ScopedFastFlag luauCodegenNumToUintFoldRange{FFlag::LuauCodegenNumToUintFoldRange, true}; + IrOp block = build.block(IrBlockKind::Internal); build.beginBlock(block); @@ -411,6 +429,8 @@ TEST_CASE_FIXTURE(IrBuilderFixture, "NumericConversions") build.inst(IrCmd::STORE_DOUBLE, build.vmReg(1), build.inst(IrCmd::UINT_TO_NUM, build.constInt(0xdeee0000u))); build.inst(IrCmd::STORE_INT, build.vmReg(2), build.inst(IrCmd::NUM_TO_INT, build.constDouble(200.0))); build.inst(IrCmd::STORE_INT, build.vmReg(3), build.inst(IrCmd::NUM_TO_UINT, build.constDouble(3740139520.0))); + build.inst(IrCmd::STORE_INT, build.vmReg(4), build.inst(IrCmd::NUM_TO_UINT, build.constDouble(-10))); + build.inst(IrCmd::STORE_INT, build.vmReg(5), build.inst(IrCmd::NUM_TO_UINT, build.constDouble(-12345678901234.0))); build.inst(IrCmd::RETURN, build.constUint(0)); @@ -423,6 +443,8 @@ TEST_CASE_FIXTURE(IrBuilderFixture, "NumericConversions") STORE_DOUBLE R1, 3740139520 STORE_INT R2, 200i STORE_INT R3, -554827776i + STORE_INT R4, -10i + STORE_INT R5, -1942892530i RETURN 0u )"); @@ -436,9 +458,8 @@ TEST_CASE_FIXTURE(IrBuilderFixture, "NumericConversionsBlocked") IrOp nan = build.inst(IrCmd::DIV_NUM, build.constDouble(0.0), build.constDouble(0.0)); build.inst(IrCmd::STORE_INT, build.vmReg(0), build.inst(IrCmd::NUM_TO_INT, build.constDouble(1e20))); - build.inst(IrCmd::STORE_INT, build.vmReg(1), build.inst(IrCmd::NUM_TO_UINT, build.constDouble(-10))); - build.inst(IrCmd::STORE_INT, build.vmReg(2), build.inst(IrCmd::NUM_TO_INT, nan)); - build.inst(IrCmd::STORE_INT, build.vmReg(3), build.inst(IrCmd::NUM_TO_UINT, nan)); + build.inst(IrCmd::STORE_INT, build.vmReg(1), build.inst(IrCmd::NUM_TO_INT, nan)); + build.inst(IrCmd::STORE_INT, build.vmReg(2), build.inst(IrCmd::NUM_TO_UINT, nan)); build.inst(IrCmd::RETURN, build.constUint(0)); @@ -449,12 +470,10 @@ TEST_CASE_FIXTURE(IrBuilderFixture, "NumericConversionsBlocked") bb_0: %1 = NUM_TO_INT 1e+20 STORE_INT R0, %1 - %3 = NUM_TO_UINT -10 + %3 = NUM_TO_INT nan STORE_INT R1, %3 - %5 = NUM_TO_INT nan + %5 = NUM_TO_UINT nan STORE_INT R2, %5 - %7 = NUM_TO_UINT nan - STORE_INT R3, %7 RETURN 0u )"); @@ -845,6 +864,92 @@ TEST_CASE_FIXTURE(IrBuilderFixture, "ControlFlowCmpNum") compareFold(build.constDouble(1), nan, IrCondition::NotGreaterEqual, true); } +TEST_CASE_FIXTURE(IrBuilderFixture, "SelectNumber") +{ + ScopedFastFlag luauCodegenLinearAndOr{FFlag::LuauCodegenLinearAndOr, true}; + + IrOp block = build.block(IrBlockKind::Internal); + + build.beginBlock(block); + + IrOp zeroNum = build.constDouble(0.0); + IrOp oneNum = build.constDouble(1.0); + IrOp unknownNum = build.inst(IrCmd::LOAD_DOUBLE, build.vmReg(0)); + + build.inst(IrCmd::STORE_DOUBLE, build.vmReg(0), build.inst(IrCmd::SELECT_NUM, build.constDouble(4), build.constDouble(8), zeroNum, zeroNum)); + build.inst(IrCmd::STORE_DOUBLE, build.vmReg(0), build.inst(IrCmd::SELECT_NUM, build.constDouble(4), build.constDouble(8), zeroNum, oneNum)); + build.inst(IrCmd::STORE_DOUBLE, build.vmReg(0), build.inst(IrCmd::SELECT_NUM, build.constDouble(4), build.constDouble(4), zeroNum, unknownNum)); + + build.inst(IrCmd::RETURN, build.constUint(0)); + + updateUseCounts(build.function); + constantFold(); + + CHECK("\n" + toString(build.function, IncludeUseInfo::No) == R"( +bb_0: + STORE_DOUBLE R0, 8 + STORE_DOUBLE R0, 4 + STORE_DOUBLE R0, 4 + RETURN 0u + +)"); +} + +TEST_CASE_FIXTURE(IrBuilderFixture, "SelectVector") +{ + ScopedFastFlag luauCodegenLinearAndOr{FFlag::LuauCodegenLinearAndOr, true}; + + IrOp block = build.block(IrBlockKind::Internal); + + build.beginBlock(block); + + IrOp unknownVec1 = build.inst(IrCmd::LOAD_TVALUE, build.vmReg(1)); + IrOp unknownVec2 = build.inst(IrCmd::LOAD_TVALUE, build.vmReg(2)); + IrOp unknownVec3 = build.inst(IrCmd::LOAD_TVALUE, build.vmReg(3)); + + build.inst(IrCmd::STORE_TVALUE, build.vmReg(0), build.inst(IrCmd::SELECT_VEC, unknownVec3, unknownVec3, unknownVec1, unknownVec2)); + + build.inst(IrCmd::RETURN, build.constUint(0)); + + updateUseCounts(build.function); + constantFold(); + + CHECK("\n" + toString(build.function, IncludeUseInfo::No) == R"( +bb_0: + %2 = LOAD_TVALUE R3 + STORE_TVALUE R0, %2 + RETURN 0u + +)"); +} + +TEST_CASE_FIXTURE(IrBuilderFixture, "SelectIfTruthy") +{ + ScopedFastFlag luauCodegenLinearAndOr{FFlag::LuauCodegenLinearAndOr, true}; + + IrOp block = build.block(IrBlockKind::Internal); + + build.beginBlock(block); + + IrOp unknownTv1 = build.inst(IrCmd::LOAD_TVALUE, build.vmReg(1)); + IrOp unknownTv2 = build.inst(IrCmd::LOAD_TVALUE, build.vmReg(2)); + + build.inst(IrCmd::STORE_TVALUE, build.vmReg(0), build.inst(IrCmd::SELECT_IF_TRUTHY, unknownTv1, unknownTv2, unknownTv2)); + + build.inst(IrCmd::RETURN, build.constUint(0)); + + updateUseCounts(build.function); + constantFold(); + + CHECK("\n" + toString(build.function, IncludeUseInfo::No) == R"( +bb_0: + %1 = LOAD_TVALUE R2 + STORE_TVALUE R0, %1 + RETURN 0u + +)"); +} + TEST_SUITE_END(); TEST_SUITE_BEGIN("ConstantPropagation"); @@ -1574,6 +1679,8 @@ TEST_CASE_FIXTURE(IrBuilderFixture, "EntryBlockUseRemoval") TEST_CASE_FIXTURE(IrBuilderFixture, "RecursiveSccUseRemoval1") { + ScopedFastFlag luauCodegenBetterSccRemoval{FFlag::LuauCodegenBetterSccRemoval, true}; + IrOp entry = build.block(IrBlockKind::Internal); IrOp block = build.block(IrBlockKind::Internal); IrOp exit = build.block(IrBlockKind::Internal); @@ -1600,19 +1707,13 @@ TEST_CASE_FIXTURE(IrBuilderFixture, "RecursiveSccUseRemoval1") bb_0: RETURN R0, 0i -bb_1: - STORE_TAG R0, tnumber - JUMP bb_2 -; glued to: bb_2 - -bb_2: - RETURN R0, 0i - )"); } TEST_CASE_FIXTURE(IrBuilderFixture, "RecursiveSccUseRemoval2") { + ScopedFastFlag luauCodegenBetterSccRemoval{FFlag::LuauCodegenBetterSccRemoval, true}; + IrOp entry = build.block(IrBlockKind::Internal); IrOp exit1 = build.block(IrBlockKind::Internal); IrOp block = build.block(IrBlockKind::Internal); @@ -1647,19 +1748,13 @@ TEST_CASE_FIXTURE(IrBuilderFixture, "RecursiveSccUseRemoval2") bb_1: RETURN R0, 0i -bb_2: - STORE_TAG R0, tnumber - JUMP bb_3 -; glued to: bb_3 - -bb_3: - RETURN R0, 0i - )"); } TEST_CASE_FIXTURE(IrBuilderFixture, "IntNumIntPeepholes") { + ScopedFastFlag luauCodegenNumIntFolds{FFlag::LuauCodegenNumIntFolds2, true}; + IrOp block = build.block(IrBlockKind::Internal); build.beginBlock(block); @@ -1668,11 +1763,11 @@ TEST_CASE_FIXTURE(IrBuilderFixture, "IntNumIntPeepholes") IrOp u1 = build.inst(IrCmd::LOAD_INT, build.vmReg(1)); IrOp ni1 = build.inst(IrCmd::INT_TO_NUM, i1); IrOp nu1 = build.inst(IrCmd::UINT_TO_NUM, u1); - IrOp i2 = build.inst(IrCmd::NUM_TO_INT, ni1); - IrOp u2 = build.inst(IrCmd::NUM_TO_UINT, nu1); - build.inst(IrCmd::STORE_INT, build.vmReg(0), i2); - build.inst(IrCmd::STORE_INT, build.vmReg(1), u2); - build.inst(IrCmd::RETURN, build.constUint(2)); + build.inst(IrCmd::STORE_INT, build.vmReg(0), build.inst(IrCmd::NUM_TO_INT, ni1)); + build.inst(IrCmd::STORE_INT, build.vmReg(1), build.inst(IrCmd::NUM_TO_UINT, nu1)); + build.inst(IrCmd::STORE_INT, build.vmReg(2), build.inst(IrCmd::NUM_TO_UINT, ni1)); + build.inst(IrCmd::STORE_INT, build.vmReg(3), build.inst(IrCmd::NUM_TO_INT, nu1)); + build.inst(IrCmd::RETURN, build.vmReg(0), build.constUint(4)); updateUseCounts(build.function); constPropInBlockChains(build); @@ -1683,13 +1778,75 @@ TEST_CASE_FIXTURE(IrBuilderFixture, "IntNumIntPeepholes") %1 = LOAD_INT R1 STORE_INT R0, %0 STORE_INT R1, %1 - RETURN 2u + STORE_INT R2, %0 + STORE_INT R3, %1 + RETURN R0, 4u + +)"); +} + +TEST_CASE_FIXTURE(IrBuilderFixture, "IntNumIntPeepholes2") +{ + ScopedFastFlag luauCodegenNumIntFolds{FFlag::LuauCodegenNumIntFolds2, true}; + + IrOp block = build.block(IrBlockKind::Internal); + + build.beginBlock(block); + + IrOp d1 = build.inst(IrCmd::LOAD_DOUBLE, build.vmReg(0)); + IrOp u = build.inst(IrCmd::NUM_TO_UINT, d1); + IrOp d2 = build.inst(IrCmd::UINT_TO_NUM, u); + build.inst(IrCmd::STORE_INT, build.vmReg(0), build.inst(IrCmd::NUM_TO_UINT, d2)); + build.inst(IrCmd::RETURN, build.vmReg(0), build.constUint(1)); + + updateUseCounts(build.function); + constPropInBlockChains(build); + + CHECK("\n" + toString(build.function, IncludeUseInfo::No) == R"( +bb_0: + %0 = LOAD_DOUBLE R0 + %1 = NUM_TO_UINT %0 + STORE_INT R0, %1 + RETURN R0, 1u + +)"); +} + +TEST_CASE_FIXTURE(IrBuilderFixture, "IntNumIntPeepholes3") +{ + ScopedFastFlag luauCodegenNumIntFolds{FFlag::LuauCodegenNumIntFolds2, true}; + + IrOp block = build.block(IrBlockKind::Internal); + + build.beginBlock(block); + + IrOp table = build.inst(IrCmd::LOAD_POINTER, build.vmReg(0)); + IrOp len = build.inst(IrCmd::TABLE_LEN, table); + IrOp d = build.inst(IrCmd::INT_TO_NUM, len); + IrOp u = build.inst(IrCmd::NUM_TO_UINT, d); + IrOp u2 = build.inst(IrCmd::TRUNCATE_UINT, u); + IrOp result = build.inst(IrCmd::ADD_INT, u2, build.constInt(1)); + build.inst(IrCmd::STORE_INT, build.vmReg(0), result); + build.inst(IrCmd::RETURN, build.vmReg(0), build.constUint(1)); + + updateUseCounts(build.function); + constPropInBlockChains(build); + + CHECK("\n" + toString(build.function, IncludeUseInfo::No) == R"( +bb_0: + %0 = LOAD_POINTER R0 + %1 = TABLE_LEN %0 + %5 = ADD_INT %1, 1i + STORE_INT R0, %5 + RETURN R0, 1u )"); } TEST_CASE_FIXTURE(IrBuilderFixture, "InvalidateReglinkVersion") { + ScopedFastFlag luauCodegenHydrateLoadWithTag{FFlag::LuauCodegenHydrateLoadWithTag, true}; + IrOp block = build.block(IrBlockKind::Internal); IrOp fallback = build.block(IrBlockKind::Fallback); @@ -1716,15 +1873,13 @@ TEST_CASE_FIXTURE(IrBuilderFixture, "InvalidateReglinkVersion") CHECK("\n" + toString(build.function, IncludeUseInfo::No) == R"( bb_0: STORE_TAG R2, tstring - %1 = LOAD_TVALUE R2 + %1 = LOAD_TVALUE R2, 0i, tstring STORE_TVALUE R1, %1 %3 = NEW_TABLE 0u, 0u STORE_POINTER R2, %3 STORE_TAG R2, ttable STORE_TVALUE R0, %1 - %8 = LOAD_TAG R0 - CHECK_TAG %8, ttable, bb_fallback_1 - RETURN 0u + JUMP bb_fallback_1 bb_fallback_1: RETURN 1u @@ -2220,6 +2375,8 @@ TEST_CASE_FIXTURE(IrBuilderFixture, "DuplicateHashSlotChecks") TEST_CASE_FIXTURE(IrBuilderFixture, "DuplicateHashSlotChecksAvoidNil") { + ScopedFastFlag luauCodegenHydrateLoadWithTag{FFlag::LuauCodegenHydrateLoadWithTag, true}; + IrOp block = build.block(IrBlockKind::Internal); IrOp fallback = build.block(IrBlockKind::Fallback); @@ -2272,7 +2429,7 @@ TEST_CASE_FIXTURE(IrBuilderFixture, "DuplicateHashSlotChecksAvoidNil") CHECK_SLOT_MATCH %6, K1, bb_fallback_1 CHECK_READONLY %5, bb_fallback_1 STORE_TAG R4, tnil - %10 = LOAD_TVALUE R4 + %10 = LOAD_TVALUE R4, 0i, tnil STORE_TVALUE %6, %10, 0i CHECK_NODE_VALUE %1, bb_fallback_1 %14 = LOAD_TVALUE %1, 0i @@ -2874,6 +3031,8 @@ TEST_CASE_FIXTURE(IrBuilderFixture, "InferNumberTagFromLimitedContext") TEST_CASE_FIXTURE(IrBuilderFixture, "DoNotProduceInvalidSplitStore1") { + ScopedFastFlag luauCodegenHydrateLoadWithTag{FFlag::LuauCodegenHydrateLoadWithTag, true}; + IrOp entry = build.block(IrBlockKind::Internal); build.beginBlock(entry); @@ -2891,7 +3050,7 @@ TEST_CASE_FIXTURE(IrBuilderFixture, "DoNotProduceInvalidSplitStore1") STORE_INT R0, 1i %1 = LOAD_TAG R0 CHECK_TAG %1, ttable, exit(1) - %3 = LOAD_TVALUE R0 + %3 = LOAD_TVALUE R0, 0i, ttable STORE_TVALUE R1, %3 RETURN R1, 1i @@ -2900,6 +3059,8 @@ TEST_CASE_FIXTURE(IrBuilderFixture, "DoNotProduceInvalidSplitStore1") TEST_CASE_FIXTURE(IrBuilderFixture, "DoNotProduceInvalidSplitStore2") { + ScopedFastFlag luauCodegenHydrateLoadWithTag{FFlag::LuauCodegenHydrateLoadWithTag, true}; + IrOp entry = build.block(IrBlockKind::Internal); build.beginBlock(entry); @@ -2917,7 +3078,7 @@ TEST_CASE_FIXTURE(IrBuilderFixture, "DoNotProduceInvalidSplitStore2") STORE_INT R0, 1i %1 = LOAD_TAG R0 CHECK_TAG %1, tnumber, exit(1) - %3 = LOAD_TVALUE R0 + %3 = LOAD_TVALUE R0, 0i, tnumber STORE_TVALUE R1, %3 RETURN R1, 1i @@ -2927,6 +3088,7 @@ TEST_CASE_FIXTURE(IrBuilderFixture, "DoNotProduceInvalidSplitStore2") TEST_CASE_FIXTURE(IrBuilderFixture, "DoNotProduceInvalidSplitStore3") { ScopedFastFlag luauCodegenStorePriority{FFlag::LuauCodegenStorePriority, true}; + ScopedFastFlag luauCodegenHydrateLoadWithTag{FFlag::LuauCodegenHydrateLoadWithTag, true}; IrOp entry = build.block(IrBlockKind::Internal); @@ -2959,7 +3121,7 @@ TEST_CASE_FIXTURE(IrBuilderFixture, "DoNotProduceInvalidSplitStore3") STORE_TAG R2, tnumber %4 = LOAD_DOUBLE R0 STORE_DOUBLE R2, %4 - %6 = LOAD_TVALUE R0 + %6 = LOAD_TVALUE R0, 0i, tnumber STORE_TVALUE R1, %6 STORE_TAG R1, tboolean RETURN R1, 1i @@ -3955,6 +4117,9 @@ TEST_CASE_FIXTURE(IrBuilderFixture, "UnusedAtReturnPartial") TEST_CASE_FIXTURE(IrBuilderFixture, "HiddenPointerUse1") { + ScopedFastFlag luauCodegenBufferLoadProp{FFlag::LuauCodegenBufferLoadProp2, true}; + ScopedFastFlag luauCodegenGcoDse{FFlag::LuauCodegenGcoDse, true}; + IrOp entry = build.block(IrBlockKind::Internal); build.beginBlock(entry); @@ -3972,9 +4137,6 @@ TEST_CASE_FIXTURE(IrBuilderFixture, "HiddenPointerUse1") CHECK("\n" + toString(build.function, IncludeUseInfo::No) == R"( bb_0: ; in regs: R0, R2 - %0 = LOAD_POINTER R0 - STORE_POINTER R1, %0 - STORE_TAG R1, ttable CALL R2, 0i, 1i RETURN R2, 1i @@ -3983,6 +4145,9 @@ TEST_CASE_FIXTURE(IrBuilderFixture, "HiddenPointerUse1") TEST_CASE_FIXTURE(IrBuilderFixture, "HiddenPointerUse2") { + ScopedFastFlag luauCodegenBufferLoadProp{FFlag::LuauCodegenBufferLoadProp2, true}; + ScopedFastFlag luauCodegenGcoDse{FFlag::LuauCodegenGcoDse, true}; + IrOp entry = build.block(IrBlockKind::Internal); build.beginBlock(entry); @@ -4000,13 +4165,11 @@ TEST_CASE_FIXTURE(IrBuilderFixture, "HiddenPointerUse2") constPropInBlockChains(build); markDeadStoresInBlockChains(build); - // Stores to pointers can be safely removed at 'return' point, but have to preserved for any GC assist trigger (such as a call) + // Stores to pointers can be safely removed as long as they are always removed together with the tag store + // This means that a GC assist during the call will not see an incomplete TValue on the stack CHECK("\n" + toString(build.function, IncludeUseInfo::No) == R"( bb_0: ; in regs: R0, R2 - %0 = LOAD_POINTER R0 - STORE_POINTER R1, %0 - STORE_TAG R1, ttable CALL R2, 0i, 1i RETURN R2, 1i @@ -4073,6 +4236,83 @@ TEST_CASE_FIXTURE(IrBuilderFixture, "HiddenPointerUse4") )"); } +TEST_CASE_FIXTURE(IrBuilderFixture, "HiddenPointerUse5") +{ + IrOp entry = build.block(IrBlockKind::Internal); + + build.beginBlock(entry); + IrOp somePtrA = build.inst(IrCmd::NEW_TABLE, build.constUint(16), build.constUint(0)); + build.inst(IrCmd::STORE_POINTER, build.vmReg(1), somePtrA); + build.inst(IrCmd::STORE_TAG, build.vmReg(1), build.constTag(ttable)); + build.inst(IrCmd::DO_LEN, build.vmReg(3), build.vmReg(2)); + IrOp somePtrB = build.inst(IrCmd::LOAD_POINTER, build.vmReg(1)); + build.inst(IrCmd::STORE_POINTER, build.vmReg(2), somePtrB); + build.inst(IrCmd::STORE_TAG, build.vmReg(2), build.constTag(ttable)); + build.inst(IrCmd::RETURN, build.vmReg(2), build.constInt(2)); + + updateUseCounts(build.function); + computeCfgInfo(build.function); + constPropInBlockChains(build); + markDeadStoresInBlockChains(build); + + // %0 is used across the DO_LEN which can call __len and require a GC assist + // This requires %0 store to R1 to remain in order for GC to see it on the stack + CHECK("\n" + toString(build.function, IncludeUseInfo::No) == R"( +bb_0: +; in regs: R2 + %0 = NEW_TABLE 16u, 0u + STORE_POINTER R1, %0 + STORE_TAG R1, ttable + DO_LEN R3, R2 + STORE_POINTER R2, %0 + STORE_TAG R2, ttable + RETURN R2, 2i + +)"); +} + +TEST_CASE_FIXTURE(IrBuilderFixture, "HiddenPointerUse6") +{ + IrOp entry = build.block(IrBlockKind::Internal); + + build.beginBlock(entry); + IrOp table = build.inst(IrCmd::DUP_TABLE, build.inst(IrCmd::LOAD_POINTER, build.vmReg(0))); + build.inst(IrCmd::STORE_POINTER, build.vmReg(1), table); + build.inst(IrCmd::STORE_TAG, build.vmReg(1), build.constTag(ttable)); + build.inst(IrCmd::CHECK_GC); + IrOp tableLen = build.inst(IrCmd::TABLE_LEN, table); + build.inst(IrCmd::STORE_DOUBLE, build.vmReg(2), build.inst(IrCmd::INT_TO_NUM, tableLen)); + build.inst(IrCmd::STORE_TAG, build.vmReg(2), build.constTag(tnumber)); + build.inst(IrCmd::STORE_DOUBLE, build.vmReg(1), build.constDouble(1.0)); + build.inst(IrCmd::STORE_TAG, build.vmReg(1), build.constTag(tnumber)); + build.inst(IrCmd::RETURN, build.vmReg(1), build.constInt(2)); + + updateUseCounts(build.function); + computeCfgInfo(build.function); + constPropInBlockChains(build); + markDeadStoresInBlockChains(build); + + // %1 is used across the CHECK_GC + // This requires not only %1 store to R1 to remain, but the table tag store as well + CHECK("\n" + toString(build.function, IncludeUseInfo::No) == R"( +bb_0: +; in regs: R0 + %0 = LOAD_POINTER R0 + %1 = DUP_TABLE %0 + STORE_POINTER R1, %1 + STORE_TAG R1, ttable + CHECK_GC + %5 = TABLE_LEN %1 + %6 = INT_TO_NUM %5 + STORE_DOUBLE R2, %6 + STORE_TAG R2, tnumber + STORE_DOUBLE R1, 1 + STORE_TAG R1, tnumber + RETURN R1, 2i + +)"); +} + TEST_CASE_FIXTURE(IrBuilderFixture, "PartialVsFullStoresWithRecombination") { IrOp entry = build.block(IrBlockKind::Internal); @@ -4545,7 +4785,7 @@ TEST_CASE_FIXTURE(IrBuilderFixture, "DoNotReturnWithPartialStores") build.beginBlock(entry); build.inst(IrCmd::STORE_POINTER, build.vmReg(1), build.inst(IrCmd::NEW_TABLE, build.constUint(0), build.constUint(0))); build.inst(IrCmd::STORE_TAG, build.vmReg(1), build.constTag(ttable)); - IrOp toUint = build.inst(IrCmd::NUM_TO_UINT, build.constDouble(-1)); + IrOp toUint = build.inst(IrCmd::NUM_TO_UINT, build.constDouble(1e20)); IrOp bitAnd = build.inst(IrCmd::BITAND_UINT, toUint, build.constInt(4)); build.inst(IrCmd::JUMP_CMP_INT, bitAnd, build.constInt(0), build.cond(IrCondition::Equal), success, fail); @@ -4576,7 +4816,7 @@ TEST_CASE_FIXTURE(IrBuilderFixture, "DoNotReturnWithPartialStores") %0 = NEW_TABLE 0u, 0u STORE_POINTER R1, %0 STORE_TAG R1, ttable - %3 = NUM_TO_UINT -1 + %3 = NUM_TO_UINT 1e+20 %4 = BITAND_UINT %3, 4i JUMP_CMP_INT %4, 0i, eq, bb_1, bb_2 diff --git a/tests/IrLowering.test.cpp b/tests/IrLowering.test.cpp index b46d3891d..2873d5256 100644 --- a/tests/IrLowering.test.cpp +++ b/tests/IrLowering.test.cpp @@ -16,15 +16,23 @@ #include #include -LUAU_FASTFLAG(LuauVectorLerp) -LUAU_FASTFLAG(LuauCompileVectorLerp) -LUAU_FASTFLAG(LuauTypeCheckerVectorLerp2) -LUAU_FASTFLAG(LuauCodeGenVectorLerp2) LUAU_FASTFLAG(LuauCompileUnusedUdataFix) +LUAU_FASTFLAG(LuauCodegenLoopStepDetectFix) LUAU_FASTFLAG(LuauCodegenFloatLoadStoreProp) +LUAU_FASTFLAG(LuauCodegenLoadFloatSubstituteLast) LUAU_FASTFLAG(LuauCodegenBlockSafeEnv) LUAU_FASTFLAG(LuauCodegenChainLink) LUAU_FASTFLAG(LuauCodegenIntegerAddSub) +LUAU_FASTFLAG(LuauCodegenSetBlockEntryState) +LUAU_FASTFLAG(LuauCodegenBetterSccRemoval) +LUAU_FASTFLAG(LuauCodegenLinearAndOr) +LUAU_FASTFLAG(LuauCodegenHydrateLoadWithTag) +LUAU_FASTFLAG(LuauCodegenUpvalueLoadProp) +LUAU_FASTFLAG(LuauCodegenGcoDse) +LUAU_FASTFLAG(LuauCodegenBufferLoadProp2) +LUAU_FASTFLAG(LuauCodegenNumIntFolds2) +LUAU_FASTFLAG(LuauCodegenNumToUintFoldRange) +LUAU_FASTFLAG(LuauCodegenSplitFloat) static void luauLibraryConstantLookup(const char* library, const char* member, Luau::CompileConstant* constant) { @@ -187,7 +195,16 @@ static std::string getCodegenAssembly(const char* source, bool includeIrTypes = // Runtime mapping is specifically created to NOT match the compilation mapping options.compilationOptions.userdataTypes = kUserdataRunTypes; - return Luau::CodeGen::getAssembly(L, -1, options, nullptr); + std::string result = Luau::CodeGen::getAssembly(L, -1, options, nullptr); + + if (Luau::CodeGen::isSupported()) + { + // Checking that other target lower correctly as well + options.target = Luau::CodeGen::AssemblyOptions::Target::A64; + Luau::CodeGen::getAssembly(L, -1, options, nullptr); + } + + return result; } FAIL("Failed to load bytecode"); @@ -254,6 +271,8 @@ TEST_SUITE_BEGIN("IrLowering"); TEST_CASE("VectorReciprocal") { + ScopedFastFlag luauCodegenHydrateLoadWithTag{FFlag::LuauCodegenHydrateLoadWithTag, true}; + CHECK_EQ( "\n" + getCodegenAssembly(R"( local function vecrcp(a: vector) @@ -269,7 +288,7 @@ end JUMP bb_bytecode_1 bb_bytecode_1: %6 = NUM_TO_VEC 1 - %7 = LOAD_TVALUE R0 + %7 = LOAD_TVALUE R0, 0i, tvector %8 = DIV_VEC %6, %7 %9 = TAG_VECTOR %8 STORE_TVALUE R1, %9 @@ -281,6 +300,8 @@ end TEST_CASE("VectorComponentRead") { + ScopedFastFlag luauCodegenSplitFloat{FFlag::LuauCodegenSplitFloat, true}; + CHECK_EQ( "\n" + getCodegenAssembly(R"( local function compsum(a: vector) @@ -296,11 +317,14 @@ end JUMP bb_bytecode_1 bb_bytecode_1: %6 = LOAD_FLOAT R0, 0i - %11 = LOAD_FLOAT R0, 4i - %20 = ADD_NUM %6, %11 - %25 = LOAD_FLOAT R0, 8i - %34 = ADD_NUM %20, %25 - STORE_DOUBLE R1, %34 + %7 = FLOAT_TO_NUM %6 + %12 = LOAD_FLOAT R0, 4i + %13 = FLOAT_TO_NUM %12 + %22 = ADD_NUM %7, %13 + %27 = LOAD_FLOAT R0, 8i + %28 = FLOAT_TO_NUM %27 + %37 = ADD_NUM %22, %28 + STORE_DOUBLE R1, %37 STORE_TAG R1, tnumber INTERRUPT 8u RETURN R1, 1i @@ -310,6 +334,8 @@ end TEST_CASE("VectorAdd") { + ScopedFastFlag luauCodegenHydrateLoadWithTag{FFlag::LuauCodegenHydrateLoadWithTag, true}; + CHECK_EQ( "\n" + getCodegenAssembly(R"( local function vec3add(a: vector, b: vector) @@ -325,8 +351,8 @@ end bb_2: JUMP bb_bytecode_1 bb_bytecode_1: - %10 = LOAD_TVALUE R0 - %11 = LOAD_TVALUE R1 + %10 = LOAD_TVALUE R0, 0i, tvector + %11 = LOAD_TVALUE R1, 0i, tvector %12 = ADD_VEC %10, %11 %13 = TAG_VECTOR %12 STORE_TVALUE R2, %13 @@ -338,6 +364,8 @@ end TEST_CASE("VectorMinus") { + ScopedFastFlag luauCodegenHydrateLoadWithTag{FFlag::LuauCodegenHydrateLoadWithTag, true}; + CHECK_EQ( "\n" + getCodegenAssembly(R"( local function vec3minus(a: vector) @@ -352,7 +380,7 @@ end bb_2: JUMP bb_bytecode_1 bb_bytecode_1: - %6 = LOAD_TVALUE R0 + %6 = LOAD_TVALUE R0, 0i, tvector %7 = UNM_VEC %6 %8 = TAG_VECTOR %7 STORE_TVALUE R1, %8 @@ -364,6 +392,8 @@ end TEST_CASE("VectorSubMulDiv") { + ScopedFastFlag luauCodegenHydrateLoadWithTag{FFlag::LuauCodegenHydrateLoadWithTag, true}; + CHECK_EQ( "\n" + getCodegenAssembly(R"( local function vec3combo(a: vector, b: vector, c: vector, d: vector) @@ -381,11 +411,11 @@ end bb_2: JUMP bb_bytecode_1 bb_bytecode_1: - %14 = LOAD_TVALUE R0 - %15 = LOAD_TVALUE R1 + %14 = LOAD_TVALUE R0, 0i, tvector + %15 = LOAD_TVALUE R1, 0i, tvector %16 = MUL_VEC %14, %15 - %23 = LOAD_TVALUE R2 - %24 = LOAD_TVALUE R3 + %23 = LOAD_TVALUE R2, 0i, tvector + %24 = LOAD_TVALUE R3, 0i, tvector %25 = DIV_VEC %23, %24 %34 = SUB_VEC %16, %25 %35 = TAG_VECTOR %34 @@ -398,6 +428,8 @@ end TEST_CASE("VectorSubMulDiv2") { + ScopedFastFlag luauCodegenHydrateLoadWithTag{FFlag::LuauCodegenHydrateLoadWithTag, true}; + CHECK_EQ( "\n" + getCodegenAssembly(R"( local function vec3combo(a: vector) @@ -413,7 +445,7 @@ end bb_2: JUMP bb_bytecode_1 bb_bytecode_1: - %8 = LOAD_TVALUE R0 + %8 = LOAD_TVALUE R0, 0i, tvector %10 = MUL_VEC %8, %8 %19 = SUB_VEC %10, %10 %28 = ADD_VEC %10, %10 @@ -428,6 +460,8 @@ end TEST_CASE("VectorMulDivMixed") { + ScopedFastFlag luauCodegenHydrateLoadWithTag{FFlag::LuauCodegenHydrateLoadWithTag, true}; + CHECK_EQ( "\n" + getCodegenAssembly(R"( local function vec3combo(a: vector, b: vector, c: vector, d: vector) @@ -445,19 +479,19 @@ end bb_2: JUMP bb_bytecode_1 bb_bytecode_1: - %12 = LOAD_TVALUE R0 + %12 = LOAD_TVALUE R0, 0i, tvector %13 = NUM_TO_VEC 2 %14 = MUL_VEC %12, %13 - %19 = LOAD_TVALUE R1 + %19 = LOAD_TVALUE R1, 0i, tvector %20 = NUM_TO_VEC 4 %21 = DIV_VEC %19, %20 %30 = ADD_VEC %14, %21 %40 = NUM_TO_VEC 0.5 - %41 = LOAD_TVALUE R2 + %41 = LOAD_TVALUE R2, 0i, tvector %42 = MUL_VEC %40, %41 %51 = ADD_VEC %30, %42 %56 = NUM_TO_VEC 40 - %57 = LOAD_TVALUE R3 + %57 = LOAD_TVALUE R3, 0i, tvector %58 = DIV_VEC %56, %57 %67 = ADD_VEC %51, %58 %68 = TAG_VECTOR %67 @@ -471,11 +505,8 @@ end TEST_CASE("VectorLerp") { ScopedFastFlag _[]{ - {FFlag::LuauCompileVectorLerp, true}, - {FFlag::LuauTypeCheckerVectorLerp2, true}, - {FFlag::LuauVectorLerp, true}, - {FFlag::LuauCodeGenVectorLerp2, true}, - {FFlag::LuauCodegenBlockSafeEnv, true} + {FFlag::LuauCodegenBlockSafeEnv, true}, + {FFlag::LuauCodegenHydrateLoadWithTag, true} }; CHECK_EQ( "\n" + getCodegenAssembly(R"( @@ -494,8 +525,8 @@ end JUMP bb_bytecode_1 bb_bytecode_1: implicit CHECK_SAFE_ENV exit(0) - %15 = LOAD_TVALUE R0 - %16 = LOAD_TVALUE R1 + %15 = LOAD_TVALUE R0, 0i, tvector + %16 = LOAD_TVALUE R1, 0i, tvector %17 = LOAD_DOUBLE R2 %18 = NUM_TO_VEC %17 %19 = NUM_TO_VEC 1 @@ -781,6 +812,7 @@ end TEST_CASE("TypeCompare") { ScopedFastFlag luauCodegenBlockSafeEnv{FFlag::LuauCodegenBlockSafeEnv, true}; + ScopedFastFlag luauCodegenGcoDse{FFlag::LuauCodegenGcoDse, true}; CHECK_EQ( "\n" + getCodegenAssembly( @@ -795,9 +827,6 @@ end bb_bytecode_0: implicit CHECK_SAFE_ENV exit(0) %1 = LOAD_TAG R0 - %2 = GET_TYPE %1 - STORE_POINTER R2, %2 - STORE_TAG R2, tstring %8 = CMP_TAG %1, tnumber, eq STORE_TAG R1, tboolean STORE_INT R1, %8 @@ -812,6 +841,7 @@ end TEST_CASE("TypeofCompare") { ScopedFastFlag luauCodegenBlockSafeEnv{FFlag::LuauCodegenBlockSafeEnv, true}; + ScopedFastFlag luauCodegenGcoDse{FFlag::LuauCodegenGcoDse, true}; CHECK_EQ( "\n" + getCodegenAssembly( @@ -825,9 +855,6 @@ end ; function foo($arg0) line 2 bb_bytecode_0: implicit CHECK_SAFE_ENV exit(0) - %1 = GET_TYPEOF R0 - STORE_POINTER R2, %1 - STORE_TAG R2, tstring %7 = CMP_TAG R0, tnumber, eq STORE_TAG R1, tboolean STORE_INT R1, %7 @@ -842,6 +869,7 @@ end TEST_CASE("TypeofCompareCustom") { ScopedFastFlag luauCodegenBlockSafeEnv{FFlag::LuauCodegenBlockSafeEnv, true}; + ScopedFastFlag luauCodegenGcoDse{FFlag::LuauCodegenGcoDse, true}; CHECK_EQ( "\n" + getCodegenAssembly( @@ -856,8 +884,6 @@ end bb_bytecode_0: implicit CHECK_SAFE_ENV exit(0) %1 = GET_TYPEOF R0 - STORE_POINTER R2, %1 - STORE_TAG R2, tstring %6 = LOAD_POINTER K2 ('User') %7 = CMP_SPLIT_TVALUE tstring, tstring, %1, %6, eq STORE_TAG R1, tboolean @@ -1019,6 +1045,8 @@ end TEST_CASE("VectorConstantTag") { + ScopedFastFlag luauCodegenHydrateLoadWithTag{FFlag::LuauCodegenHydrateLoadWithTag, true}; + CHECK_EQ( "\n" + getCodegenAssembly(R"( local function vecrcp(a: vector) @@ -1034,7 +1062,7 @@ end JUMP bb_bytecode_1 bb_bytecode_1: %4 = LOAD_TVALUE K0 (1, 2, 3), 0i, tvector - %11 = LOAD_TVALUE R0 + %11 = LOAD_TVALUE R0, 0i, tvector %12 = ADD_VEC %4, %11 %13 = TAG_VECTOR %12 STORE_TVALUE R1, %13 @@ -1072,6 +1100,8 @@ end TEST_CASE("VectorRandomProp") { + ScopedFastFlag luauCodegenSetBlockEntryState{FFlag::LuauCodegenSetBlockEntryState, true}; + CHECK_EQ( "\n" + getCodegenAssembly(R"( local function foo(a: vector) @@ -1096,7 +1126,6 @@ end STORE_TAG R2, tnumber JUMP bb_4 bb_4: - CHECK_TAG R0, tvector, exit(5) FALLBACK_GETTABLEKS 5u, R3, R0, K2 ('ZZ') CHECK_TAG R2, tnumber, bb_fallback_5 CHECK_TAG R3, tnumber, bb_fallback_5 @@ -1114,6 +1143,8 @@ end TEST_CASE("VectorCustomAccess") { + ScopedFastFlag luauCodegenSplitFloat{FFlag::LuauCodegenSplitFloat, true}; + CHECK_EQ( "\n" + getCodegenAssembly(R"( local function vec3magn(a: vector) @@ -1131,14 +1162,17 @@ end %6 = LOAD_FLOAT R0, 0i %7 = LOAD_FLOAT R0, 4i %8 = LOAD_FLOAT R0, 8i - %9 = MUL_NUM %6, %6 - %10 = MUL_NUM %7, %7 - %11 = MUL_NUM %8, %8 - %12 = ADD_NUM %9, %10 - %13 = ADD_NUM %12, %11 - %14 = SQRT_NUM %13 - %20 = MUL_NUM %14, 3 - STORE_DOUBLE R1, %20 + %9 = FLOAT_TO_NUM %6 + %10 = FLOAT_TO_NUM %7 + %11 = FLOAT_TO_NUM %8 + %12 = MUL_NUM %9, %9 + %13 = MUL_NUM %10, %10 + %14 = MUL_NUM %11, %11 + %15 = ADD_NUM %12, %13 + %16 = ADD_NUM %15, %14 + %17 = SQRT_NUM %16 + %23 = MUL_NUM %17, 3 + STORE_DOUBLE R1, %23 STORE_TAG R1, tnumber INTERRUPT 3u RETURN R1, 1i @@ -1148,6 +1182,11 @@ end TEST_CASE("VectorCustomNamecall") { + ScopedFastFlag luauCodegenHydrateLoadWithTag{FFlag::LuauCodegenHydrateLoadWithTag, true}; + ScopedFastFlag luauCodegenUpvalueLoadProp{FFlag::LuauCodegenUpvalueLoadProp, true}; + ScopedFastFlag luauCodegenFloatLoadStoreProp{FFlag::LuauCodegenFloatLoadStoreProp, true}; + ScopedFastFlag luauCodegenSplitFloat{FFlag::LuauCodegenSplitFloat, true}; + CHECK_EQ( "\n" + getCodegenAssembly(R"( local function vec3dot(a: vector, b: vector) @@ -1163,20 +1202,25 @@ end bb_2: JUMP bb_bytecode_1 bb_bytecode_1: - %6 = LOAD_TVALUE R1 - STORE_TVALUE R4, %6 + %6 = LOAD_TVALUE R1, 0i, tvector %12 = LOAD_FLOAT R0, 0i - %13 = LOAD_FLOAT R4, 0i - %14 = MUL_NUM %12, %13 - %15 = LOAD_FLOAT R0, 4i - %16 = LOAD_FLOAT R4, 4i - %17 = MUL_NUM %15, %16 - %18 = LOAD_FLOAT R0, 8i - %19 = LOAD_FLOAT R4, 8i - %20 = MUL_NUM %18, %19 - %21 = ADD_NUM %14, %17 - %22 = ADD_NUM %21, %20 - STORE_DOUBLE R2, %22 + %13 = EXTRACT_VEC %6, 0i + %14 = FLOAT_TO_NUM %12 + %15 = FLOAT_TO_NUM %13 + %16 = MUL_NUM %14, %15 + %17 = LOAD_FLOAT R0, 4i + %18 = EXTRACT_VEC %6, 1i + %19 = FLOAT_TO_NUM %17 + %20 = FLOAT_TO_NUM %18 + %21 = MUL_NUM %19, %20 + %22 = LOAD_FLOAT R0, 8i + %23 = EXTRACT_VEC %6, 2i + %24 = FLOAT_TO_NUM %22 + %25 = FLOAT_TO_NUM %23 + %26 = MUL_NUM %24, %25 + %27 = ADD_NUM %16, %21 + %28 = ADD_NUM %27, %26 + STORE_DOUBLE R2, %28 STORE_TAG R2, tnumber INTERRUPT 4u RETURN R2, 1i @@ -1184,8 +1228,49 @@ end ); } +TEST_CASE("VectorCustomNamecall2") +{ + ScopedFastFlag luauCodegenUpvalueLoadProp{FFlag::LuauCodegenUpvalueLoadProp, true}; + ScopedFastFlag luauCodegenFloatLoadStoreProp{FFlag::LuauCodegenFloatLoadStoreProp, true}; + ScopedFastFlag luauCodegenSplitFloat{FFlag::LuauCodegenSplitFloat, true}; + + CHECK_EQ( + "\n" + getCodegenAssembly(R"( +local function vec3dot(a: vector) + return (a:Dot(vector.create(1, 2, 3))) +end +)"), + R"( +; function vec3dot($arg0) line 2 +bb_0: + CHECK_TAG R0, tvector, exit(entry) + JUMP bb_2 +bb_2: + JUMP bb_bytecode_1 +bb_bytecode_1: + %10 = LOAD_FLOAT R0, 0i + %12 = FLOAT_TO_NUM %10 + %15 = LOAD_FLOAT R0, 4i + %17 = FLOAT_TO_NUM %15 + %19 = ADD_NUM %17, %17 + %20 = LOAD_FLOAT R0, 8i + %22 = FLOAT_TO_NUM %20 + %24 = MUL_NUM %22, 3 + %25 = ADD_NUM %12, %19 + %26 = ADD_NUM %25, %24 + STORE_DOUBLE R1, %26 + STORE_TAG R1, tnumber + INTERRUPT 4u + RETURN R1, 1i +)" + ); +} + TEST_CASE("VectorCustomAccessChain") { + ScopedFastFlag luauCodegenHydrateLoadWithTag{FFlag::LuauCodegenHydrateLoadWithTag, true}; + ScopedFastFlag luauCodegenSplitFloat{FFlag::LuauCodegenSplitFloat, true}; + CHECK_EQ( "\n" + getCodegenAssembly(R"( local function foo(a: vector, b: vector) @@ -1204,32 +1289,41 @@ end %8 = LOAD_FLOAT R0, 0i %9 = LOAD_FLOAT R0, 4i %10 = LOAD_FLOAT R0, 8i - %11 = MUL_NUM %8, %8 - %12 = MUL_NUM %9, %9 - %13 = MUL_NUM %10, %10 - %14 = ADD_NUM %11, %12 - %15 = ADD_NUM %14, %13 - %16 = SQRT_NUM %15 - %17 = DIV_NUM 1, %16 - %18 = MUL_NUM %8, %17 - %19 = MUL_NUM %9, %17 - %20 = MUL_NUM %10, %17 - STORE_VECTOR R3, %18, %19, %20 + %11 = FLOAT_TO_NUM %8 + %12 = FLOAT_TO_NUM %9 + %13 = FLOAT_TO_NUM %10 + %14 = MUL_NUM %11, %11 + %15 = MUL_NUM %12, %12 + %16 = MUL_NUM %13, %13 + %17 = ADD_NUM %14, %15 + %18 = ADD_NUM %17, %16 + %19 = SQRT_NUM %18 + %20 = DIV_NUM 1, %19 + %21 = MUL_NUM %11, %20 + %22 = MUL_NUM %12, %20 + %23 = MUL_NUM %13, %20 + %24 = NUM_TO_FLOAT %21 + %25 = NUM_TO_FLOAT %22 + %26 = NUM_TO_FLOAT %23 + STORE_VECTOR R3, %24, %25, %26 STORE_TAG R3, tvector - %25 = LOAD_FLOAT R1, 0i - %26 = LOAD_FLOAT R1, 4i - %27 = LOAD_FLOAT R1, 8i - %28 = MUL_NUM %25, %25 - %29 = MUL_NUM %26, %26 - %30 = MUL_NUM %27, %27 - %31 = ADD_NUM %28, %29 - %32 = ADD_NUM %31, %30 - %33 = SQRT_NUM %32 - %40 = LOAD_TVALUE R3 - %42 = NUM_TO_VEC %33 - %43 = MUL_VEC %40, %42 - %44 = TAG_VECTOR %43 - STORE_TVALUE R2, %44 + %31 = LOAD_FLOAT R1, 0i + %32 = LOAD_FLOAT R1, 4i + %33 = LOAD_FLOAT R1, 8i + %34 = FLOAT_TO_NUM %31 + %35 = FLOAT_TO_NUM %32 + %36 = FLOAT_TO_NUM %33 + %37 = MUL_NUM %34, %34 + %38 = MUL_NUM %35, %35 + %39 = MUL_NUM %36, %36 + %40 = ADD_NUM %37, %38 + %41 = ADD_NUM %40, %39 + %42 = SQRT_NUM %41 + %49 = LOAD_TVALUE R3, 0i, tvector + %51 = NUM_TO_VEC %42 + %52 = MUL_VEC %49, %51 + %53 = TAG_VECTOR %52 + STORE_TVALUE R2, %53 INTERRUPT 5u RETURN R2, 1i )" @@ -1238,6 +1332,11 @@ end TEST_CASE("VectorCustomNamecallChain") { + ScopedFastFlag luauCodegenHydrateLoadWithTag{FFlag::LuauCodegenHydrateLoadWithTag, true}; + ScopedFastFlag luauCodegenUpvalueLoadProp{FFlag::LuauCodegenUpvalueLoadProp, true}; + ScopedFastFlag luauCodegenFloatLoadStoreProp{FFlag::LuauCodegenFloatLoadStoreProp, true}; + ScopedFastFlag luauCodegenSplitFloat{FFlag::LuauCodegenSplitFloat, true}; + CHECK_EQ( "\n" + getCodegenAssembly(R"( local function foo(n: vector, b: vector, t: vector) @@ -1254,40 +1353,48 @@ end bb_2: JUMP bb_bytecode_1 bb_bytecode_1: - %8 = LOAD_TVALUE R2 - STORE_TVALUE R6, %8 + %8 = LOAD_TVALUE R2, 0i, tvector %14 = LOAD_FLOAT R0, 0i - %15 = LOAD_FLOAT R6, 0i + %15 = EXTRACT_VEC %8, 0i %16 = LOAD_FLOAT R0, 4i - %17 = LOAD_FLOAT R6, 4i + %17 = EXTRACT_VEC %8, 1i %18 = LOAD_FLOAT R0, 8i - %19 = LOAD_FLOAT R6, 8i - %20 = MUL_NUM %16, %19 - %21 = MUL_NUM %18, %17 - %22 = SUB_NUM %20, %21 - %23 = MUL_NUM %18, %15 - %24 = MUL_NUM %14, %19 - %25 = SUB_NUM %23, %24 - %26 = MUL_NUM %14, %17 - %27 = MUL_NUM %16, %15 + %19 = EXTRACT_VEC %8, 2i + %20 = FLOAT_TO_NUM %14 + %21 = FLOAT_TO_NUM %15 + %22 = FLOAT_TO_NUM %16 + %23 = FLOAT_TO_NUM %17 + %24 = FLOAT_TO_NUM %18 + %25 = FLOAT_TO_NUM %19 + %26 = MUL_NUM %22, %25 + %27 = MUL_NUM %24, %23 %28 = SUB_NUM %26, %27 - STORE_VECTOR R4, %22, %25, %28 - STORE_TAG R4, tvector - %31 = LOAD_TVALUE R1 - STORE_TVALUE R6, %31 - %37 = LOAD_FLOAT R4, 0i - %38 = LOAD_FLOAT R6, 0i - %39 = MUL_NUM %37, %38 - %40 = LOAD_FLOAT R4, 4i - %41 = LOAD_FLOAT R6, 4i - %42 = MUL_NUM %40, %41 - %43 = LOAD_FLOAT R4, 8i - %44 = LOAD_FLOAT R6, 8i - %45 = MUL_NUM %43, %44 - %46 = ADD_NUM %39, %42 - %47 = ADD_NUM %46, %45 - %53 = ADD_NUM %47, 1 - STORE_DOUBLE R3, %53 + %29 = MUL_NUM %24, %21 + %30 = MUL_NUM %20, %25 + %31 = SUB_NUM %29, %30 + %32 = MUL_NUM %20, %23 + %33 = MUL_NUM %22, %21 + %34 = SUB_NUM %32, %33 + %35 = NUM_TO_FLOAT %28 + %36 = NUM_TO_FLOAT %31 + %37 = NUM_TO_FLOAT %34 + %40 = LOAD_TVALUE R1, 0i, tvector + %47 = EXTRACT_VEC %40, 0i + %48 = FLOAT_TO_NUM %35 + %49 = FLOAT_TO_NUM %47 + %50 = MUL_NUM %48, %49 + %52 = EXTRACT_VEC %40, 1i + %53 = FLOAT_TO_NUM %36 + %54 = FLOAT_TO_NUM %52 + %55 = MUL_NUM %53, %54 + %57 = EXTRACT_VEC %40, 2i + %58 = FLOAT_TO_NUM %37 + %59 = FLOAT_TO_NUM %57 + %60 = MUL_NUM %58, %59 + %61 = ADD_NUM %50, %55 + %62 = ADD_NUM %61, %60 + %68 = ADD_NUM %62, 1 + STORE_DOUBLE R3, %68 STORE_TAG R3, tnumber INTERRUPT 9u RETURN R3, 1i @@ -1297,6 +1404,12 @@ end TEST_CASE("VectorCustomNamecallChain2") { + ScopedFastFlag luauCodegenSetBlockEntryState{FFlag::LuauCodegenSetBlockEntryState, true}; + ScopedFastFlag luauCodegenUpvalueLoadProp{FFlag::LuauCodegenUpvalueLoadProp, true}; + ScopedFastFlag luauCodegenFloatLoadStoreProp{FFlag::LuauCodegenFloatLoadStoreProp, true}; + ScopedFastFlag luauCodegenHydrateLoadWithTag{FFlag::LuauCodegenHydrateLoadWithTag, true}; + ScopedFastFlag luauCodegenSplitFloat{FFlag::LuauCodegenSplitFloat, true}; + CHECK_EQ( "\n" + getCodegenAssembly(R"( type Vertex = {n: vector, b: vector} @@ -1321,49 +1434,62 @@ end STORE_TVALUE R3, %11 JUMP bb_4 bb_4: - %16 = LOAD_TVALUE R1 + %16 = LOAD_TVALUE R1, 0i, tvector STORE_TVALUE R5, %16 CHECK_TAG R3, tvector, exit(3) - CHECK_TAG R5, tvector, exit(3) %22 = LOAD_FLOAT R3, 0i - %23 = LOAD_FLOAT R5, 0i + %23 = EXTRACT_VEC %16, 0i %24 = LOAD_FLOAT R3, 4i - %25 = LOAD_FLOAT R5, 4i + %25 = EXTRACT_VEC %16, 1i %26 = LOAD_FLOAT R3, 8i - %27 = LOAD_FLOAT R5, 8i - %28 = MUL_NUM %24, %27 - %29 = MUL_NUM %26, %25 - %30 = SUB_NUM %28, %29 - %31 = MUL_NUM %26, %23 - %32 = MUL_NUM %22, %27 - %33 = SUB_NUM %31, %32 - %34 = MUL_NUM %22, %25 - %35 = MUL_NUM %24, %23 + %27 = EXTRACT_VEC %16, 2i + %28 = FLOAT_TO_NUM %22 + %29 = FLOAT_TO_NUM %23 + %30 = FLOAT_TO_NUM %24 + %31 = FLOAT_TO_NUM %25 + %32 = FLOAT_TO_NUM %26 + %33 = FLOAT_TO_NUM %27 + %34 = MUL_NUM %30, %33 + %35 = MUL_NUM %32, %31 %36 = SUB_NUM %34, %35 - STORE_VECTOR R3, %30, %33, %36 - CHECK_TAG R0, ttable, exit(6) - %41 = LOAD_POINTER R0 - %42 = GET_SLOT_NODE_ADDR %41, 6u, K3 ('b') - CHECK_SLOT_MATCH %42, K3 ('b'), bb_fallback_5 - %44 = LOAD_TVALUE %42, 0i - STORE_TVALUE R5, %44 + %37 = MUL_NUM %32, %29 + %38 = MUL_NUM %28, %33 + %39 = SUB_NUM %37, %38 + %40 = MUL_NUM %28, %31 + %41 = MUL_NUM %30, %29 + %42 = SUB_NUM %40, %41 + %43 = NUM_TO_FLOAT %36 + %44 = NUM_TO_FLOAT %39 + %45 = NUM_TO_FLOAT %42 + STORE_VECTOR R3, %43, %44, %45 + %50 = LOAD_POINTER R0 + %51 = GET_SLOT_NODE_ADDR %50, 6u, K3 ('b') + CHECK_SLOT_MATCH %51, K3 ('b'), bb_fallback_5 + %53 = LOAD_TVALUE %51, 0i + STORE_TVALUE R5, %53 JUMP bb_6 bb_6: CHECK_TAG R3, tvector, exit(8) CHECK_TAG R5, tvector, exit(8) - %53 = LOAD_FLOAT R3, 0i - %54 = LOAD_FLOAT R5, 0i - %55 = MUL_NUM %53, %54 - %56 = LOAD_FLOAT R3, 4i - %57 = LOAD_FLOAT R5, 4i - %58 = MUL_NUM %56, %57 - %59 = LOAD_FLOAT R3, 8i - %60 = LOAD_FLOAT R5, 8i - %61 = MUL_NUM %59, %60 - %62 = ADD_NUM %55, %58 - %63 = ADD_NUM %62, %61 - %69 = ADD_NUM %63, 1 - STORE_DOUBLE R2, %69 + %62 = LOAD_FLOAT R3, 0i + %63 = LOAD_FLOAT R5, 0i + %64 = FLOAT_TO_NUM %62 + %65 = FLOAT_TO_NUM %63 + %66 = MUL_NUM %64, %65 + %67 = LOAD_FLOAT R3, 4i + %68 = LOAD_FLOAT R5, 4i + %69 = FLOAT_TO_NUM %67 + %70 = FLOAT_TO_NUM %68 + %71 = MUL_NUM %69, %70 + %72 = LOAD_FLOAT R3, 8i + %73 = LOAD_FLOAT R5, 8i + %74 = FLOAT_TO_NUM %72 + %75 = FLOAT_TO_NUM %73 + %76 = MUL_NUM %74, %75 + %77 = ADD_NUM %66, %71 + %78 = ADD_NUM %77, %76 + %84 = ADD_NUM %78, 1 + STORE_DOUBLE R2, %84 STORE_TAG R2, tnumber INTERRUPT 12u RETURN R2, 1i @@ -1371,6 +1497,52 @@ end ); } +TEST_CASE("VectorLibraryChain") +{ + ScopedFastFlag luauCodegenBlockSafeEnv{FFlag::LuauCodegenBlockSafeEnv, true}; + ScopedFastFlag luauCodegenHydrateLoadWithTag{FFlag::LuauCodegenHydrateLoadWithTag, true}; + ScopedFastFlag luauCodegenSplitFloat{FFlag::LuauCodegenSplitFloat, true}; + + CHECK_EQ( + "\n" + getCodegenAssembly(R"( +local function foo(a: vector, b: vector) + return vector.normalize(a) * (vector.magnitude(b) + vector.dot(a, b)) +end +)"), + R"( +; function foo($arg0, $arg1) line 2 +bb_0: + CHECK_TAG R0, tvector, exit(entry) + CHECK_TAG R1, tvector, exit(entry) + JUMP bb_2 +bb_2: + JUMP bb_bytecode_1 +bb_bytecode_1: + implicit CHECK_SAFE_ENV exit(0) + %9 = LOAD_TVALUE R0, 0i, tvector + %10 = DOT_VEC %9, %9 + %11 = FLOAT_TO_NUM %10 + %12 = SQRT_NUM %11 + %13 = DIV_NUM 1, %12 + %14 = NUM_TO_VEC %13 + %15 = MUL_VEC %9, %14 + %21 = LOAD_TVALUE R1, 0i, tvector + %22 = DOT_VEC %21, %21 + %23 = FLOAT_TO_NUM %22 + %24 = SQRT_NUM %23 + %34 = DOT_VEC %9, %21 + %35 = FLOAT_TO_NUM %34 + %44 = ADD_NUM %24, %35 + %53 = NUM_TO_VEC %44 + %54 = MUL_VEC %15, %53 + %55 = TAG_VECTOR %54 + STORE_TVALUE R2, %55 + INTERRUPT 19u + RETURN R2, 1i +)" + ); +} + TEST_CASE("UserDataGetIndex") { CHECK_EQ( @@ -1469,8 +1641,68 @@ end ); } +TEST_CASE("EntryBlockChecksAreNotInferred") +{ + ScopedFastFlag luauCodegenBlockSafeEnv{FFlag::LuauCodegenBlockSafeEnv, true}; + ScopedFastFlag luauCodegenSetBlockEntryState{FFlag::LuauCodegenSetBlockEntryState, true}; + + CHECK_EQ( + "\n" + getCodegenAssembly( + R"( +function eq(a: number, b: number, limit) + if not limit then limit = 0.125 end + return math.abs(a - b) <= limit +end +)" + ), + R"( +; function eq($arg0, $arg1, $arg2) line 2 +bb_0: + CHECK_TAG R0, tnumber, exit(entry) + CHECK_TAG R1, tnumber, exit(entry) + JUMP bb_5 +bb_5: + JUMP bb_bytecode_1 +bb_bytecode_1: + JUMP_IF_TRUTHY R2, bb_bytecode_2, bb_6 +bb_6: + STORE_DOUBLE R2, 0.125 + STORE_TAG R2, tnumber + JUMP bb_bytecode_2 +bb_bytecode_2: + implicit CHECK_SAFE_ENV exit(2) + %14 = LOAD_DOUBLE R0 + %16 = SUB_NUM %14, R1 + STORE_DOUBLE R5, %16 + STORE_TAG R5, tnumber + %23 = ABS_NUM %16 + STORE_DOUBLE R4, %23 + STORE_TAG R4, tnumber + CHECK_TAG R2, tnumber, bb_fallback_9 + %31 = LOAD_DOUBLE R2 + JUMP_CMP_NUM %23, %31, le, bb_bytecode_3, bb_8 +bb_8: + STORE_INT R3, 0i + STORE_TAG R3, tboolean + JUMP bb_bytecode_4 +bb_bytecode_3: + STORE_INT R3, 1i + STORE_TAG R3, tboolean + JUMP bb_bytecode_4 +bb_bytecode_4: + INTERRUPT 11u + RETURN R3, 1i +)" + ); +} + TEST_CASE("ExplicitUpvalueAndLocalTypes") { + ScopedFastFlag luauCodegenUpvalueLoadProp{FFlag::LuauCodegenUpvalueLoadProp, true}; + ScopedFastFlag luauCodegenFloatLoadStoreProp{FFlag::LuauCodegenFloatLoadStoreProp, true}; + ScopedFastFlag luauCodegenGcoDse{FFlag::LuauCodegenGcoDse, true}; + ScopedFastFlag luauCodegenSplitFloat{FFlag::LuauCodegenSplitFloat, true}; + CHECK_EQ( "\n" + getCodegenAssembly( R"( @@ -1490,23 +1722,24 @@ end bb_bytecode_0: CHECK_TAG R0, tvector, exit(0) %2 = LOAD_FLOAT R0, 0i - STORE_DOUBLE R4, %2 + %3 = FLOAT_TO_NUM %2 + STORE_DOUBLE R4, %3 STORE_TAG R4, tnumber - %7 = LOAD_FLOAT R0, 4i - %16 = ADD_NUM %2, %7 - STORE_DOUBLE R3, %16 + %8 = LOAD_FLOAT R0, 4i + %9 = FLOAT_TO_NUM %8 + %18 = ADD_NUM %3, %9 + STORE_DOUBLE R3, %18 STORE_TAG R3, tnumber - GET_UPVALUE R5, U0 + %21 = GET_UPVALUE U0 + STORE_TVALUE R5, %21 CHECK_TAG R5, tvector, exit(6) - %22 = LOAD_FLOAT R5, 0i - %31 = ADD_NUM %16, %22 - STORE_DOUBLE R2, %31 - STORE_TAG R2, tnumber - GET_UPVALUE R4, U0 - CHECK_TAG R4, tvector, exit(10) - %37 = LOAD_FLOAT R4, 4i - %46 = ADD_NUM %31, %37 - STORE_DOUBLE R1, %46 + %25 = EXTRACT_VEC %21, 0i + %26 = FLOAT_TO_NUM %25 + %35 = ADD_NUM %18, %26 + %42 = EXTRACT_VEC %21, 1i + %43 = FLOAT_TO_NUM %42 + %52 = ADD_NUM %35, %43 + STORE_DOUBLE R1, %52 STORE_TAG R1, tnumber INTERRUPT 13u RETURN R1, 1i @@ -1514,11 +1747,136 @@ end ); } +TEST_CASE("DuplicateArrayLoads") +{ + ScopedFastFlag luauCodegenSetBlockEntryState{FFlag::LuauCodegenSetBlockEntryState, true}; + + // TODO: opportunity - if we can track heap space version, we can remove duplicate loads + CHECK_EQ( + "\n" + getCodegenAssembly( + R"( +local function foo(n: number, t: {number}, u: {number}) + return t[n] * t[n] + u[n] * u[n] +end +)" + ), + R"( +; function foo($arg0, $arg1, $arg2) line 2 +bb_0: + CHECK_TAG R0, tnumber, exit(entry) + CHECK_TAG R1, ttable, exit(entry) + CHECK_TAG R2, ttable, exit(entry) + JUMP bb_2 +bb_2: + JUMP bb_bytecode_1 +bb_bytecode_1: + %12 = LOAD_POINTER R1 + %13 = LOAD_DOUBLE R0 + %14 = TRY_NUM_TO_INDEX %13, bb_fallback_3 + %15 = SUB_INT %14, 1i + CHECK_ARRAY_SIZE %12, %15, bb_fallback_3 + CHECK_NO_METATABLE %12, bb_fallback_3 + %18 = GET_ARR_ADDR %12, %15 + %19 = LOAD_TVALUE %18 + STORE_TVALUE R5, %19 + JUMP bb_linear_17 +bb_linear_17: + %124 = LOAD_TVALUE %18 + STORE_TVALUE R6, %124 + CHECK_TAG R5, tnumber, bb_fallback_7 + CHECK_TAG R6, tnumber, bb_fallback_7 + %131 = LOAD_DOUBLE R5 + %133 = MUL_NUM %131, R6 + STORE_DOUBLE R4, %133 + STORE_TAG R4, tnumber + %137 = LOAD_POINTER R2 + CHECK_ARRAY_SIZE %137, %15, bb_fallback_9 + CHECK_NO_METATABLE %137, bb_fallback_9 + %143 = GET_ARR_ADDR %137, %15 + %144 = LOAD_TVALUE %143 + STORE_TVALUE R6, %144 + %154 = LOAD_TVALUE %143 + STORE_TVALUE R7, %154 + CHECK_TAG R6, tnumber, bb_fallback_13 + CHECK_TAG R7, tnumber, bb_fallback_13 + %161 = LOAD_DOUBLE R6 + %163 = MUL_NUM %161, R7 + %173 = ADD_NUM %133, %163 + STORE_DOUBLE R3, %173 + STORE_TAG R3, tnumber + INTERRUPT 7u + RETURN R3, 1i +bb_4: + %29 = LOAD_POINTER R1 + %30 = LOAD_DOUBLE R0 + %31 = TRY_NUM_TO_INDEX %30, bb_fallback_5 + %32 = SUB_INT %31, 1i + CHECK_ARRAY_SIZE %29, %32, bb_fallback_5 + CHECK_NO_METATABLE %29, bb_fallback_5 + %35 = GET_ARR_ADDR %29, %32 + %36 = LOAD_TVALUE %35 + STORE_TVALUE R6, %36 + JUMP bb_6 +bb_6: + CHECK_TAG R5, tnumber, bb_fallback_7 + CHECK_TAG R6, tnumber, bb_fallback_7 + %46 = LOAD_DOUBLE R5 + %48 = MUL_NUM %46, R6 + STORE_DOUBLE R4, %48 + STORE_TAG R4, tnumber + JUMP bb_8 +bb_8: + %59 = LOAD_POINTER R2 + %60 = LOAD_DOUBLE R0 + %61 = TRY_NUM_TO_INDEX %60, bb_fallback_9 + %62 = SUB_INT %61, 1i + CHECK_ARRAY_SIZE %59, %62, bb_fallback_9 + CHECK_NO_METATABLE %59, bb_fallback_9 + %65 = GET_ARR_ADDR %59, %62 + %66 = LOAD_TVALUE %65 + STORE_TVALUE R6, %66 + JUMP bb_10 +bb_10: + %76 = LOAD_POINTER R2 + %77 = LOAD_DOUBLE R0 + %78 = TRY_NUM_TO_INDEX %77, bb_fallback_11 + %79 = SUB_INT %78, 1i + CHECK_ARRAY_SIZE %76, %79, bb_fallback_11 + CHECK_NO_METATABLE %76, bb_fallback_11 + %82 = GET_ARR_ADDR %76, %79 + %83 = LOAD_TVALUE %82 + STORE_TVALUE R7, %83 + JUMP bb_12 +bb_12: + CHECK_TAG R6, tnumber, bb_fallback_13 + CHECK_TAG R7, tnumber, bb_fallback_13 + %93 = LOAD_DOUBLE R6 + %95 = MUL_NUM %93, R7 + STORE_DOUBLE R5, %95 + STORE_TAG R5, tnumber + JUMP bb_14 +bb_14: + CHECK_TAG R4, tnumber, bb_fallback_15 + CHECK_TAG R5, tnumber, bb_fallback_15 + %106 = LOAD_DOUBLE R4 + %108 = ADD_NUM %106, R5 + STORE_DOUBLE R3, %108 + STORE_TAG R3, tnumber + JUMP bb_16 +bb_16: + INTERRUPT 7u + RETURN R3, 1i +)" + ); +} + #if LUA_VECTOR_SIZE == 3 TEST_CASE("FastcallTypeInferThroughLocal") { ScopedFastFlag luauCodegenBlockSafeEnv{FFlag::LuauCodegenBlockSafeEnv, true}; + ScopedFastFlag luauCodegenSplitFloat{FFlag::LuauCodegenSplitFloat, true}; + // TODO: opportunity - bb_3 has only one predecessor, but doesn't retain any info from it CHECK_EQ( "\n" + getCodegenAssembly( R"( @@ -1544,22 +1902,26 @@ end STORE_TAG R5, tnumber CHECK_TAG R0, tnumber, exit(4) %11 = LOAD_DOUBLE R0 - STORE_VECTOR R2, %11, 2, 3 + %14 = NUM_TO_FLOAT %11 + STORE_VECTOR R2, %14, 2, 3 STORE_TAG R2, tvector JUMP_IF_FALSY R1, bb_bytecode_1, bb_3 bb_3: CHECK_TAG R2, tvector, exit(9) - %19 = LOAD_FLOAT R2, 0i - %24 = LOAD_FLOAT R2, 4i - %33 = ADD_NUM %19, %24 - STORE_DOUBLE R3, %33 + %22 = LOAD_FLOAT R2, 0i + %23 = FLOAT_TO_NUM %22 + %28 = LOAD_FLOAT R2, 4i + %29 = FLOAT_TO_NUM %28 + %38 = ADD_NUM %23, %29 + STORE_DOUBLE R3, %38 STORE_TAG R3, tnumber INTERRUPT 14u RETURN R3, 1i bb_bytecode_1: CHECK_TAG R2, tvector, exit(15) - %40 = LOAD_FLOAT R2, 8i - STORE_DOUBLE R3, %40 + %45 = LOAD_FLOAT R2, 8i + %46 = FLOAT_TO_NUM %45 + STORE_DOUBLE R3, %46 STORE_TAG R3, tnumber INTERRUPT 17u RETURN R3, 1i @@ -1570,6 +1932,11 @@ end TEST_CASE("FastcallTypeInferThroughUpvalue") { ScopedFastFlag luauCodegenBlockSafeEnv{FFlag::LuauCodegenBlockSafeEnv, true}; + ScopedFastFlag luauCodegenHydrateLoadWithTag{FFlag::LuauCodegenHydrateLoadWithTag, true}; + ScopedFastFlag luauCodegenUpvalueLoadProp{FFlag::LuauCodegenUpvalueLoadProp, true}; + ScopedFastFlag luauCodegenFloatLoadStoreProp{FFlag::LuauCodegenFloatLoadStoreProp, true}; + ScopedFastFlag luauCodegenGcoDse{FFlag::LuauCodegenGcoDse, true}; + ScopedFastFlag luauCodegenSplitFloat{FFlag::LuauCodegenSplitFloat, true}; CHECK_EQ( "\n" + getCodegenAssembly( @@ -1598,29 +1965,32 @@ end STORE_TAG R5, tnumber CHECK_TAG R0, tnumber, exit(4) %11 = LOAD_DOUBLE R0 - STORE_VECTOR R2, %11, 2, 3 + %14 = NUM_TO_FLOAT %11 + STORE_VECTOR R2, %14, 2, 3 STORE_TAG R2, tvector - SET_UPVALUE U0, R2, tvector + %19 = LOAD_TVALUE R2, 0i, tvector + SET_UPVALUE U0, %19, tvector JUMP_IF_FALSY R1, bb_bytecode_1, bb_3 bb_3: - GET_UPVALUE R4, U0 + %22 = GET_UPVALUE U0 + STORE_TVALUE R4, %22 CHECK_TAG R4, tvector, exit(11) - %21 = LOAD_FLOAT R4, 0i - STORE_DOUBLE R3, %21 - STORE_TAG R3, tnumber - GET_UPVALUE R5, U0 - CHECK_TAG R5, tvector, exit(14) - %27 = LOAD_FLOAT R5, 4i - %36 = ADD_NUM %21, %27 - STORE_DOUBLE R2, %36 + %26 = EXTRACT_VEC %22, 0i + %27 = FLOAT_TO_NUM %26 + %34 = EXTRACT_VEC %22, 1i + %35 = FLOAT_TO_NUM %34 + %44 = ADD_NUM %27, %35 + STORE_DOUBLE R2, %44 STORE_TAG R2, tnumber INTERRUPT 17u RETURN R2, 1i bb_bytecode_1: - GET_UPVALUE R3, U0 + %49 = GET_UPVALUE U0 + STORE_TVALUE R3, %49 CHECK_TAG R3, tvector, exit(19) - %44 = LOAD_FLOAT R3, 8i - STORE_DOUBLE R2, %44 + %53 = EXTRACT_VEC %49, 2i + %54 = FLOAT_TO_NUM %53 + STORE_DOUBLE R2, %54 STORE_TAG R2, tnumber INTERRUPT 21u RETURN R2, 1i @@ -1702,6 +2072,10 @@ end TEST_CASE("ArgumentTypeRefinement") { ScopedFastFlag luauCodegenBlockSafeEnv{FFlag::LuauCodegenBlockSafeEnv, true}; + ScopedFastFlag luauCodegenHydrateLoadWithTag{FFlag::LuauCodegenHydrateLoadWithTag, true}; + ScopedFastFlag luauCodegenUpvalueLoadProp{FFlag::LuauCodegenUpvalueLoadProp, true}; + ScopedFastFlag luauCodegenFloatLoadStoreProp{FFlag::LuauCodegenFloatLoadStoreProp, true}; + ScopedFastFlag luauCodegenSplitFloat{FFlag::LuauCodegenSplitFloat, true}; CHECK_EQ( "\n" + getCodegenAssembly( @@ -1724,14 +2098,12 @@ end STORE_TAG R5, tnumber CHECK_TAG R1, tnumber, exit(4) %12 = LOAD_DOUBLE R1 - STORE_VECTOR R2, 1, %12, 3 + %15 = NUM_TO_FLOAT %12 + STORE_VECTOR R2, 1, %15, 3 STORE_TAG R2, tvector - %16 = LOAD_TVALUE R2 - STORE_TVALUE R0, %16 - %20 = LOAD_FLOAT R0, 4i - %25 = LOAD_FLOAT R0, 8i - %34 = ADD_NUM %20, %25 - STORE_DOUBLE R2, %34 + %24 = FLOAT_TO_NUM %15 + %39 = ADD_NUM %24, 3 + STORE_DOUBLE R2, %39 STORE_TAG R2, tnumber INTERRUPT 14u RETURN R2, 1i @@ -1743,6 +2115,7 @@ end TEST_CASE("InlineFunctionType") { ScopedFastFlag luauCodegenFloatLoadStoreProp{FFlag::LuauCodegenFloatLoadStoreProp, true}; + ScopedFastFlag luauCodegenSplitFloat{FFlag::LuauCodegenSplitFloat, true}; CHECK_EQ( "\n" + getCodegenAssembly( @@ -1769,8 +2142,9 @@ end JUMP bb_bytecode_1 bb_bytecode_1: %8 = LOAD_FLOAT R0, 4i - %17 = MUL_NUM %8, R1 - STORE_DOUBLE R2, %17 + %9 = FLOAT_TO_NUM %8 + %18 = MUL_NUM %9, R1 + STORE_DOUBLE R2, %18 STORE_TAG R2, tnumber INTERRUPT 3u RETURN R2, 1i @@ -1780,10 +2154,11 @@ end bb_bytecode_0: CHECK_TAG R0, tvector, exit(0) %2 = LOAD_FLOAT R0, 4i - %8 = MUL_NUM %2, 3 - %19 = MUL_NUM %2, 5 - %28 = ADD_NUM %8, %19 - STORE_DOUBLE R1, %28 + %3 = FLOAT_TO_NUM %2 + %9 = MUL_NUM %3, 3 + %21 = MUL_NUM %3, 5 + %30 = ADD_NUM %9, %21 + STORE_DOUBLE R1, %30 STORE_TAG R1, tnumber INTERRUPT 7u RETURN R1, 1i @@ -1793,6 +2168,8 @@ end TEST_CASE("ResolveTablePathTypes") { + ScopedFastFlag luauCodegenSplitFloat{FFlag::LuauCodegenSplitFloat, true}; + CHECK_EQ( "\n" + getCodegenAssembly( R"( @@ -1840,7 +2217,8 @@ end bb_6: CHECK_TAG R4, tvector, exit(3) %33 = LOAD_FLOAT R4, 4i - STORE_DOUBLE R3, %33 + %34 = FLOAT_TO_NUM %33 + STORE_DOUBLE R3, %34 STORE_TAG R3, tnumber INTERRUPT 5u RETURN R3, 1i @@ -1906,6 +2284,12 @@ end TEST_CASE("ResolveVectorNamecalls") { + ScopedFastFlag luauCodegenHydrateLoadWithTag{FFlag::LuauCodegenHydrateLoadWithTag, true}; + ScopedFastFlag luauCodegenUpvalueLoadProp{FFlag::LuauCodegenUpvalueLoadProp, true}; + ScopedFastFlag luauCodegenFloatLoadStoreProp{FFlag::LuauCodegenFloatLoadStoreProp, true}; + + ScopedFastFlag luauCodegenSplitFloat{FFlag::LuauCodegenSplitFloat, true}; + CHECK_EQ( "\n" + getCodegenAssembly( R"( @@ -1951,17 +2335,17 @@ end STORE_TVALUE R4, %31 CHECK_TAG R2, tvector, exit(4) %37 = LOAD_FLOAT R2, 0i - %38 = LOAD_FLOAT R4, 0i - %39 = MUL_NUM %37, %38 - %40 = LOAD_FLOAT R2, 4i - %41 = LOAD_FLOAT R4, 4i - %42 = MUL_NUM %40, %41 - %43 = LOAD_FLOAT R2, 8i - %44 = LOAD_FLOAT R4, 8i - %45 = MUL_NUM %43, %44 - %46 = ADD_NUM %39, %42 - %47 = ADD_NUM %46, %45 - STORE_DOUBLE R2, %47 + %39 = FLOAT_TO_NUM %37 + %41 = MUL_NUM %39, 0.7070000171661377 + %42 = LOAD_FLOAT R2, 4i + %44 = FLOAT_TO_NUM %42 + %46 = MUL_NUM %44, 0 + %47 = LOAD_FLOAT R2, 8i + %49 = FLOAT_TO_NUM %47 + %51 = MUL_NUM %49, 0.7070000171661377 + %52 = ADD_NUM %41, %46 + %53 = ADD_NUM %52, %51 + STORE_DOUBLE R2, %53 STORE_TAG R2, tnumber ADJUST_STACK_TO_REG R2, 1i INTERRUPT 7u @@ -1972,6 +2356,8 @@ end TEST_CASE("ImmediateTypeAnnotationHelp") { + ScopedFastFlag luauCodegenHydrateLoadWithTag{FFlag::LuauCodegenHydrateLoadWithTag, true}; + CHECK_EQ( "\n" + getCodegenAssembly( R"( @@ -1999,7 +2385,7 @@ end JUMP bb_2 bb_2: CHECK_TAG R3, tvector, exit(1) - %19 = LOAD_TVALUE R3 + %19 = LOAD_TVALUE R3, 0i, tvector %20 = NUM_TO_VEC 5 %21 = DIV_VEC %19, %20 %22 = TAG_VECTOR %21 @@ -2037,6 +2423,8 @@ end TEST_CASE("ForInManualAnnotation") { ScopedFastFlag luauCodegenBlockSafeEnv{FFlag::LuauCodegenBlockSafeEnv, true}; + ScopedFastFlag luauCodegenHydrateLoadWithTag{FFlag::LuauCodegenHydrateLoadWithTag, true}; + ScopedFastFlag luauCodegenSplitFloat{FFlag::LuauCodegenSplitFloat, true}; CHECK_EQ( "\n" + getCodegenAssembly( @@ -2071,7 +2459,7 @@ end STORE_DOUBLE R1, 0 STORE_TAG R1, tnumber GET_CACHED_IMPORT R2, K1 (nil), 1073741824u ('ipairs'), 2u - %8 = LOAD_TVALUE R0 + %8 = LOAD_TVALUE R0, 0i, ttable STORE_TVALUE R3, %8 INTERRUPT 4u SET_SAVEDPC 5u @@ -2097,30 +2485,31 @@ end bb_8: CHECK_TAG R8, tvector, exit(8) %38 = LOAD_FLOAT R8, 0i - STORE_DOUBLE R7, %38 + %39 = FLOAT_TO_NUM %38 + STORE_DOUBLE R7, %39 STORE_TAG R7, tnumber CHECK_TAG R1, tnumber, exit(10) - %45 = LOAD_DOUBLE R1 - %47 = ADD_NUM %45, %38 - STORE_DOUBLE R1, %47 + %46 = LOAD_DOUBLE R1 + %48 = ADD_NUM %46, %39 + STORE_DOUBLE R1, %48 JUMP bb_bytecode_3 bb_bytecode_3: INTERRUPT 11u CHECK_TAG R2, tnil, bb_fallback_10 - %53 = LOAD_POINTER R3 - %54 = LOAD_INT R4 - %55 = GET_ARR_ADDR %53, %54 - CHECK_ARRAY_SIZE %53, %54, bb_9 - %57 = LOAD_TAG %55 - JUMP_EQ_TAG %57, tnil, bb_9, bb_11 + %54 = LOAD_POINTER R3 + %55 = LOAD_INT R4 + %56 = GET_ARR_ADDR %54, %55 + CHECK_ARRAY_SIZE %54, %55, bb_9 + %58 = LOAD_TAG %56 + JUMP_EQ_TAG %58, tnil, bb_9, bb_11 bb_11: - %59 = ADD_INT %54, 1i - STORE_INT R4, %59 - %61 = INT_TO_NUM %59 - STORE_DOUBLE R5, %61 + %60 = ADD_INT %55, 1i + STORE_INT R4, %60 + %62 = INT_TO_NUM %60 + STORE_DOUBLE R5, %62 STORE_TAG R5, tnumber - %64 = LOAD_TVALUE %55 - STORE_TVALUE R6, %64 + %65 = LOAD_TVALUE %56 + STORE_TVALUE R6, %65 JUMP bb_bytecode_2 bb_9: INTERRUPT 13u @@ -2236,6 +2625,8 @@ TEST_CASE("CustomUserdataPropertyAccess") if (!Luau::CodeGen::isSupported()) return; + ScopedFastFlag luauCodegenSplitFloat{FFlag::LuauCodegenSplitFloat, true}; + CHECK_EQ( "\n" + getCodegenAssembly( R"( @@ -2257,9 +2648,11 @@ end %6 = LOAD_POINTER R0 CHECK_USERDATA_TAG %6, 12i, exit(0) %8 = BUFFER_READF32 %6, 0i, tuserdata - %15 = BUFFER_READF32 %6, 4i, tuserdata - %24 = ADD_NUM %8, %15 - STORE_DOUBLE R1, %24 + %9 = FLOAT_TO_NUM %8 + %16 = BUFFER_READF32 %6, 4i, tuserdata + %17 = FLOAT_TO_NUM %16 + %26 = ADD_NUM %9, %17 + STORE_DOUBLE R1, %26 STORE_TAG R1, tnumber INTERRUPT 5u RETURN R1, 1i @@ -2273,6 +2666,8 @@ TEST_CASE("CustomUserdataPropertyAccess2") if (!Luau::CodeGen::isSupported()) return; + ScopedFastFlag luauCodegenHydrateLoadWithTag{FFlag::LuauCodegenHydrateLoadWithTag, true}; + CHECK_EQ( "\n" + getCodegenAssembly( R"( @@ -2295,8 +2690,8 @@ end FALLBACK_GETTABLEKS 2u, R3, R0, K1 ('Row2') CHECK_TAG R2, tvector, exit(4) CHECK_TAG R3, tvector, exit(4) - %14 = LOAD_TVALUE R2 - %15 = LOAD_TVALUE R3 + %14 = LOAD_TVALUE R2, 0i, tvector + %15 = LOAD_TVALUE R3, 0i, tvector %16 = MUL_VEC %14, %15 %17 = TAG_VECTOR %16 STORE_TVALUE R1, %17 @@ -2312,6 +2707,9 @@ TEST_CASE("CustomUserdataNamecall1") if (!Luau::CodeGen::isSupported()) return; + ScopedFastFlag luauCodegenHydrateLoadWithTag{FFlag::LuauCodegenHydrateLoadWithTag, true}; + ScopedFastFlag luauCodegenSplitFloat{FFlag::LuauCodegenSplitFloat, true}; + CHECK_EQ( "\n" + getCodegenAssembly( R"( @@ -2332,7 +2730,7 @@ end bb_2: JUMP bb_bytecode_1 bb_bytecode_1: - %6 = LOAD_TVALUE R1 + %6 = LOAD_TVALUE R1, 0i, tuserdata STORE_TVALUE R4, %6 %10 = LOAD_POINTER R0 CHECK_USERDATA_TAG %10, 12i, exit(1) @@ -2340,12 +2738,16 @@ end CHECK_USERDATA_TAG %14, 12i, exit(1) %16 = BUFFER_READF32 %10, 0i, tuserdata %17 = BUFFER_READF32 %14, 0i, tuserdata - %18 = MUL_NUM %16, %17 - %19 = BUFFER_READF32 %10, 4i, tuserdata - %20 = BUFFER_READF32 %14, 4i, tuserdata - %21 = MUL_NUM %19, %20 - %22 = ADD_NUM %18, %21 - STORE_DOUBLE R2, %22 + %18 = FLOAT_TO_NUM %16 + %19 = FLOAT_TO_NUM %17 + %20 = MUL_NUM %18, %19 + %21 = BUFFER_READF32 %10, 4i, tuserdata + %22 = BUFFER_READF32 %14, 4i, tuserdata + %23 = FLOAT_TO_NUM %21 + %24 = FLOAT_TO_NUM %22 + %25 = MUL_NUM %23, %24 + %26 = ADD_NUM %20, %25 + STORE_DOUBLE R2, %26 STORE_TAG R2, tnumber ADJUST_STACK_TO_REG R2, 1i INTERRUPT 4u @@ -2360,6 +2762,9 @@ TEST_CASE("CustomUserdataNamecall2") if (!Luau::CodeGen::isSupported()) return; + ScopedFastFlag luauCodegenHydrateLoadWithTag{FFlag::LuauCodegenHydrateLoadWithTag, true}; + ScopedFastFlag luauCodegenSplitFloat{FFlag::LuauCodegenSplitFloat, true}; + CHECK_EQ( "\n" + getCodegenAssembly( R"( @@ -2380,7 +2785,7 @@ end bb_2: JUMP bb_bytecode_1 bb_bytecode_1: - %6 = LOAD_TVALUE R1 + %6 = LOAD_TVALUE R1, 0i, tuserdata STORE_TVALUE R4, %6 %10 = LOAD_POINTER R0 CHECK_USERDATA_TAG %10, 12i, exit(1) @@ -2388,15 +2793,21 @@ end CHECK_USERDATA_TAG %14, 12i, exit(1) %16 = BUFFER_READF32 %10, 0i, tuserdata %17 = BUFFER_READF32 %14, 0i, tuserdata - %18 = MIN_NUM %16, %17 - %19 = BUFFER_READF32 %10, 4i, tuserdata - %20 = BUFFER_READF32 %14, 4i, tuserdata - %21 = MIN_NUM %19, %20 + %18 = FLOAT_TO_NUM %16 + %19 = FLOAT_TO_NUM %17 + %20 = MIN_NUM %18, %19 + %21 = BUFFER_READF32 %10, 4i, tuserdata + %22 = BUFFER_READF32 %14, 4i, tuserdata + %23 = FLOAT_TO_NUM %21 + %24 = FLOAT_TO_NUM %22 + %25 = MIN_NUM %23, %24 + %26 = NUM_TO_FLOAT %20 + %27 = NUM_TO_FLOAT %25 CHECK_GC - %23 = NEW_USERDATA 8i, 12i - BUFFER_WRITEF32 %23, 0i, %18, tuserdata - BUFFER_WRITEF32 %23, 4i, %21, tuserdata - STORE_POINTER R2, %23 + %29 = NEW_USERDATA 8i, 12i + BUFFER_WRITEF32 %29, 0i, %26, tuserdata + BUFFER_WRITEF32 %29, 4i, %27, tuserdata + STORE_POINTER R2, %29 STORE_TAG R2, tuserdata ADJUST_STACK_TO_REG R2, 1i INTERRUPT 4u @@ -2505,10 +2916,15 @@ end TEST_CASE("CustomUserdataMetamethod") { + ScopedFastFlag luauCodegenBufferLoadProp{FFlag::LuauCodegenBufferLoadProp2, true}; + ScopedFastFlag luauCodegenGcoDse{FFlag::LuauCodegenGcoDse, true}; + ScopedFastFlag luauCodegenSplitFloat{FFlag::LuauCodegenSplitFloat, true}; + // This test requires runtime component to be present if (!Luau::CodeGen::isSupported()) return; + // TODO: opportunity - with float load/store separated from float/double conversion, reads at the end can be removed CHECK_EQ( "\n" + getCodegenAssembly( R"( @@ -2535,39 +2951,46 @@ end CHECK_USERDATA_TAG %10, 12i, exit(0) %12 = BUFFER_READF32 %10, 0i, tuserdata %13 = BUFFER_READF32 %10, 4i, tuserdata - %14 = UNM_NUM %12 - %15 = UNM_NUM %13 + %14 = FLOAT_TO_NUM %12 + %15 = FLOAT_TO_NUM %13 + %16 = UNM_NUM %14 + %17 = UNM_NUM %15 + %18 = NUM_TO_FLOAT %16 + %19 = NUM_TO_FLOAT %17 CHECK_GC - %17 = NEW_USERDATA 8i, 12i - BUFFER_WRITEF32 %17, 0i, %14, tuserdata - BUFFER_WRITEF32 %17, 4i, %15, tuserdata - STORE_POINTER R4, %17 + %21 = NEW_USERDATA 8i, 12i + BUFFER_WRITEF32 %21, 0i, %18, tuserdata + BUFFER_WRITEF32 %21, 4i, %19, tuserdata + STORE_POINTER R4, %21 STORE_TAG R4, tuserdata - %26 = LOAD_POINTER R0 - CHECK_USERDATA_TAG %26, 12i, exit(1) - %28 = LOAD_POINTER R1 - CHECK_USERDATA_TAG %28, 12i, exit(1) - %30 = BUFFER_READF32 %26, 0i, tuserdata - %31 = BUFFER_READF32 %28, 0i, tuserdata - %32 = MUL_NUM %30, %31 - %33 = BUFFER_READF32 %26, 4i, tuserdata - %34 = BUFFER_READF32 %28, 4i, tuserdata - %35 = MUL_NUM %33, %34 - %37 = NEW_USERDATA 8i, 12i - BUFFER_WRITEF32 %37, 0i, %32, tuserdata - BUFFER_WRITEF32 %37, 4i, %35, tuserdata - STORE_POINTER R5, %37 - STORE_TAG R5, tuserdata - %50 = BUFFER_READF32 %17, 0i, tuserdata - %51 = BUFFER_READF32 %37, 0i, tuserdata - %52 = ADD_NUM %50, %51 - %53 = BUFFER_READF32 %17, 4i, tuserdata - %54 = BUFFER_READF32 %37, 4i, tuserdata - %55 = ADD_NUM %53, %54 - %57 = NEW_USERDATA 8i, 12i - BUFFER_WRITEF32 %57, 0i, %52, tuserdata - BUFFER_WRITEF32 %57, 4i, %55, tuserdata - STORE_POINTER R3, %57 + %30 = LOAD_POINTER R0 + CHECK_USERDATA_TAG %30, 12i, exit(1) + %32 = LOAD_POINTER R1 + CHECK_USERDATA_TAG %32, 12i, exit(1) + %34 = BUFFER_READF32 %30, 0i, tuserdata + %35 = BUFFER_READF32 %32, 0i, tuserdata + %36 = FLOAT_TO_NUM %34 + %37 = FLOAT_TO_NUM %35 + %38 = MUL_NUM %36, %37 + %39 = BUFFER_READF32 %30, 4i, tuserdata + %40 = BUFFER_READF32 %32, 4i, tuserdata + %41 = FLOAT_TO_NUM %39 + %42 = FLOAT_TO_NUM %40 + %43 = MUL_NUM %41, %42 + %44 = NUM_TO_FLOAT %38 + %45 = NUM_TO_FLOAT %43 + %62 = FLOAT_TO_NUM %18 + %63 = FLOAT_TO_NUM %44 + %64 = ADD_NUM %62, %63 + %67 = FLOAT_TO_NUM %19 + %68 = FLOAT_TO_NUM %45 + %69 = ADD_NUM %67, %68 + %70 = NUM_TO_FLOAT %64 + %71 = NUM_TO_FLOAT %69 + %73 = NEW_USERDATA 8i, 12i + BUFFER_WRITEF32 %73, 0i, %70, tuserdata + BUFFER_WRITEF32 %73, 4i, %71, tuserdata + STORE_POINTER R3, %73 STORE_TAG R3, tuserdata INTERRUPT 3u RETURN R3, 1i @@ -2579,6 +3002,7 @@ TEST_CASE("CustomUserdataMapping") { ScopedFastFlag luauCompileUnusedUdataFix{FFlag::LuauCompileUnusedUdataFix, true}; ScopedFastFlag luauCodegenBlockSafeEnv{FFlag::LuauCodegenBlockSafeEnv, true}; + ScopedFastFlag luauCodegenHydrateLoadWithTag{FFlag::LuauCodegenHydrateLoadWithTag, true}; // This test requires runtime component to be present if (!Luau::CodeGen::isSupported()) @@ -2604,7 +3028,7 @@ end bb_bytecode_1: implicit CHECK_SAFE_ENV exit(0) GET_CACHED_IMPORT R1, K1 (nil), 1073741824u ('print'), 1u - %6 = LOAD_TVALUE R0 + %6 = LOAD_TVALUE R0, 0i, tuserdata STORE_TVALUE R2, %6 GET_CACHED_IMPORT R3, K4 (nil), 2149583872u ('vec2'.'create'), 4u STORE_DOUBLE R4, 0 @@ -2625,6 +3049,8 @@ end TEST_CASE("LibraryFieldTypesAndConstants") { + ScopedFastFlag luauCodegenHydrateLoadWithTag{FFlag::LuauCodegenHydrateLoadWithTag, true}; + CHECK_EQ( "\n" + getCodegenAssembly( R"( @@ -2647,7 +3073,7 @@ end JUMP bb_bytecode_1 bb_bytecode_1: %4 = LOAD_TVALUE K0 (1, 0, 0), 0i, tvector - %11 = LOAD_TVALUE R0 + %11 = LOAD_TVALUE R0, 0i, tvector %12 = MUL_VEC %4, %11 %15 = LOAD_TVALUE K1 (0, 1, 0), 0i, tvector %23 = ADD_VEC %12, %15 @@ -2661,6 +3087,8 @@ end TEST_CASE("LibraryFieldTypesAndConstants") { + ScopedFastFlag luauCodegenHydrateLoadWithTag{FFlag::LuauCodegenHydrateLoadWithTag, true}; + CHECK_EQ( "\n" + getCodegenAssembly( R"( @@ -2683,7 +3111,7 @@ end JUMP bb_bytecode_1 bb_bytecode_1: %4 = LOAD_TVALUE K0 (0, 0, 0), 0i, tvector - %11 = LOAD_TVALUE R0 + %11 = LOAD_TVALUE R0, 0i, tvector %12 = ADD_VEC %4, %11 %13 = TAG_VECTOR %12 STORE_TVALUE R1, %13 @@ -2756,6 +3184,7 @@ end TEST_CASE("VectorLoadReuse") { ScopedFastFlag luauCodegenFloatLoadStoreProp{FFlag::LuauCodegenFloatLoadStoreProp, true}; + ScopedFastFlag luauCodegenSplitFloat{FFlag::LuauCodegenSplitFloat, true}; CHECK_EQ( "\n" + getCodegenAssembly(R"( @@ -2772,11 +3201,13 @@ end JUMP bb_bytecode_1 bb_bytecode_1: %6 = LOAD_FLOAT R0, 0i - %20 = MUL_NUM %6, %6 - %25 = LOAD_FLOAT R0, 4i - %39 = MUL_NUM %25, %25 - %48 = ADD_NUM %20, %39 - STORE_DOUBLE R1, %48 + %7 = FLOAT_TO_NUM %6 + %22 = MUL_NUM %7, %7 + %27 = LOAD_FLOAT R0, 4i + %28 = FLOAT_TO_NUM %27 + %43 = MUL_NUM %28, %28 + %52 = ADD_NUM %22, %43 + STORE_DOUBLE R1, %52 STORE_TAG R1, tnumber INTERRUPT 11u RETURN R1, 1i @@ -2787,6 +3218,7 @@ end TEST_CASE("VectorShuffle1") { ScopedFastFlag luauCodegenBlockSafeEnv{FFlag::LuauCodegenBlockSafeEnv, true}; + ScopedFastFlag luauCodegenSplitFloat{FFlag::LuauCodegenSplitFloat, true}; // TODO: opportunity - if we introduce a separate vector shuffle instruction, this can be done in a single shuffle (+/- load and store) CHECK_EQ( @@ -2805,9 +3237,9 @@ end bb_bytecode_1: implicit CHECK_SAFE_ENV exit(0) %6 = LOAD_FLOAT R0, 8i - %11 = LOAD_FLOAT R0, 0i - %16 = LOAD_FLOAT R0, 4i - STORE_VECTOR R1, %6, %11, %16 + %12 = LOAD_FLOAT R0, 0i + %18 = LOAD_FLOAT R0, 4i + STORE_VECTOR R1, %6, %12, %18 STORE_TAG R1, tvector INTERRUPT 10u RETURN R1, 1i @@ -2818,9 +3250,9 @@ end TEST_CASE("VectorShuffle2") { ScopedFastFlag luauCodegenBlockSafeEnv{FFlag::LuauCodegenBlockSafeEnv, true}; + ScopedFastFlag luauCodegenSplitFloat{FFlag::LuauCodegenSplitFloat, true}; ScopedFastFlag luauCodegenFloatLoadStoreProp{FFlag::LuauCodegenFloatLoadStoreProp, true}; - // TODO: opportunity - LOAD_FLOAT performs float->double conversion and STORE_VECTOR immediately performs double->float which should be skipped CHECK_EQ( "\n" + getCodegenAssembly(R"( local function crossshuffle(v: vector, t: vector) @@ -2840,9 +3272,9 @@ end bb_bytecode_1: implicit CHECK_SAFE_ENV exit(0) %8 = LOAD_FLOAT R0, 0i - %18 = LOAD_FLOAT R0, 8i - %35 = LOAD_FLOAT R1, 4i - STORE_VECTOR R4, %18, %35, %8, tvector + %20 = LOAD_FLOAT R0, 8i + %41 = LOAD_FLOAT R1, 4i + STORE_VECTOR R4, %20, %41, %8, tvector INTERRUPT 30u RETURN R4, 1i )" @@ -2856,8 +3288,9 @@ TEST_CASE("VectorShuffleFromComposite1") return; ScopedFastFlag luauCodegenFloatLoadStoreProp{FFlag::LuauCodegenFloatLoadStoreProp, true}; + ScopedFastFlag luauCodegenBufferLoadProp{FFlag::LuauCodegenBufferLoadProp2, true}; + ScopedFastFlag luauCodegenSplitFloat{FFlag::LuauCodegenSplitFloat, true}; - // TODO: opportunity - buffer memory load-store propagation can remove duplicate loads CHECK_EQ( "\n" + getCodegenAssembly(R"( local function test(v: vertex) @@ -2875,13 +3308,13 @@ end %6 = LOAD_POINTER R0 CHECK_USERDATA_TAG %6, 13i, exit(0) %8 = BUFFER_READF32 %6, 12i, tuserdata - %22 = BUFFER_READF32 %6, 12i, tuserdata - %38 = MUL_NUM %8, %22 - %46 = BUFFER_READF32 %6, 16i, tuserdata - %60 = BUFFER_READF32 %6, 16i, tuserdata - %75 = MUL_NUM %46, %60 - %84 = ADD_NUM %38, %75 - STORE_DOUBLE R1, %84 + %9 = BUFFER_READF32 %6, 16i, tuserdata + %16 = FLOAT_TO_NUM %8 + %40 = MUL_NUM %16, %16 + %55 = FLOAT_TO_NUM %9 + %79 = MUL_NUM %55, %55 + %88 = ADD_NUM %40, %79 + STORE_DOUBLE R1, %88 STORE_TAG R1, tnumber INTERRUPT 19u RETURN R1, 1i @@ -2895,7 +3328,10 @@ TEST_CASE("VectorShuffleFromComposite2") if (!Luau::CodeGen::isSupported()) return; - // TODO: opportunity - userdata memory load-store propagation can remove loads from values that have just been stored + ScopedFastFlag luauCodegenBufferLoadProp{FFlag::LuauCodegenBufferLoadProp2, true}; + ScopedFastFlag luauCodegenGcoDse{FFlag::LuauCodegenGcoDse, true}; + ScopedFastFlag luauCodegenSplitFloat{FFlag::LuauCodegenSplitFloat, true}; + CHECK_EQ( "\n" + getCodegenAssembly(R"( local function test(v: vertex) @@ -2915,20 +3351,10 @@ end %8 = BUFFER_READF32 %6, 24i, tuserdata %9 = BUFFER_READF32 %6, 28i, tuserdata CHECK_GC - %11 = NEW_USERDATA 8i, 12i - BUFFER_WRITEF32 %11, 0i, %8, tuserdata - BUFFER_WRITEF32 %11, 4i, %9, tuserdata - %20 = BUFFER_READF32 %11, 0i, tuserdata - %27 = BUFFER_READF32 %6, 24i, tuserdata - %28 = BUFFER_READF32 %6, 28i, tuserdata - %30 = NEW_USERDATA 8i, 12i - BUFFER_WRITEF32 %30, 0i, %27, tuserdata - BUFFER_WRITEF32 %30, 4i, %28, tuserdata - STORE_POINTER R4, %30 - STORE_TAG R4, tuserdata - %39 = BUFFER_READF32 %30, 4i, tuserdata - %48 = MUL_NUM %20, %39 - STORE_DOUBLE R1, %48 + %21 = FLOAT_TO_NUM %8 + %41 = FLOAT_TO_NUM %9 + %50 = MUL_NUM %21, %41 + STORE_DOUBLE R1, %50 STORE_TAG R1, tnumber INTERRUPT 9u RETURN R1, 1i @@ -2936,12 +3362,13 @@ end ); } -// Vectors use float storage, so in some cases it is impossible to forward value that was passed to the constructor as it was double->float truncated TEST_CASE("VectorLoadStoreOnlySamePrecision") { ScopedFastFlag luauCodegenBlockSafeEnv{FFlag::LuauCodegenBlockSafeEnv, true}; + ScopedFastFlag luauCodegenUpvalueLoadProp{FFlag::LuauCodegenUpvalueLoadProp, true}; + ScopedFastFlag luauCodegenFloatLoadStoreProp{FFlag::LuauCodegenFloatLoadStoreProp, true}; + ScopedFastFlag luauCodegenSplitFloat{FFlag::LuauCodegenSplitFloat, true}; - // TODO: opportunity - LOAD_FLOAT can be replaced with a new NUM_TO_FLOAT instruction which will only handle the truncation CHECK_EQ( "\n" + getCodegenAssembly(R"( local function test(x: number, y: number) @@ -2961,14 +3388,13 @@ end implicit CHECK_SAFE_ENV exit(0) %15 = LOAD_DOUBLE R0 %16 = LOAD_DOUBLE R1 - STORE_VECTOR R2, %15, %16, 0 - STORE_TAG R2, tvector - %22 = LOAD_FLOAT R2, 0i - %27 = LOAD_FLOAT R2, 4i - %36 = ADD_NUM %22, %27 - %41 = LOAD_FLOAT R2, 8i - %50 = ADD_NUM %36, %41 - STORE_DOUBLE R3, %50 + %18 = NUM_TO_FLOAT %15 + %19 = NUM_TO_FLOAT %16 + %26 = FLOAT_TO_NUM %18 + %32 = FLOAT_TO_NUM %19 + %41 = ADD_NUM %26, %32 + %56 = ADD_NUM %41, 0 + STORE_DOUBLE R3, %56 STORE_TAG R3, tnumber INTERRUPT 16u RETURN R3, 1i @@ -3174,4 +3600,1210 @@ end ); } +TEST_CASE("OptionalOr") +{ + ScopedFastFlag luauCodegenLinearAndOr{FFlag::LuauCodegenLinearAndOr, true}; + + CHECK_EQ( + "\n" + getCodegenAssembly(R"( +local function foo(a, b) + a = a or 0 + b = b or 0 + return a + b +end +-- when a function like 'foo' is inlined, those 'default values' collapse +local function bar() + return foo(3, 4) +end +)"), + R"( +; function foo($arg0, $arg1) line 2 +bb_bytecode_0: + %0 = LOAD_TVALUE R0 + %1 = LOAD_TVALUE K0 (0) + %2 = SELECT_IF_TRUTHY %0, %0, %1 + STORE_TVALUE R0, %2 + %4 = LOAD_TVALUE R1 + %5 = LOAD_TVALUE K0 (0) + %6 = SELECT_IF_TRUTHY %4, %4, %5 + STORE_TVALUE R1, %6 + CHECK_TAG R0, tnumber, bb_fallback_1 + CHECK_TAG R1, tnumber, bb_fallback_1 + %12 = LOAD_DOUBLE R0 + %14 = ADD_NUM %12, R1 + STORE_DOUBLE R2, %14 + STORE_TAG R2, tnumber + JUMP bb_2 +bb_2: + INTERRUPT 3u + RETURN R2, 1i +; function bar() line 8 +bb_bytecode_0: + STORE_DOUBLE R0, 7 + STORE_TAG R0, tnumber + INTERRUPT 5u + RETURN R0, 1i +)" + ); +} + +TEST_CASE("LinearAndOr") +{ + ScopedFastFlag luauCodegenLinearAndOr{FFlag::LuauCodegenLinearAndOr, true}; + + CHECK_EQ( + "\n" + getCodegenAssembly(R"( +local function foo(a, b) + return a and b, a or b +end +local function bar() + local a, b = foo(3, 4) + return a, b +end +)"), + R"( +; function foo($arg0, $arg1) line 2 +bb_bytecode_0: + %0 = LOAD_TVALUE R0 + %1 = LOAD_TVALUE R1 + %2 = SELECT_IF_TRUTHY %0, %1, %0 + STORE_TVALUE R2, %2 + %6 = SELECT_IF_TRUTHY %0, %0, %1 + STORE_TVALUE R3, %6 + INTERRUPT 2u + RETURN R2, 2i +; function bar() line 5 +bb_bytecode_0: + STORE_DOUBLE R0, 4 + STORE_TAG R0, tnumber + STORE_DOUBLE R1, 3 + STORE_TAG R1, tnumber + INTERRUPT 2u + RETURN R0, 2i +)" + ); +} + +TEST_CASE("OldStyleConditional") +{ + ScopedFastFlag luauCodegenSetBlockEntryState{FFlag::LuauCodegenSetBlockEntryState, true}; + ScopedFastFlag luauCodegenLinearAndOr{FFlag::LuauCodegenLinearAndOr, true}; + ScopedFastFlag luauCodegenHydrateLoadWithTag{FFlag::LuauCodegenHydrateLoadWithTag, true}; + + // TODO: opportunity - this can be done in two SELECT_IF_TRUTHY, but we cannot match such complex sequences right now + CHECK_EQ( + "\n" + getCodegenAssembly(R"( +local function foo(a: boolean, b: number, c: number) + local x = a and b or c + return x + 1 +end +)"), + R"( +; function foo($arg0, $arg1, $arg2) line 2 +bb_0: + CHECK_TAG R0, tboolean, exit(entry) + CHECK_TAG R1, tnumber, exit(entry) + CHECK_TAG R2, tnumber, exit(entry) + JUMP bb_4 +bb_4: + JUMP bb_bytecode_1 +bb_bytecode_1: + JUMP_IF_FALSY R0, bb_bytecode_2, bb_5 +bb_5: + %9 = LOAD_TVALUE R1, 0i, tnumber + STORE_TVALUE R3, %9 + JUMP bb_bytecode_3 +bb_bytecode_2: + %12 = LOAD_TVALUE R2, 0i, tnumber + STORE_TVALUE R3, %12 + JUMP bb_bytecode_3 +bb_bytecode_3: + CHECK_TAG R3, tnumber, exit(4) + %17 = LOAD_DOUBLE R3 + %18 = ADD_NUM %17, 1 + STORE_DOUBLE R4, %18 + STORE_TAG R4, tnumber + INTERRUPT 5u + RETURN R4, 1i +)" + ); +} + +TEST_CASE("NewStyleConditional") +{ + ScopedFastFlag luauCodegenLinearAndOr{FFlag::LuauCodegenLinearAndOr, true}; + ScopedFastFlag luauCodegenHydrateLoadWithTag{FFlag::LuauCodegenHydrateLoadWithTag, true}; + ScopedFastFlag luauCodegenSetBlockEntryState{FFlag::LuauCodegenSetBlockEntryState, true}; + + // TODO: opportunity - this can be done in one SELECT_IF_TRUTHY, but this is also hard to detect in current system + CHECK_EQ( + "\n" + getCodegenAssembly(R"( +local function foo(a: boolean, b: number, c: number) + local x = if a then b else c + return x + 1 +end +)"), + R"( +; function foo($arg0, $arg1, $arg2) line 2 +bb_0: + CHECK_TAG R0, tboolean, exit(entry) + CHECK_TAG R1, tnumber, exit(entry) + CHECK_TAG R2, tnumber, exit(entry) + JUMP bb_4 +bb_4: + JUMP bb_bytecode_1 +bb_bytecode_1: + JUMP_IF_FALSY R0, bb_bytecode_2, bb_5 +bb_5: + %9 = LOAD_TVALUE R1, 0i, tnumber + STORE_TVALUE R3, %9 + JUMP bb_bytecode_3 +bb_bytecode_2: + %12 = LOAD_TVALUE R2, 0i, tnumber + STORE_TVALUE R3, %12 + JUMP bb_bytecode_3 +bb_bytecode_3: + CHECK_TAG R3, tnumber, exit(4) + %17 = LOAD_DOUBLE R3 + %18 = ADD_NUM %17, 1 + STORE_DOUBLE R4, %18 + STORE_TAG R4, tnumber + INTERRUPT 5u + RETURN R4, 1i +)" + ); +} + +TEST_CASE("RecursiveRemoval1") +{ + ScopedFastFlag luauCodegenBetterSccRemoval{FFlag::LuauCodegenBetterSccRemoval, true}; + + // Check that this compiles with no assertions + CHECK( + getCodegenAssembly(R"( +repeat +local _ = # {} < # {} < _ < _ < _ ^ _ ^ "" +until "" +)") + .size() > 0 + ); +} + +TEST_CASE("RecursiveRemoval2") +{ + ScopedFastFlag luauCodegenBetterSccRemoval{FFlag::LuauCodegenBetterSccRemoval, true}; + + // Check that this compiles with no assertions + CHECK( + getCodegenAssembly(R"( +local _ + +for l0={[1]=(_),},_ do + _ += _ + for l0={[1]=(_),},_ do + break + end +end +)") + .size() > 0 + ); +} + +TEST_CASE("RecursiveRemoval3") +{ + ScopedFastFlag luauCodegenBetterSccRemoval{FFlag::LuauCodegenBetterSccRemoval, true}; + + // Check that this compiles with no assertions + CHECK( + getCodegenAssembly(R"( +while {_,} do + local _ + repeat + _ = "x",_ or {} + until "x" +end +return l0 +)") + .size() > 0 + ); +} + +TEST_CASE("RecursiveRemoval4") +{ + ScopedFastFlag luauCodegenBetterSccRemoval{FFlag::LuauCodegenBetterSccRemoval, true}; + + // Check that this compiles with no assertions + CHECK( + getCodegenAssembly(R"( +local _ = 5633,5633 +while "" do + _ += bit32.replace(_,function() end,0) + for l0=_,{_,} do + do + _ += bit32.replace(_,nil,0) + local _ + end + end +end +)") + .size() > 0 + ); +} + +TEST_CASE("FuzzTest1") +{ + // Check that this compiles with no assertions + CHECK( + getCodegenAssembly(R"( +local _ = 5633,5633 +while _ do + _ ^= _ + for l0=_,_,{[# {}]=_,} do + repeat + until _ + end +end +)") + .size() > 0 + ); +} + +TEST_CASE("FuzzTest2") +{ + // Check that this compiles with no assertions + CHECK( + getCodegenAssembly(R"( +local _ +while true ~= _ do + _ = nil +end +_ = _,{},16711935 ~= _,{["" ~= _]=16711935,},_ ~= _ +while {} ~= _ do + _ = nil +end +)") + .size() > 0 + ); +} + +TEST_CASE("FuzzTest3") +{ + ScopedFastFlag luauCodegenBetterSccRemoval{FFlag::LuauCodegenBetterSccRemoval, true}; + + // Check that this compiles with no assertions + CHECK( + getCodegenAssembly(R"( +local function f(...) + local _ = ``,_ + _ ..= _(_()(_(_ and (...),_ .. _),_(_)),_(_(_(_(_ .. _,- _),_(_)),_()(_)),_(_,_._))) + _(_) +end +)") + .size() > 0 + ); +} + +TEST_CASE("FuzzTest4") +{ + // Check that this compiles with no assertions + CHECK( + getCodegenAssembly(R"( +local _ = math.exp,_(),_ +local _ = math._,_(_(_),_(_ and _),_(_(_),_,_,_()),`{nil}`),_ +for l41=_,_ do +end +l0 -= _(0,_(# _,_(_),_(_(_),_(_),_,_()),`{nil}`)) +)") + .size() > 0 + ); +} + +TEST_CASE("FuzzTest5") +{ + // Check that this compiles with no assertions + CHECK( + getCodegenAssembly(R"( +local _ = 32768 +while "" do + n0 ..= 0 + for l0=`{-2013233152}`,65535 do + local l0 = vector.create(`{-2013233152}`,-2147450880,_,_) + _ = _,_ // 0,_ // _ + end +end +)") + .size() > 0 + ); +} + +TEST_CASE("FuzzTest6") +{ + // Check that this compiles with no assertions + CHECK( + getCodegenAssembly(R"( +local l0:any = _(393216),# 0,n0 +while vector.sign(_ and true) do +_ ..= nil +do end +for l0=_,_,vector.sign(_ + - _) do +for l0=_,_,vector.sign() do +do end +end +end +end +)") + .size() > 0 + ); +} + +TEST_CASE("FuzzTest7") +{ + ScopedFastFlag luauCodegenLoadFloatSubstituteLast{FFlag::LuauCodegenLoadFloatSubstituteLast, true}; + + // Check that this compiles with no assertions + CHECK( + getCodegenAssembly(R"( +for l0=0,32768 do end + +local _ = vector.create(14941,14941,14848) + +local function l0() end + +_,l0,_,_,_G,_ = vector.clamp(_,_ / _,_),- - _ +)") + .size() > 0 + ); +} + +TEST_CASE("FuzzTest8") +{ + // Check that this compiles with no assertions + CHECK( + getCodegenAssembly(R"( +local function f(...) + local _ = bit32.arshift + local _ = (_),_(_(# # true,0),30),_(l158(true,_),0),_(_(l9,8258560),_)(_),_ + _,_ +end +)") + .size() > 0 + ); +} + +TEST_CASE("UpvalueAccessLoadStore1") +{ + ScopedFastFlag luauCodegenBlockSafeEnv{FFlag::LuauCodegenBlockSafeEnv, true}; + ScopedFastFlag luauCodegenUpvalueLoadProp{FFlag::LuauCodegenUpvalueLoadProp, true}; + ScopedFastFlag luauCodegenGcoDse{FFlag::LuauCodegenGcoDse, true}; + + CHECK_EQ( + "\n" + getCodegenAssembly(R"( +local m = 1 + +local function foo(a: number, b: number) + return m * a + m * b +end + +function setm(x) m = x end +)"), + R"( +; function foo($arg0, $arg1) line 4 +bb_0: + CHECK_TAG R0, tnumber, exit(entry) + CHECK_TAG R1, tnumber, exit(entry) + JUMP bb_2 +bb_2: + JUMP bb_bytecode_1 +bb_bytecode_1: + %6 = GET_UPVALUE U0 + STORE_TVALUE R4, %6 + CHECK_TAG R4, tnumber, exit(1) + %12 = LOAD_DOUBLE R4 + %14 = MUL_NUM %12, R0 + %25 = MUL_NUM %12, R1 + %34 = ADD_NUM %14, %25 + STORE_DOUBLE R2, %34 + STORE_TAG R2, tnumber + INTERRUPT 5u + RETURN R2, 1i +; function setm($arg0) line 8 +bb_bytecode_0: + %0 = LOAD_TVALUE R0 + SET_UPVALUE U0, %0, undef + INTERRUPT 1u + RETURN R0, 0i +)" + ); +} + +TEST_CASE("UpvalueAccessLoadStore2") +{ + ScopedFastFlag luauCodegenBlockSafeEnv{FFlag::LuauCodegenBlockSafeEnv, true}; + ScopedFastFlag luauCodegenHydrateLoadWithTag{FFlag::LuauCodegenHydrateLoadWithTag, true}; + ScopedFastFlag luauCodegenUpvalueLoadProp{FFlag::LuauCodegenUpvalueLoadProp, true}; + + // TODO: opportunity - if the value was just stored to VM register in parts, we can use those parts to store upvalue + CHECK_EQ( + "\n" + getCodegenAssembly(R"( +local m + +local function foo(a: number, b: number) + m = a - b + m = m * a + m * b + return m + a +end +)"), + R"( +; function foo($arg0, $arg1) line 4 +bb_0: + CHECK_TAG R0, tnumber, exit(entry) + CHECK_TAG R1, tnumber, exit(entry) + JUMP bb_2 +bb_2: + JUMP bb_bytecode_1 +bb_bytecode_1: + %10 = LOAD_DOUBLE R0 + %11 = LOAD_DOUBLE R1 + %12 = SUB_NUM %10, %11 + STORE_DOUBLE R2, %12 + STORE_TAG R2, tnumber + %15 = LOAD_TVALUE R2, 0i, tnumber + SET_UPVALUE U0, %15, tnumber + %25 = MUL_NUM %12, %10 + %36 = MUL_NUM %12, %11 + %45 = ADD_NUM %25, %36 + STORE_DOUBLE R2, %45 + %48 = LOAD_TVALUE R2, 0i, tnumber + SET_UPVALUE U0, %48, tnumber + %58 = ADD_NUM %45, %10 + STORE_DOUBLE R2, %58 + INTERRUPT 10u + RETURN R2, 1i +)" + ); +} + +TEST_CASE("UpvalueAccessLoadStore3") +{ + ScopedFastFlag luauCodegenBlockSafeEnv{FFlag::LuauCodegenBlockSafeEnv, true}; + ScopedFastFlag luauCodegenUpvalueLoadProp{FFlag::LuauCodegenUpvalueLoadProp, true}; + + CHECK_EQ( + "\n" + getCodegenAssembly(R"( +local m = 1 + +local function foo() + local a = m + m = a + local b = m + m = b + return m + a + b +end + +function setm(x, y) m = x end +)"), + R"( +; function foo() line 4 +bb_bytecode_0: + %0 = GET_UPVALUE U0 + STORE_TVALUE R0, %0 + SET_UPVALUE U0, %0, undef + STORE_TVALUE R1, %0 + SET_UPVALUE U0, %0, undef + STORE_TVALUE R4, %0 + CHECK_TAG R4, tnumber, exit(5) + %14 = LOAD_DOUBLE R4 + %16 = ADD_NUM %14, %14 + %25 = ADD_NUM %16, %14 + STORE_DOUBLE R2, %25 + STORE_TAG R2, tnumber + INTERRUPT 7u + RETURN R2, 1i +; function setm($arg0, $arg1) line 12 +bb_bytecode_0: + %0 = LOAD_TVALUE R0 + SET_UPVALUE U0, %0, undef + INTERRUPT 1u + RETURN R0, 0i +)" + ); +} + +TEST_CASE("UpvalueAccessLoadStore4") +{ + ScopedFastFlag luauCodegenBlockSafeEnv{FFlag::LuauCodegenBlockSafeEnv, true}; + ScopedFastFlag luauCodegenUpvalueLoadProp{FFlag::LuauCodegenUpvalueLoadProp, true}; + ScopedFastFlag luauCodegenLoopStepDetectFix{FFlag::LuauCodegenLoopStepDetectFix, true}; + ScopedFastFlag luauCodegenSetBlockEntryState{FFlag::LuauCodegenSetBlockEntryState, true}; + ScopedFastFlag luauCodegenBufferLoadProp{FFlag::LuauCodegenBufferLoadProp2, true}; + + CHECK_EQ( + "\n" + getCodegenAssembly(R"( +local arr: {number} + +local function foo(a: number) + for i = 1,#arr do + arr[i] = arr[i] + arr[i] * a + end +end + +arr = {1, 2, 3, 4} +)"), + R"( +; function foo($arg0) line 4 +bb_0: + CHECK_TAG R0, tnumber, exit(entry) + JUMP bb_4 +bb_4: + JUMP bb_bytecode_1 +bb_bytecode_1: + STORE_DOUBLE R3, 1 + STORE_TAG R3, tnumber + %6 = GET_UPVALUE U0 + STORE_TVALUE R4, %6 + CHECK_TAG R4, ttable, exit(2) + %10 = LOAD_POINTER R4 + CHECK_NO_METATABLE %10, bb_fallback_5 + %12 = TABLE_LEN %10 + %13 = INT_TO_NUM %12 + STORE_DOUBLE R1, %13 + STORE_TAG R1, tnumber + JUMP bb_6 +bb_6: + STORE_DOUBLE R2, 1 + STORE_TAG R2, tnumber + CHECK_TAG R1, tnumber, exit(4) + CHECK_TAG R3, tnumber, exit(4) + %26 = LOAD_DOUBLE R1 + JUMP_CMP_NUM R3, %26, not_le, bb_bytecode_3, bb_bytecode_2 +bb_bytecode_2: + INTERRUPT 5u + %30 = GET_UPVALUE U0 + STORE_TVALUE R4, %30 + STORE_TVALUE R7, %30 + CHECK_TAG R7, ttable, exit(7) + CHECK_TAG R3, tnumber, exit(7) + %38 = LOAD_POINTER R7 + %39 = LOAD_DOUBLE R3 + %40 = TRY_NUM_TO_INDEX %39, bb_fallback_7 + %41 = SUB_INT %40, 1i + CHECK_ARRAY_SIZE %38, %41, bb_fallback_7 + CHECK_NO_METATABLE %38, bb_fallback_7 + %44 = GET_ARR_ADDR %38, %41 + %45 = LOAD_TVALUE %44 + STORE_TVALUE R6, %45 + JUMP bb_linear_17 +bb_linear_17: + STORE_TVALUE R9, %30 + %135 = LOAD_TVALUE %44 + STORE_TVALUE R8, %135 + CHECK_TAG R8, tnumber, bb_fallback_11 + %140 = LOAD_DOUBLE R8 + %142 = MUL_NUM %140, R0 + STORE_DOUBLE R7, %142 + STORE_TAG R7, tnumber + CHECK_TAG R6, tnumber, bb_fallback_13 + %150 = LOAD_DOUBLE R6 + %152 = ADD_NUM %150, %142 + STORE_DOUBLE R5, %152 + STORE_TAG R5, tnumber + CHECK_NO_METATABLE %38, bb_fallback_15 + CHECK_READONLY %38, bb_fallback_15 + STORE_SPLIT_TVALUE %44, tnumber, %152 + %172 = LOAD_DOUBLE R1 + %174 = ADD_NUM %39, 1 + STORE_DOUBLE R3, %174 + JUMP_CMP_NUM %174, %172, le, bb_bytecode_2, bb_bytecode_3 +bb_8: + %51 = GET_UPVALUE U0 + STORE_TVALUE R9, %51 + CHECK_TAG R9, ttable, exit(9) + CHECK_TAG R3, tnumber, exit(9) + %57 = LOAD_POINTER R9 + %58 = LOAD_DOUBLE R3 + %59 = TRY_NUM_TO_INDEX %58, bb_fallback_9 + %60 = SUB_INT %59, 1i + CHECK_ARRAY_SIZE %57, %60, bb_fallback_9 + CHECK_NO_METATABLE %57, bb_fallback_9 + %63 = GET_ARR_ADDR %57, %60 + %64 = LOAD_TVALUE %63 + STORE_TVALUE R8, %64 + JUMP bb_10 +bb_10: + CHECK_TAG R8, tnumber, bb_fallback_11 + %74 = LOAD_DOUBLE R8 + %76 = MUL_NUM %74, R0 + STORE_DOUBLE R7, %76 + STORE_TAG R7, tnumber + JUMP bb_12 +bb_12: + CHECK_TAG R6, tnumber, bb_fallback_13 + CHECK_TAG R7, tnumber, bb_fallback_13 + %87 = LOAD_DOUBLE R6 + %89 = ADD_NUM %87, R7 + STORE_DOUBLE R5, %89 + STORE_TAG R5, tnumber + JUMP bb_14 +bb_14: + CHECK_TAG R4, ttable, exit(12) + CHECK_TAG R3, tnumber, exit(12) + %100 = LOAD_POINTER R4 + %101 = LOAD_DOUBLE R3 + %102 = TRY_NUM_TO_INDEX %101, bb_fallback_15 + %103 = SUB_INT %102, 1i + CHECK_ARRAY_SIZE %100, %103, bb_fallback_15 + CHECK_NO_METATABLE %100, bb_fallback_15 + CHECK_READONLY %100, bb_fallback_15 + %107 = GET_ARR_ADDR %100, %103 + %108 = LOAD_TVALUE R5 + STORE_TVALUE %107, %108 + BARRIER_TABLE_FORWARD %100, R5, undef + JUMP bb_16 +bb_16: + %115 = LOAD_DOUBLE R1 + %116 = LOAD_DOUBLE R3 + %117 = ADD_NUM %116, 1 + STORE_DOUBLE R3, %117 + JUMP_CMP_NUM %117, %115, le, bb_bytecode_2, bb_bytecode_3 +bb_bytecode_3: + INTERRUPT 14u + RETURN R0, 0i +)" + ); +} + +TEST_CASE("BufferLoadStoreProp1") +{ + ScopedFastFlag luauCodegenBlockSafeEnv{FFlag::LuauCodegenBlockSafeEnv, true}; + ScopedFastFlag luauCodegenBufferLoadProp{FFlag::LuauCodegenBufferLoadProp2, true}; + ScopedFastFlag luauCodegenSplitFloat{FFlag::LuauCodegenSplitFloat, true}; + + CHECK_EQ( + "\n" + getCodegenAssembly(R"( +local function test(b: buffer) + return buffer.readf32(b, 0) * buffer.readf32(b, 0) + buffer.readf32(b, 4) * buffer.readf32(b, 4) +end +)"), + R"( +; function test($arg0) line 2 +bb_0: + CHECK_TAG R0, tbuffer, exit(entry) + JUMP bb_2 +bb_2: + JUMP bb_bytecode_1 +bb_bytecode_1: + implicit CHECK_SAFE_ENV exit(0) + %7 = LOAD_POINTER R0 + CHECK_BUFFER_LEN %7, 4i, 4i, exit(2) + %10 = BUFFER_READF32 %7, 0i + %11 = FLOAT_TO_NUM %10 + %30 = MUL_NUM %11, %11 + %39 = BUFFER_READF32 %7, 4i + %40 = FLOAT_TO_NUM %39 + %59 = MUL_NUM %40, %40 + %68 = ADD_NUM %30, %59 + STORE_DOUBLE R1, %68 + STORE_TAG R1, tnumber + INTERRUPT 31u + RETURN R1, 1i +)" + ); +} + +TEST_CASE("BufferLoadStoreProp2") +{ + ScopedFastFlag luauCodegenBlockSafeEnv{FFlag::LuauCodegenBlockSafeEnv, true}; + ScopedFastFlag luauCodegenChainLink{FFlag::LuauCodegenChainLink, true}; + ScopedFastFlag luauCodegenBufferLoadProp{FFlag::LuauCodegenBufferLoadProp2, true}; + + CHECK_EQ( + "\n" + getCodegenAssembly(R"( +local function test(b: buffer) + buffer.writei8(b, 10, 32) + assert(buffer.readi8(b, 10) == 32) + + buffer.writei8(b, 14, 4) + buffer.writei8(b, 13, 3) + buffer.writei8(b, 12, 2) + buffer.writei8(b, 11, 1) + + return buffer.readi8(b, 11) + buffer.readi8(b, 12) + buffer.readi8(b, 14) + buffer.readi8(b, 13) +end +)"), + R"( +; function test($arg0) line 2 +bb_0: + CHECK_TAG R0, tbuffer, exit(entry) + JUMP bb_4 +bb_4: + JUMP bb_bytecode_1 +bb_bytecode_1: + implicit CHECK_SAFE_ENV exit(0) + STORE_DOUBLE R3, 10 + STORE_TAG R3, tnumber + STORE_DOUBLE R4, 32 + STORE_TAG R4, tnumber + %15 = LOAD_POINTER R0 + CHECK_BUFFER_LEN %15, 14i, 1i, exit(4) + BUFFER_WRITEI8 %15, 10i, 32i + JUMP bb_bytecode_3 +bb_bytecode_3: + JUMP bb_8 +bb_8: + BUFFER_WRITEI8 %15, 14i, 4i + BUFFER_WRITEI8 %15, 13i, 3i + BUFFER_WRITEI8 %15, 12i, 2i + BUFFER_WRITEI8 %15, 11i, 1i + STORE_DOUBLE R1, 10 + STORE_TAG R1, tnumber + INTERRUPT 86u + RETURN R1, 1i +)" + ); +} + +// When dealing with constants and buffer loads/store of the same size, all assertions disappear as conditions are true +TEST_CASE("BufferLoadStoreProp3") +{ + ScopedFastFlag luauCodegenBlockSafeEnv{FFlag::LuauCodegenBlockSafeEnv, true}; + ScopedFastFlag luauCodegenChainLink{FFlag::LuauCodegenChainLink, true}; + ScopedFastFlag luauCodegenNumToUintFoldRange{FFlag::LuauCodegenNumToUintFoldRange, true}; + ScopedFastFlag luauCodegenBufferLoadProp{FFlag::LuauCodegenBufferLoadProp2, true}; + + CHECK_EQ( + "\n" + getCodegenAssembly(R"( +local function storeloadpreserve(b: buffer) + buffer.writeu32(b, 0, 0xffffffff) + assert(buffer.readi32(b, 0) == -1) + assert(buffer.readu32(b, 0) == 4294967295) + + buffer.writei32(b, 0, -1) + assert(buffer.readi32(b, 0) == -1) + assert(buffer.readu32(b, 0) == 4294967295) + + buffer.writei16(b, 0, 65535) + assert(buffer.readi16(b, 0) == -1) + assert(buffer.readu16(b, 0) == 65535) + + buffer.writeu16(b, 0, 65535) + assert(buffer.readi16(b, 0) == -1) + assert(buffer.readu16(b, 0) == 65535) + + buffer.writeu8(b, 0, 0xffffffff) + assert(buffer.readi8(b, 0) == -1) + assert(buffer.readu8(b, 0) == 255) + + buffer.writeu16(b, 0, 0xffffffff) + assert(buffer.readi16(b, 0) == -1) + assert(buffer.readu16(b, 0) == 65535) +end +)"), + R"( +; function storeloadpreserve($arg0) line 2 +bb_0: + CHECK_TAG R0, tbuffer, exit(entry) + JUMP bb_26 +bb_26: + JUMP bb_bytecode_1 +bb_bytecode_1: + implicit CHECK_SAFE_ENV exit(0) + STORE_DOUBLE R3, 0 + STORE_TAG R3, tnumber + STORE_DOUBLE R4, 4294967295 + STORE_TAG R4, tnumber + %15 = LOAD_POINTER R0 + CHECK_BUFFER_LEN %15, 0i, 4i, exit(4) + BUFFER_WRITEI32 %15, 0i, -1i + STORE_TAG R2, tboolean + STORE_INT R2, 1i + JUMP bb_bytecode_3 +bb_bytecode_3: + JUMP bb_30 +bb_30: + JUMP bb_bytecode_5 +bb_bytecode_5: + JUMP bb_33 +bb_33: + BUFFER_WRITEI32 %15, 0i, -1i + JUMP bb_bytecode_7 +bb_bytecode_7: + JUMP bb_37 +bb_37: + JUMP bb_bytecode_9 +bb_bytecode_9: + JUMP bb_40 +bb_40: + STORE_DOUBLE R3, 0 + STORE_DOUBLE R4, 65535 + CHECK_BUFFER_LEN %15, 0i, 2i, exit(80) + BUFFER_WRITEI16 %15, 0i, 65535i + JUMP bb_bytecode_11 +bb_bytecode_11: + JUMP bb_44 +bb_44: + JUMP bb_bytecode_13 +bb_bytecode_13: + JUMP bb_47 +bb_47: + BUFFER_WRITEI16 %15, 0i, 65535i + JUMP bb_bytecode_15 +bb_bytecode_15: + JUMP bb_51 +bb_51: + JUMP bb_bytecode_17 +bb_bytecode_17: + JUMP bb_54 +bb_54: + STORE_DOUBLE R3, 0 + STORE_DOUBLE R4, 4294967295 + CHECK_BUFFER_LEN %15, 0i, 1i, exit(156) + BUFFER_WRITEI8 %15, 0i, -1i + JUMP bb_bytecode_19 +bb_bytecode_19: + JUMP bb_58 +bb_58: + JUMP bb_bytecode_21 +bb_bytecode_21: + JUMP bb_61 +bb_61: + BUFFER_WRITEI16 %15, 0i, -1i + JUMP bb_bytecode_23 +bb_bytecode_23: + JUMP bb_65 +bb_65: + JUMP bb_bytecode_25 +bb_bytecode_25: + JUMP bb_68 +bb_68: + INTERRUPT 228u + RETURN R0, 0i +)" + ); +} + +// When dealing with unknown numbers, stores can be propagated to loads with proper zero/signed extension +TEST_CASE("BufferLoadStoreProp4") +{ + ScopedFastFlag luauCodegenBlockSafeEnv{FFlag::LuauCodegenBlockSafeEnv, true}; + ScopedFastFlag luauCodegenNumIntFolds{FFlag::LuauCodegenNumIntFolds2, true}; + ScopedFastFlag luauCodegenIntegerAddSub{FFlag::LuauCodegenIntegerAddSub, true}; + ScopedFastFlag luauCodegenBufferLoadProp{FFlag::LuauCodegenBufferLoadProp2, true}; + ScopedFastFlag luauCodegenSplitFloat{FFlag::LuauCodegenSplitFloat, true}; + + // TODO: opportunity - CHECK_BUFFER_LEN with different access sizes can be merged + CHECK_EQ( + "\n" + getCodegenAssembly(R"( +local function test(b: buffer, n: number, f: number) + buffer.writei8(b, 0, n) + buffer.writef64(b, 100, buffer.readi8(b, 0)) + buffer.writei8(b, 108, buffer.readi8(b, 0)) + buffer.writeu8(b, 109, buffer.readi8(b, 0)) + + buffer.writeu8(b, 2, n) + buffer.writef64(b, 116, buffer.readu8(b, 2)) + buffer.writeu8(b, 124, buffer.readu8(b, 2)) + buffer.writei8(b, 125, buffer.readu8(b, 2)) + + buffer.writei16(b, 4, n) + buffer.writef64(b, 132, buffer.readi16(b, 4)) + buffer.writei16(b, 140, buffer.readi16(b, 4)) + buffer.writeu16(b, 142, buffer.readi16(b, 4)) + + buffer.writeu16(b, 8, n) + buffer.writef64(b, 148, buffer.readu16(b, 8)) + buffer.writeu16(b, 156, buffer.readu16(b, 8)) + buffer.writei16(b, 158, buffer.readu16(b, 8)) + + buffer.writei32(b, 12, n) + buffer.writef64(b, 164, buffer.readi32(b, 12)) + buffer.writei32(b, 172, buffer.readi32(b, 12)) + buffer.writeu32(b, 176, buffer.readi32(b, 12)) + + buffer.writeu32(b, 20, n) + buffer.writef64(b, 180, buffer.readu32(b, 20)) + buffer.writeu32(b, 188, buffer.readu32(b, 20)) + buffer.writei32(b, 192, buffer.readu32(b, 20)) + + buffer.writef32(b, 28, f) + buffer.writef64(b, 196, buffer.readf32(b, 28)) + buffer.writef32(b, 196, buffer.readf32(b, 28)) + + buffer.writef64(b, 32, f) + buffer.writef64(b, 204, buffer.readf64(b, 32)) + buffer.writef32(b, 204, buffer.readf64(b, 32)) +end +)"), + R"( +; function test($arg0, $arg1, $arg2) line 2 +bb_0: + CHECK_TAG R0, tbuffer, exit(entry) + CHECK_TAG R1, tnumber, exit(entry) + CHECK_TAG R2, tnumber, exit(entry) + JUMP bb_2 +bb_2: + JUMP bb_bytecode_1 +bb_bytecode_1: + implicit CHECK_SAFE_ENV exit(0) + STORE_DOUBLE R5, 0 + STORE_TAG R5, tnumber + %17 = LOAD_POINTER R0 + CHECK_BUFFER_LEN %17, 125i, 1i, exit(3) + %21 = LOAD_DOUBLE R1 + %22 = NUM_TO_UINT %21 + BUFFER_WRITEI8 %17, 0i, %22 + STORE_DOUBLE R5, 100 + %32 = SEXTI8_INT %22 + %33 = INT_TO_NUM %32 + STORE_DOUBLE R6, %33 + STORE_TAG R6, tnumber + CHECK_BUFFER_LEN %17, 204i, 8i, exit(18) + BUFFER_WRITEF64 %17, 100i, %33 + BUFFER_WRITEI8 %17, 108i, %22 + BUFFER_WRITEI8 %17, 109i, %22 + BUFFER_WRITEI8 %17, 2i, %22 + %125 = BITAND_UINT %22, 255i + %126 = INT_TO_NUM %125 + BUFFER_WRITEF64 %17, 116i, %126 + BUFFER_WRITEI8 %17, 124i, %22 + STORE_DOUBLE R6, %126 + BUFFER_WRITEI8 %17, 125i, %22 + STORE_DOUBLE R5, 4 + CHECK_BUFFER_LEN %17, 158i, 2i, exit(103) + BUFFER_WRITEI16 %17, 4i, %22 + %218 = SEXTI16_INT %22 + %219 = INT_TO_NUM %218 + BUFFER_WRITEF64 %17, 132i, %219 + BUFFER_WRITEI16 %17, 140i, %22 + BUFFER_WRITEI16 %17, 142i, %22 + BUFFER_WRITEI16 %17, 8i, %22 + %311 = BITAND_UINT %22, 65535i + %312 = INT_TO_NUM %311 + BUFFER_WRITEF64 %17, 148i, %312 + BUFFER_WRITEI16 %17, 156i, %22 + STORE_DOUBLE R6, %312 + BUFFER_WRITEI16 %17, 158i, %22 + STORE_DOUBLE R5, 12 + CHECK_BUFFER_LEN %17, 204i, 4i, exit(203) + BUFFER_WRITEI32 %17, 12i, %22 + %404 = TRUNCATE_UINT %22 + %405 = INT_TO_NUM %404 + BUFFER_WRITEF64 %17, 164i, %405 + BUFFER_WRITEI32 %17, 172i, %22 + BUFFER_WRITEI32 %17, 176i, %22 + BUFFER_WRITEI32 %17, 20i, %22 + %498 = UINT_TO_NUM %404 + BUFFER_WRITEF64 %17, 180i, %498 + BUFFER_WRITEI32 %17, 188i, %22 + BUFFER_WRITEI32 %17, 192i, %22 + %579 = LOAD_DOUBLE R2 + %580 = NUM_TO_FLOAT %579 + BUFFER_WRITEF32 %17, 28i, %580 + %591 = FLOAT_TO_NUM %580 + BUFFER_WRITEF64 %17, 196i, %591 + BUFFER_WRITEF32 %17, 196i, %580 + BUFFER_WRITEF64 %17, 32i, %579 + BUFFER_WRITEF64 %17, 204i, %579 + BUFFER_WRITEF32 %17, 204i, %580 + INTERRUPT 372u + RETURN R0, 0i +)" + ); +} + +TEST_CASE("LoopStepDetection1") +{ + ScopedFastFlag luauCodegenLoopStepDetectFix{FFlag::LuauCodegenLoopStepDetectFix, true}; + ScopedFastFlag luauCodegenHydrateLoadWithTag{FFlag::LuauCodegenHydrateLoadWithTag, true}; + + CHECK_EQ( + "\n" + getCodegenAssembly( + R"( +local function foo(n: number) + local s = 0 + for i = 1,n do + s += i + end + return s +end +)" + ), + R"( +; function foo($arg0) line 2 +bb_0: + CHECK_TAG R0, tnumber, exit(entry) + JUMP bb_4 +bb_4: + JUMP bb_bytecode_1 +bb_bytecode_1: + STORE_DOUBLE R1, 0 + STORE_TAG R1, tnumber + STORE_DOUBLE R4, 1 + STORE_TAG R4, tnumber + %8 = LOAD_TVALUE R0, 0i, tnumber + STORE_TVALUE R2, %8 + STORE_DOUBLE R3, 1 + STORE_TAG R3, tnumber + %16 = LOAD_DOUBLE R2 + JUMP_CMP_NUM 1, %16, not_le, bb_bytecode_3, bb_bytecode_2 +bb_bytecode_2: + INTERRUPT 5u + CHECK_TAG R1, tnumber, exit(5) + CHECK_TAG R4, tnumber, exit(5) + %24 = LOAD_DOUBLE R1 + %25 = LOAD_DOUBLE R4 + %26 = ADD_NUM %24, %25 + STORE_DOUBLE R1, %26 + %28 = LOAD_DOUBLE R2 + %30 = ADD_NUM %25, 1 + STORE_DOUBLE R4, %30 + JUMP_CMP_NUM %30, %28, le, bb_bytecode_2, bb_bytecode_3 +bb_bytecode_3: + INTERRUPT 7u + RETURN R1, 1i +)" + ); +} + +TEST_CASE("LoopStepDetection2") +{ + ScopedFastFlag luauCodegenLoopStepDetectFix{FFlag::LuauCodegenLoopStepDetectFix, true}; + ScopedFastFlag luauCodegenSetBlockEntryState{FFlag::LuauCodegenSetBlockEntryState, true}; + + CHECK_EQ( + "\n" + getCodegenAssembly( + R"( +local function foo(n: number, t: {number}) + local s = 0 + for i = 1,#t do + s += t[i] + end + return s +end +)" + ), + R"( +; function foo($arg0, $arg1) line 2 +bb_0: + CHECK_TAG R0, tnumber, exit(entry) + CHECK_TAG R1, ttable, exit(entry) + JUMP bb_4 +bb_4: + JUMP bb_bytecode_1 +bb_bytecode_1: + STORE_DOUBLE R2, 0 + STORE_TAG R2, tnumber + STORE_DOUBLE R5, 1 + STORE_TAG R5, tnumber + %12 = LOAD_POINTER R1 + CHECK_NO_METATABLE %12, bb_fallback_5 + %14 = TABLE_LEN %12 + %15 = INT_TO_NUM %14 + STORE_DOUBLE R3, %15 + STORE_TAG R3, tnumber + JUMP bb_6 +bb_6: + STORE_DOUBLE R4, 1 + STORE_TAG R4, tnumber + CHECK_TAG R3, tnumber, exit(4) + CHECK_TAG R5, tnumber, exit(4) + %28 = LOAD_DOUBLE R3 + JUMP_CMP_NUM R5, %28, not_le, bb_bytecode_3, bb_bytecode_2 +bb_bytecode_2: + INTERRUPT 5u + CHECK_TAG R5, tnumber, exit(5) + %36 = LOAD_POINTER R1 + %37 = LOAD_DOUBLE R5 + %38 = TRY_NUM_TO_INDEX %37, bb_fallback_7 + %39 = SUB_INT %38, 1i + CHECK_ARRAY_SIZE %36, %39, bb_fallback_7 + CHECK_NO_METATABLE %36, bb_fallback_7 + %42 = GET_ARR_ADDR %36, %39 + %43 = LOAD_TVALUE %42 + STORE_TVALUE R6, %43 + JUMP bb_8 +bb_8: + CHECK_TAG R2, tnumber, exit(6) + CHECK_TAG R6, tnumber, bb_fallback_9 + %53 = LOAD_DOUBLE R2 + %55 = ADD_NUM %53, R6 + STORE_DOUBLE R2, %55 + JUMP bb_10 +bb_10: + %61 = LOAD_DOUBLE R3 + %62 = LOAD_DOUBLE R5 + %63 = ADD_NUM %62, 1 + STORE_DOUBLE R5, %63 + JUMP_CMP_NUM %63, %61, le, bb_bytecode_2, bb_bytecode_3 +bb_bytecode_3: + INTERRUPT 8u + RETURN R2, 1i +)" + ); +} + +TEST_CASE("UintSourceSanity") +{ + ScopedFastFlag luauCodegenBlockSafeEnv{FFlag::LuauCodegenBlockSafeEnv, true}; + ScopedFastFlag luauCodegenNumIntFolds{FFlag::LuauCodegenNumIntFolds2, true}; + + // TODO: opportunity - many conversions and stores remain because of VM exits + CHECK_EQ( + "\n" + getCodegenAssembly( + R"( +local function foo(b: buffer, a: number, s: string) + local r1 = buffer.readi32(b, bit32.bor(a, 0)) + local r2 = buffer.readu32(b, r1) + local r3 = buffer.readi32(b, r2) + local r4 = buffer.readu32(b, string.len(s)) + return r1, r2, r3, r4 +end +)" + ), + R"( +; function foo($arg0, $arg1, $arg2) line 2 +bb_0: + CHECK_TAG R0, tbuffer, exit(entry) + CHECK_TAG R1, tnumber, exit(entry) + CHECK_TAG R2, tstring, exit(entry) + JUMP bb_2 +bb_2: + JUMP bb_bytecode_1 +bb_bytecode_1: + implicit CHECK_SAFE_ENV exit(0) + %11 = LOAD_DOUBLE R1 + %12 = NUM_TO_UINT %11 + %15 = UINT_TO_NUM %12 + STORE_DOUBLE R5, %15 + STORE_TAG R5, tnumber + %23 = LOAD_POINTER R0 + %25 = TRUNCATE_UINT %12 + CHECK_BUFFER_LEN %23, %25, 4i, exit(9) + %27 = BUFFER_READI32 %23, %25 + %28 = INT_TO_NUM %27 + STORE_DOUBLE R3, %28 + STORE_TAG R3, tnumber + CHECK_BUFFER_LEN %23, %27, 4i, exit(15) + %40 = BUFFER_READI32 %23, %27 + %41 = UINT_TO_NUM %40 + STORE_DOUBLE R4, %41 + STORE_TAG R4, tnumber + CHECK_BUFFER_LEN %23, %40, 4i, exit(22) + %53 = BUFFER_READI32 %23, %40 + %54 = INT_TO_NUM %53 + STORE_DOUBLE R5, %54 + %60 = LOAD_POINTER R2 + %61 = STRING_LEN %60 + %62 = INT_TO_NUM %61 + STORE_DOUBLE R8, %62 + STORE_TAG R8, tnumber + CHECK_BUFFER_LEN %23, %61, 4i, exit(34) + %74 = BUFFER_READI32 %23, %61 + %75 = UINT_TO_NUM %74 + STORE_DOUBLE R6, %75 + STORE_TAG R6, tnumber + INTERRUPT 38u + RETURN R3, 4i +)" + ); +} + TEST_SUITE_END(); diff --git a/tests/Normalize.test.cpp b/tests/Normalize.test.cpp index 514159678..60fa81d58 100644 --- a/tests/Normalize.test.cpp +++ b/tests/Normalize.test.cpp @@ -15,7 +15,6 @@ LUAU_FASTFLAG(LuauSolverV2) LUAU_FASTINT(LuauTypeInferRecursionLimit) LUAU_FASTINT(LuauNormalizeIntersectionLimit) LUAU_FASTINT(LuauNormalizeUnionLimit) -LUAU_FASTFLAG(LuauNormalizerUnionTyvarsTakeMaxSize) using namespace Luau; @@ -1131,8 +1130,6 @@ TEST_CASE_FIXTURE(NormalizeFixture, "free_type_intersection_ordering") TEST_CASE_FIXTURE(NormalizeFixture, "tyvar_limit_one_sided_intersection" * doctest::timeout(0.5)) { - ScopedFastFlag _{FFlag::LuauNormalizerUnionTyvarsTakeMaxSize, true}; // Affects stringification of free types. - std::vector options; for (auto i = 0; i < 120; i++) options.push_back(arena.freshType(getBuiltins(), getGlobalScope())); diff --git a/tests/OverloadResolver.test.cpp b/tests/OverloadResolver.test.cpp index f65670674..590bf1934 100644 --- a/tests/OverloadResolver.test.cpp +++ b/tests/OverloadResolver.test.cpp @@ -7,7 +7,6 @@ #include "Luau/Normalize.h" #include "Luau/UnifierSharedState.h" -LUAU_FASTFLAG(LuauConsiderErrorSuppressionInTypes) LUAU_FASTFLAG(LuauNewOverloadResolver2) using namespace Luau; diff --git a/tests/RuntimeLimits.test.cpp b/tests/RuntimeLimits.test.cpp index 8f4ef2279..9506bcd10 100644 --- a/tests/RuntimeLimits.test.cpp +++ b/tests/RuntimeLimits.test.cpp @@ -25,11 +25,9 @@ LUAU_FASTINT(LuauTypeInferRecursionLimit) LUAU_FASTFLAG(LuauSolverV2) LUAU_FASTFLAG(LuauIceLess) LUAU_FASTFLAG(LuauDontDynamicallyCreateRedundantSubtypeConstraints) -LUAU_FASTFLAG(LuauLimitUnification) LUAU_FASTFLAG(LuauUseNativeStackGuard) LUAU_FASTINT(LuauGenericCounterMaxSteps) LUAU_FASTFLAG(LuauGenericCounterStepsInsteadOfCount) -LUAU_FASTFLAG(LuauNormalizerStepwiseFuel) struct LimitFixture : BuiltinsFixture { @@ -562,11 +560,7 @@ TEST_CASE_FIXTURE(BuiltinsFixture, "test_generic_pruning_recursion_limit") TEST_CASE_FIXTURE(BuiltinsFixture, "unification_runs_a_limited_number_of_iterations_before_stopping" * doctest::timeout(4.0)) { ScopedFastFlag sff[] = { - // These are necessary to trigger the bug {FFlag::LuauSolverV2, true}, - - // This is the fix - {FFlag::LuauLimitUnification, true} }; ScopedFastInt sfi{FInt::LuauTypeInferIterationLimit, 100}; @@ -592,7 +586,6 @@ TEST_CASE_FIXTURE(BuiltinsFixture, "native_stack_guard_prevents_stack_overflows" { ScopedFastFlag sff[] = { {FFlag::LuauSolverV2, true}, - {FFlag::LuauLimitUnification, true}, {FFlag::LuauUseNativeStackGuard, true}, }; @@ -661,10 +654,8 @@ end )")); } -TEST_CASE_FIXTURE(BuiltinsFixture, "fuzzer_" * doctest::timeout(4.0)) +TEST_CASE_FIXTURE(BuiltinsFixture, "fuzzer_stepwise_normalization_works" * doctest::timeout(4.0)) { - ScopedFastFlag _{FFlag::LuauNormalizerStepwiseFuel, true}; - LUAU_REQUIRE_ERRORS(check(R"( _ = if _ then {n0=# _,[_]=_,``,[function(l0,l0,l0) do end @@ -676,8 +667,6 @@ TEST_CASE_FIXTURE(BuiltinsFixture, "fuzzer_" * doctest::timeout(4.0)) TEST_CASE_FIXTURE(BuiltinsFixture, "fuzzer_oom_unions" * doctest::timeout(4.0)) { - ScopedFastFlag _{FFlag::LuauNormalizerStepwiseFuel, true}; - LUAU_REQUIRE_ERRORS(check(R"( local _ = true,l0 _ = if _ then _ else _._,if _[_] then nil elseif _ then `` else _._,... diff --git a/tests/Simplify.test.cpp b/tests/Simplify.test.cpp index 714f99d88..721c3c4db 100644 --- a/tests/Simplify.test.cpp +++ b/tests/Simplify.test.cpp @@ -9,7 +9,6 @@ using namespace Luau; LUAU_FASTFLAG(LuauSolverV2) -LUAU_FASTFLAG(LuauSimplifyRefinementOfReadOnlyProperty) LUAU_DYNAMIC_FASTINT(LuauSimplificationComplexityLimit) LUAU_FASTFLAG(LuauSimplifyIntersectionNoTreeSet) @@ -642,8 +641,6 @@ TEST_CASE_FIXTURE(SimplifyFixture, "(error | string) & any") TEST_CASE_FIXTURE(SimplifyFixture, "{ x: number, y: number } & { x: unknown }") { - ScopedFastFlag sff{FFlag::LuauSimplifyRefinementOfReadOnlyProperty, true}; - TypeId leftTy = mkTable({{"x", builtinTypes->numberType}, {"y", builtinTypes->numberType}}); TypeId rightTy = mkTable({{"x", Property::rw(builtinTypes->unknownType)}}); @@ -652,8 +649,6 @@ TEST_CASE_FIXTURE(SimplifyFixture, "{ x: number, y: number } & { x: unknown }") TEST_CASE_FIXTURE(SimplifyFixture, "{ x: number, y: number } & { read x: unknown }") { - ScopedFastFlag sff{FFlag::LuauSimplifyRefinementOfReadOnlyProperty, true}; - TypeId leftTy = mkTable({{"x", builtinTypes->numberType}, {"y", builtinTypes->numberType}}); TypeId rightTy = mkTable({{"x", Property::readonly(builtinTypes->unknownType)}}); @@ -662,8 +657,6 @@ TEST_CASE_FIXTURE(SimplifyFixture, "{ x: number, y: number } & { read x: unknown TEST_CASE_FIXTURE(SimplifyFixture, "{ read x: Child } & { x: Parent }") { - ScopedFastFlag sff{FFlag::LuauSimplifyRefinementOfReadOnlyProperty, true}; - createSomeExternTypes(getFrontend()); TypeId parentTy = getFrontend().globals.globalScope->exportedTypeBindings["Parent"].type; diff --git a/tests/Subtyping.test.cpp b/tests/Subtyping.test.cpp index ae9d6b86e..d0837f0ea 100644 --- a/tests/Subtyping.test.cpp +++ b/tests/Subtyping.test.cpp @@ -17,7 +17,6 @@ #include LUAU_FASTFLAG(LuauSolverV2) -LUAU_FASTFLAG(LuauConsiderErrorSuppressionInTypes) using namespace Luau; @@ -1440,7 +1439,6 @@ TEST_CASE_FIXTURE(SubtypeFixture, "(number, number...) numberType, getBuiltins()->errorType}}); TypeId superTy = getBuiltins()->booleanType; SubtypingResult result = isSubtype(subTy, superTy); @@ -1464,8 +1462,6 @@ TEST_CASE_FIXTURE(SubtypeFixture, "subtyping_reasonings_check_for_error_suppress TEST_CASE_FIXTURE(SubtypeFixture, "subtyping_reasonings_check_for_error_suppression_in_intersect_type_path") { - ScopedFastFlag sff{FFlag::LuauConsiderErrorSuppressionInTypes, true}; - TypeId subTy = getBuiltins()->booleanType; TypeId superTy = arena.addType(IntersectionType{{getBuiltins()->numberType, getBuiltins()->errorType}}); SubtypingResult result = isSubtype(subTy, superTy); diff --git a/tests/ToString.test.cpp b/tests/ToString.test.cpp index 55d6afb65..fd9fa4aee 100644 --- a/tests/ToString.test.cpp +++ b/tests/ToString.test.cpp @@ -14,6 +14,7 @@ using namespace Luau; LUAU_FASTFLAG(LuauRecursiveTypeParameterRestriction) LUAU_FASTFLAG(LuauSolverV2) +LUAU_FASTFLAG(LuauBetterTypeMismatchErrors) TEST_SUITE_BEGIN("ToString"); @@ -845,13 +846,34 @@ TEST_CASE_FIXTURE(Fixture, "tostring_error_mismatch") )"); std::string expected; - if (FFlag::LuauSolverV2) + if (FFlag::LuauSolverV2 && FFlag::LuauBetterTypeMismatchErrors) + expected = "Expected this to be\n\t" + "'{ a: number, b: string, c: { d: number } }'\n" + "but got\n\t" + "'{ a: number, b: string, c: { d: string } }'; \n" + "accessing `c.d` results in `string` in the latter type and `number` in the former " + "type, and `string` is not exactly `number`"; + else if (FFlag::LuauSolverV2) expected = "Type\n\t" "'{ a: number, b: string, c: { d: string } }'\n" "could not be converted into\n\t" "'{ a: number, b: string, c: { d: number } }'; \n" "this is because accessing `c.d` results in `string` in the former type and `number` in the latter " "type, and `string` is not exactly `number`"; + else if (FFlag::LuauBetterTypeMismatchErrors) + expected = "Expected this to be exactly\n\t" + "'{ a: number, b: string, c: { d: number } }'\n" + "but got\n\t" + "'{ a: number, b: string, c: { d: string } }'\n" + "caused by:\n " + "Property 'c' is not compatible.\n" + "Expected this to be exactly\n\t" + "'{ d: number }'\n" + "but got\n\t" + "'{ d: string }'\n" + "caused by:\n " + "Property 'd' is not compatible.\n" + "Expected this to be exactly 'number', but got 'string'"; else expected = "Type\n\t" "'{ a: number, b: string, c: { d: string } }'\n" diff --git a/tests/TypeFunction.test.cpp b/tests/TypeFunction.test.cpp index 3da9005e5..418eeba5f 100644 --- a/tests/TypeFunction.test.cpp +++ b/tests/TypeFunction.test.cpp @@ -15,10 +15,10 @@ using namespace Luau; LUAU_FASTFLAG(LuauSolverV2) LUAU_DYNAMIC_FASTINT(LuauTypeFamilyApplicationCartesianProductLimit) LUAU_FASTFLAG(DebugLuauAssertOnForcedConstraint) -LUAU_FASTFLAG(LuauNoMoreComparisonTypeFunctions) LUAU_FASTFLAG(LuauBuiltinTypeFunctionsArentGlobal) LUAU_FASTFLAG(LuauGetmetatableError) -LUAU_FASTFLAG(LuauInstantiationUsesGenericPolarity) +LUAU_FASTFLAG(LuauBetterTypeMismatchErrors) +LUAU_FASTFLAG(LuauSetmetatableWaitForPendingTypes) struct TypeFunctionFixture : Fixture { @@ -123,8 +123,16 @@ TEST_CASE_FIXTURE(TypeFunctionFixture, "function_as_fn_arg") LUAU_REQUIRE_ERROR_COUNT(2, result); CHECK("unknown" == toString(requireType("a"))); CHECK("unknown" == toString(requireType("b"))); - CHECK("Type 'number' could not be converted into 'never'" == toString(result.errors[0])); - CHECK("Type 'boolean' could not be converted into 'never'" == toString(result.errors[1])); + if (FFlag::LuauBetterTypeMismatchErrors) + { + CHECK("Expected this to be unreachable, but got 'number'" == toString(result.errors[0])); + CHECK("Expected this to be unreachable, but got 'boolean'" == toString(result.errors[1])); + } + else + { + CHECK("Type 'number' could not be converted into 'never'" == toString(result.errors[0])); + CHECK("Type 'boolean' could not be converted into 'never'" == toString(result.errors[1])); + } } TEST_CASE_FIXTURE(TypeFunctionFixture, "resolve_deep_functions") @@ -152,8 +160,16 @@ TEST_CASE_FIXTURE(TypeFunctionFixture, "unsolvable_function") )"); LUAU_REQUIRE_ERROR_COUNT(2, result); - CHECK(toString(result.errors[0]) == "Type 'number' could not be converted into 'never'"); - CHECK(toString(result.errors[1]) == "Type 'boolean' could not be converted into 'never'"); + if (FFlag::LuauBetterTypeMismatchErrors) + { + CHECK("Expected this to be unreachable, but got 'number'" == toString(result.errors[0])); + CHECK("Expected this to be unreachable, but got 'boolean'" == toString(result.errors[1])); + } + else + { + CHECK(toString(result.errors[0]) == "Type 'number' could not be converted into 'never'"); + CHECK(toString(result.errors[1]) == "Type 'boolean' could not be converted into 'never'"); + } } TEST_CASE_FIXTURE(TypeFunctionFixture, "table_internal_functions") @@ -1895,19 +1911,6 @@ TEST_CASE_FIXTURE(TFFixture, "reduce_union_of_error_nil_table_with_table") CHECK_EQ("*error-type* | table", toString(refinement)); } -TEST_CASE_FIXTURE(Fixture, "generic_type_functions_should_not_get_stuck_or") -{ - ScopedFastFlag sffs[] = {{FFlag::LuauSolverV2, true}, {FFlag::LuauNoMoreComparisonTypeFunctions, false}}; - - CheckResult result = check(R"( - local function init(data) - return not data or data == '' - end - )"); - LUAU_REQUIRE_ERROR_COUNT(1, result); - CHECK(get(result.errors[0])); -} - TEST_CASE_FIXTURE(TypeFunctionFixture, "recursive_restraint_violation") { CheckResult result = check(R"( @@ -1948,4 +1951,66 @@ TEST_CASE_FIXTURE(TypeFunctionFixture, "recursive_restraint_violation3") CHECK(get(result.errors[0])); } +TEST_CASE_FIXTURE(BuiltinsFixture, "oss_2106_wait_for_pending_types_in_setmetatable_ex1") +{ + ScopedFastFlag sffs[] = { + {FFlag::LuauSolverV2, true}, + // This is the actual fix for this test + {FFlag::LuauSetmetatableWaitForPendingTypes, true}, + {FFlag::DebugLuauAssertOnForcedConstraint, true} + }; + + LUAU_REQUIRE_NO_ERRORS(check(R"( + local MyClass = {} + local MyClassMetatable = table.freeze({ __index = MyClass }) + + type MyClass = setmetatable<{ name: string }, typeof(MyClassMetatable)> + + function MyClass.new(name: string): MyClass + return setmetatable({ name = name }, MyClassMetatable) + end + + function MyClass.hello(self: MyClass): string + return `Hello, {self.name}!` + end + + local instance = MyClass.new("World") + local g = instance:hello() + )")); + + CHECK_EQ("string", toString(requireType("g"))); +} + +TEST_CASE_FIXTURE(BuiltinsFixture, "oss_2106_wait_for_pending_types_in_setmetatable_ex2") +{ + ScopedFastFlag sffs[] = { + {FFlag::LuauSolverV2, true}, + // This is the actual fix for this test + {FFlag::LuauSetmetatableWaitForPendingTypes, true}, + {FFlag::DebugLuauAssertOnForcedConstraint, true} + }; + + LUAU_REQUIRE_NO_ERRORS(check(R"( + local MyClass = {} + local MyClassMetatable = { __index = MyClass } + table.freeze(MyClassMetatable) + + type CommonFields = { read name: T } + type MyClass = setmetatable, typeof(MyClassMetatable)> + + function MyClass.new(name: string): MyClass + return setmetatable({ name = name }, MyClassMetatable) + end + + function MyClass.hello(self: MyClass): string + return `Hello, {self.name}!` + end + + local instance = MyClass.new("World") + local g = instance:hello() + )")); + + CHECK_EQ("string", toString(requireType("g"))); +} + TEST_SUITE_END(); diff --git a/tests/TypeFunction.user.test.cpp b/tests/TypeFunction.user.test.cpp index 54004a9b5..2de77c574 100644 --- a/tests/TypeFunction.user.test.cpp +++ b/tests/TypeFunction.user.test.cpp @@ -1,5 +1,7 @@ // This file is part of the Luau programming language and is licensed under MIT License; see LICENSE.txt for details +#include "Luau/Error.h" + #include "ClassFixture.h" #include "Fixture.h" @@ -10,7 +12,10 @@ using namespace Luau; LUAU_FASTFLAG(LuauSolverV2) LUAU_FASTFLAG(LuauUnknownGlobalFixSuggestion) LUAU_FASTFLAG(LuauMorePermissiveNewtableType) +LUAU_FASTFLAG(LuauBetterTypeMismatchErrors) LUAU_FASTFLAG(LuauUserTypeFunctionsNoUninhabitedError) +LUAU_FASTFLAG(LuauUnionofIntersectionofFlattens) +LUAU_FASTFLAG(LuauTypeFunctionDeserializationShouldNotCrashOnGenericPacks) TEST_SUITE_BEGIN("UserDefinedTypeFunctionTests"); @@ -405,8 +410,7 @@ TEST_CASE_FIXTURE(BuiltinsFixture, "udtf_optional_works_on_unions") TEST_CASE_FIXTURE(BuiltinsFixture, "udtf_union_methods_work") { - if (!FFlag::LuauSolverV2) - return; + ScopedFastFlag newSolver{FFlag::LuauSolverV2, true}; CheckResult result = check(R"( type function getunion() @@ -432,6 +436,124 @@ TEST_CASE_FIXTURE(BuiltinsFixture, "udtf_union_methods_work") CHECK(toString(tm->givenType) == "boolean | number | string"); } +TEST_CASE_FIXTURE(BuiltinsFixture, "udtf_flatten_on_unionof") +{ + ScopedFastFlag newSolver{FFlag::LuauSolverV2, true}; + ScopedFastFlag sff{FFlag::LuauUnionofIntersectionofFlattens, true}; + + CheckResult result = check(R"( + type function foobar() + local tys = { types.string, types.number, types.never, types.boolean, types.singleton(nil) } + local result = types.never + for _, ty in tys do + result = types.unionof(result, ty) + end + return result + end + -- forcing an error here to check the exact type of the union + local function ok(idx: foobar<>): never return idx end + )"); + + LUAU_REQUIRE_ERROR_COUNT(1, result); + TypeMismatch* tm = get(result.errors[0]); + REQUIRE(tm); + CHECK(toString(tm->givenType) == "(boolean | number | string)?"); +} + +TEST_CASE_FIXTURE(BuiltinsFixture, "udtf_flatten_on_unionof_empty") +{ + ScopedFastFlag newSolver{FFlag::LuauSolverV2, true}; + ScopedFastFlag sff{FFlag::LuauUnionofIntersectionofFlattens, true}; + + CheckResult result = check(R"( + type function foobar() + return types.unionof() + end + + local f: foobar<> + )"); + + LUAU_REQUIRE_NO_ERRORS(result); + CHECK(toString(requireType("f")) == "never"); +} + +TEST_CASE_FIXTURE(BuiltinsFixture, "udtf_flatten_on_unionof_two_things") +{ + ScopedFastFlag newSolver{FFlag::LuauSolverV2, true}; + ScopedFastFlag sff{FFlag::LuauUnionofIntersectionofFlattens, true}; + + CheckResult result = check(R"( + type function foobar() + return types.unionof(types.string, types.never) + end + -- forcing an error here to check the exact type of the union + local function ok(idx: foobar<>): never return idx end + )"); + + LUAU_REQUIRE_ERROR_COUNT(1, result); + TypeMismatch* tm = get(result.errors[0]); + REQUIRE(tm); + CHECK(toString(tm->givenType) == "string"); +} + +TEST_CASE_FIXTURE(BuiltinsFixture, "udtf_flatten_on_intersectionof") +{ + ScopedFastFlag newSolver{FFlag::LuauSolverV2, true}; + ScopedFastFlag sff{FFlag::LuauUnionofIntersectionofFlattens, true}; + + CheckResult result = check(R"( + type function foobar() + local tys = { types.string, types.number, types.unknown, types.boolean } + local result = types.unknown + for _, ty in tys do + result = types.intersectionof(result, ty) + end + return result + end + + local f: foobar<> + )"); + + LUAU_REQUIRE_NO_ERRORS(result); + CHECK(toString(requireType("f")) == "boolean & number & string"); +} + +TEST_CASE_FIXTURE(BuiltinsFixture, "udtf_flatten_on_intersectionof_empty") +{ + ScopedFastFlag newSolver{FFlag::LuauSolverV2, true}; + ScopedFastFlag sff{FFlag::LuauUnionofIntersectionofFlattens, true}; + + CheckResult result = check(R"( + type function foobar() + return types.intersectionof() + end + + local f: foobar<> + )"); + + LUAU_REQUIRE_NO_ERRORS(result); + CHECK(toString(requireType("f")) == "unknown"); +} + +TEST_CASE_FIXTURE(BuiltinsFixture, "udtf_flatten_on_intersectionof_two_things") +{ + ScopedFastFlag newSolver{FFlag::LuauSolverV2, true}; + ScopedFastFlag sff{FFlag::LuauUnionofIntersectionofFlattens, true}; + + CheckResult result = check(R"( + type function foobar() + return types.intersectionof(types.unknown, types.string) + end + -- forcing an error here to check the exact type of the union + local function ok(idx: foobar<>): never return idx end + )"); + + LUAU_REQUIRE_ERROR_COUNT(1, result); + TypeMismatch* tm = get(result.errors[0]); + REQUIRE(tm); + CHECK(toString(tm->givenType) == "string"); +} + TEST_CASE_FIXTURE(BuiltinsFixture, "udtf_intersection_serialization_works") { ScopedFastFlag newSolver{FFlag::LuauSolverV2, true}; @@ -1379,9 +1501,19 @@ TEST_CASE_FIXTURE(BuiltinsFixture, "tag_field") LUAU_REQUIRE_ERROR_COUNT(3, result); - CHECK(toString(result.errors[0]) == "Type '\"number\"' could not be converted into 'never'"); - CHECK(toString(result.errors[1]) == "Type '\"string\"' could not be converted into 'never'"); - CHECK(toString(result.errors[2]) == "Type '\"table\"' could not be converted into 'never'"); + + if (FFlag::LuauBetterTypeMismatchErrors) + { + CHECK("Expected this to be unreachable, but got '\"number\"'" == toString(result.errors[0])); + CHECK("Expected this to be unreachable, but got '\"string\"'" == toString(result.errors[1])); + CHECK("Expected this to be unreachable, but got '\"table\"'" == toString(result.errors[2])); + } + else + { + CHECK(toString(result.errors[0]) == "Type '\"number\"' could not be converted into 'never'"); + CHECK(toString(result.errors[1]) == "Type '\"string\"' could not be converted into 'never'"); + CHECK(toString(result.errors[2]) == "Type '\"table\"' could not be converted into 'never'"); + } } TEST_CASE_FIXTURE(BuiltinsFixture, "metatable_serialization") @@ -1409,7 +1541,10 @@ TEST_CASE_FIXTURE(BuiltinsFixture, "metatable_serialization") )"); LUAU_REQUIRE_ERROR_COUNT(1, result); - CHECK(toString(result.errors[0]) == R"(Type '{ @metatable { ma: boolean }, { a: number } }' could not be converted into 'number')"); + if (FFlag::LuauBetterTypeMismatchErrors) + CHECK(toString(result.errors[0]) == R"(Expected this to be 'number', but got '{ @metatable { ma: boolean }, { a: number } }')"); + else + CHECK(toString(result.errors[0]) == R"(Type '{ @metatable { ma: boolean }, { a: number } }' could not be converted into 'number')"); } TEST_CASE_FIXTURE(BuiltinsFixture, "nonstrict_mode") @@ -2650,4 +2785,32 @@ TEST_CASE_FIXTURE(BuiltinsFixture, "oss_1887_basic_match") LUAU_REQUIRE_ERROR(results, UnappliedTypeFunction); } +TEST_CASE_FIXTURE(BuiltinsFixture, "typeof_into_type_function_should_not_crash") +{ + ScopedFastFlag sff{FFlag::LuauSolverV2, true}; + ScopedFastFlag noCrash{FFlag::LuauTypeFunctionDeserializationShouldNotCrashOnGenericPacks, true}; + + CheckResult results = check(R"( + type function identity(t: type) + return t + end + + type func = typeof(function(...: parameters...) end) + local whomp: (arg1: T) -> identity + whomp(function(...) end :: func) + )"); + + // FIXME: this should not error at all, but type alias expansion is broken because we don't have proper instantiation + // LUAU_REQUIRE_NO_ERRORS(results); + + if (FFlag::LuauUserTypeFunctionsNoUninhabitedError) + LUAU_REQUIRE_ERROR_COUNT(1, results); + else + LUAU_REQUIRE_ERROR_COUNT(2, results); + auto err = get(results.errors[0]); + REQUIRE(err); + CHECK_EQ("Encountered unexpected generic type pack", err->message); +} + + TEST_SUITE_END(); diff --git a/tests/TypeInfer.aliases.test.cpp b/tests/TypeInfer.aliases.test.cpp index bc08030cb..4a974ff08 100644 --- a/tests/TypeInfer.aliases.test.cpp +++ b/tests/TypeInfer.aliases.test.cpp @@ -11,6 +11,7 @@ using namespace Luau; LUAU_FASTFLAG(LuauSolverV2) LUAU_FASTFLAG(LuauInitializeDefaultGenericParamsAtProgramPoint) +LUAU_FASTFLAG(LuauBetterTypeMismatchErrors) TEST_SUITE_BEGIN("TypeAliases"); @@ -214,7 +215,10 @@ TEST_CASE_FIXTURE(Fixture, "generic_aliases") LUAU_REQUIRE_ERROR_COUNT(1, result); CHECK(result.errors[0].location == Location{{4, 37}, {4, 42}}); - CHECK_EQ("Type 'string' could not be converted into 'number'", toString(result.errors[0])); + if (FFlag::LuauBetterTypeMismatchErrors) + CHECK_EQ("Expected this to be 'number', but got 'string'", toString(result.errors[0])); + else + CHECK_EQ("Type 'string' could not be converted into 'number'", toString(result.errors[0])); } TEST_CASE_FIXTURE(Fixture, "dependent_generic_aliases") @@ -230,7 +234,10 @@ TEST_CASE_FIXTURE(Fixture, "dependent_generic_aliases") LUAU_REQUIRE_ERROR_COUNT(1, result); CHECK(result.errors[0].location == Location{{4, 43}, {4, 48}}); - CHECK_EQ("Type 'string' could not be converted into 'number'", toString(result.errors[0])); + if (FFlag::LuauBetterTypeMismatchErrors) + CHECK_EQ("Expected this to be 'number', but got 'string'", toString(result.errors[0])); + else + CHECK_EQ("Type 'string' could not be converted into 'number'", toString(result.errors[0])); } TEST_CASE_FIXTURE(Fixture, "mutually_recursive_generic_aliases") diff --git a/tests/TypeInfer.builtins.test.cpp b/tests/TypeInfer.builtins.test.cpp index fc34305e6..a49ef3d87 100644 --- a/tests/TypeInfer.builtins.test.cpp +++ b/tests/TypeInfer.builtins.test.cpp @@ -12,11 +12,9 @@ using namespace Luau; LUAU_FASTFLAG(LuauSolverV2) LUAU_FASTFLAG(LuauTableCloneClonesType4) LUAU_FASTFLAG(LuauNoScopeShallNotSubsumeAll) -LUAU_FASTFLAG(LuauVectorLerp) -LUAU_FASTFLAG(LuauCompileVectorLerp) -LUAU_FASTFLAG(LuauTypeCheckerVectorLerp2) LUAU_FASTFLAG(LuauUnknownGlobalFixSuggestion) LUAU_FASTFLAG(LuauNewOverloadResolver2) +LUAU_FASTFLAG(LuauBetterTypeMismatchErrors) LUAU_FASTFLAG(LuauCloneForIntersectionsUnions) TEST_SUITE_BEGIN("BuiltinTests"); @@ -151,19 +149,32 @@ TEST_CASE_FIXTURE(BuiltinsFixture, "sort_with_bad_predicate") )"); LUAU_REQUIRE_ERROR_COUNT(1, result); - const std::string expected = "Type\n\t" - "'(number, number) -> boolean'" - "\ncould not be converted into\n\t" - "'((string, string) -> boolean)?'" - "\ncaused by:\n" - " None of the union options are compatible. For example:\n" - "Type\n\t" - "'(number, number) -> boolean'" - "\ncould not be converted into\n\t" - "'(string, string) -> boolean'" - "\ncaused by:\n" - " Argument #1 type is not compatible.\n" - "Type 'string' could not be converted into 'number'"; + const std::string expected = FFlag::LuauBetterTypeMismatchErrors ? "Expected this to be\n\t" + "'((string, string) -> boolean)?'" + "\nbut got\n\t" + "'(number, number) -> boolean'" + "\ncaused by:\n" + " None of the union options are compatible. For example:\n" + "Expected this to be\n\t" + "'(string, string) -> boolean'" + "\nbut got\n\t" + "'(number, number) -> boolean'" + "\ncaused by:\n" + " Argument #1 type is not compatible.\n" + "Expected this to be 'number', but got 'string'" + : "Type\n\t" + "'(number, number) -> boolean'" + "\ncould not be converted into\n\t" + "'((string, string) -> boolean)?'" + "\ncaused by:\n" + " None of the union options are compatible. For example:\n" + "Type\n\t" + "'(number, number) -> boolean'" + "\ncould not be converted into\n\t" + "'(string, string) -> boolean'" + "\ncaused by:\n" + " Argument #1 type is not compatible.\n" + "Type 'string' could not be converted into 'number'"; CHECK_EQ(expected, toString(result.errors[0])); } @@ -194,7 +205,10 @@ TEST_CASE_FIXTURE(BuiltinsFixture, "math_max_checks_for_numbers") )"); LUAU_REQUIRE_ERRORS(result); - CHECK_EQ("Type 'string' could not be converted into 'number'", toString(result.errors[0])); + if (FFlag::LuauBetterTypeMismatchErrors) + CHECK_EQ("Expected this to be 'number', but got 'string'", toString(result.errors[0])); + else + CHECK_EQ("Type 'string' could not be converted into 'number'", toString(result.errors[0])); } TEST_CASE_FIXTURE(BuiltinsFixture, "builtin_tables_sealed") @@ -845,8 +859,17 @@ TEST_CASE_FIXTURE(Fixture, "string_format_use_correct_argument2") LUAU_REQUIRE_ERROR_COUNT(2, result); - CHECK_EQ("Type 'string' could not be converted into 'number'", toString(result.errors[0])); - CHECK_EQ("Type 'number' could not be converted into 'string'", toString(result.errors[1])); + + if (FFlag::LuauBetterTypeMismatchErrors) + { + CHECK_EQ("Expected this to be 'number', but got 'string'", toString(result.errors[0])); + CHECK_EQ("Expected this to be 'string', but got 'number'", toString(result.errors[1])); + } + else + { + CHECK_EQ("Type 'string' could not be converted into 'number'", toString(result.errors[0])); + CHECK_EQ("Type 'number' could not be converted into 'string'", toString(result.errors[1])); + } } TEST_CASE_FIXTURE(BuiltinsFixture, "string_format_use_correct_argument3") @@ -903,7 +926,10 @@ TEST_CASE_FIXTURE(BuiltinsFixture, "aliased_string_format") )"); LUAU_REQUIRE_ERROR_COUNT(1, result); - CHECK_EQ("Type 'string' could not be converted into 'number'", toString(result.errors[0])); + if (FFlag::LuauBetterTypeMismatchErrors) + CHECK_EQ("Expected this to be 'number', but got 'string'", toString(result.errors[0])); + else + CHECK_EQ("Type 'string' could not be converted into 'number'", toString(result.errors[0])); } TEST_CASE_FIXTURE(BuiltinsFixture, "string_lib_self_noself") @@ -996,13 +1022,27 @@ TEST_CASE_FIXTURE(BuiltinsFixture, "tonumber_returns_optional_number_type") LUAU_REQUIRE_ERROR_COUNT(1, result); if (FFlag::LuauSolverV2) - CHECK_EQ( - "Type 'number?' could not be converted into 'number'; \n" - "this is because the 2nd component of the union is `nil`, which is not a subtype of `number`", - toString(result.errors[0]) - ); + { + if (FFlag::LuauBetterTypeMismatchErrors) + CHECK_EQ( + "Expected this to be 'number', but got 'number?'; \n" + "the 2nd component of the union is `nil`, which is not a subtype of `number`", + toString(result.errors[0]) + ); + else + CHECK_EQ( + "Type 'number?' could not be converted into 'number'; \n" + "this is because the 2nd component of the union is `nil`, which is not a subtype of `number`", + toString(result.errors[0]) + ); + } else - CHECK_EQ("Type 'number?' could not be converted into 'number'", toString(result.errors[0])); + { + if (FFlag::LuauBetterTypeMismatchErrors) + CHECK_EQ("Expected this to be 'number', but got 'number?'", toString(result.errors[0])); + else + CHECK_EQ("Type 'number?' could not be converted into 'number'", toString(result.errors[0])); + } } TEST_CASE_FIXTURE(BuiltinsFixture, "tonumber_returns_optional_number_type2") @@ -1882,12 +1922,6 @@ TEST_CASE_FIXTURE(BuiltinsFixture, "pairs_with_refined_any") TEST_CASE_FIXTURE(BuiltinsFixture, "vector_lerp_should_not_crash") { - ScopedFastFlag _[]{ - {FFlag::LuauCompileVectorLerp, true}, - {FFlag::LuauTypeCheckerVectorLerp2, true}, - {FFlag::LuauVectorLerp, true}, - }; - LUAU_REQUIRE_NO_ERRORS(check(R"( local function half(x: number, y: number, z: number): vector return vector.lerp(vector.zero, vector.create(x, y, z), 0.5) diff --git a/tests/TypeInfer.cfa.test.cpp b/tests/TypeInfer.cfa.test.cpp index e097e18e4..a72d81ed1 100644 --- a/tests/TypeInfer.cfa.test.cpp +++ b/tests/TypeInfer.cfa.test.cpp @@ -4,6 +4,8 @@ using namespace Luau; +LUAU_FASTFLAG(LuauBetterTypeMismatchErrors) + TEST_SUITE_BEGIN("ControlFlowAnalysis"); TEST_CASE_FIXTURE(BuiltinsFixture, "if_not_x_return") @@ -811,7 +813,10 @@ TEST_CASE_FIXTURE(BuiltinsFixture, "prototyping_and_visiting_alias_has_the_same_ LUAU_REQUIRE_ERROR_COUNT(1, result); - CHECK_EQ("Type 'nil' could not be converted into 'number'", toString(result.errors[0])); + if (FFlag::LuauBetterTypeMismatchErrors) + CHECK_EQ("Expected this to be 'number', but got 'nil'", toString(result.errors[0])); + else + CHECK_EQ("Type 'nil' could not be converted into 'number'", toString(result.errors[0])); CHECK_EQ("nil", toString(requireTypeAtPosition({8, 29}))); } @@ -834,7 +839,10 @@ TEST_CASE_FIXTURE(BuiltinsFixture, "prototyping_and_visiting_alias_has_the_same_ LUAU_REQUIRE_ERROR_COUNT(1, result); - CHECK_EQ("Type 'nil' could not be converted into 'number'", toString(result.errors[0])); + if (FFlag::LuauBetterTypeMismatchErrors) + CHECK_EQ("Expected this to be 'number', but got 'nil'", toString(result.errors[0])); + else + CHECK_EQ("Type 'nil' could not be converted into 'number'", toString(result.errors[0])); CHECK_EQ("nil", toString(requireTypeAtPosition({9, 43}))); } @@ -857,7 +865,10 @@ TEST_CASE_FIXTURE(BuiltinsFixture, "prototyping_and_visiting_alias_has_the_same_ LUAU_REQUIRE_ERROR_COUNT(1, result); - CHECK_EQ("Type 'nil' could not be converted into 'number'", toString(result.errors[0])); + if (FFlag::LuauBetterTypeMismatchErrors) + CHECK_EQ("Expected this to be 'number', but got 'nil'", toString(result.errors[0])); + else + CHECK_EQ("Type 'nil' could not be converted into 'number'", toString(result.errors[0])); CHECK_EQ("nil", toString(requireTypeAtPosition({9, 43}))); } diff --git a/tests/TypeInfer.classes.test.cpp b/tests/TypeInfer.classes.test.cpp index 0d14c6c8f..708028a7e 100644 --- a/tests/TypeInfer.classes.test.cpp +++ b/tests/TypeInfer.classes.test.cpp @@ -15,7 +15,7 @@ using namespace Luau; using std::nullopt; LUAU_FASTFLAG(LuauSolverV2) -LUAU_FASTFLAG(LuauPushTypeConstraint2) +LUAU_FASTFLAG(LuauBetterTypeMismatchErrors) TEST_SUITE_BEGIN("TypeInferExternTypes"); @@ -400,7 +400,10 @@ TEST_CASE_FIXTURE(ExternTypeFixture, "table_class_unification_reports_sane_error if (FFlag::LuauSolverV2) { LUAU_REQUIRE_ERROR_COUNT(1, result); - CHECK("Type 'Vector2' could not be converted into '{ Y: number, w: number, x: number }'" == toString(result.errors[0])); + if (FFlag::LuauBetterTypeMismatchErrors) + CHECK("Expected this to be '{ Y: number, w: number, x: number }', but got 'Vector2'" == toString(result.errors[0])); + else + CHECK("Type 'Vector2' could not be converted into '{ Y: number, w: number, x: number }'" == toString(result.errors[0])); } else { @@ -420,8 +423,16 @@ TEST_CASE_FIXTURE(ExternTypeFixture, "class_unification_type_mismatch_is_correct LUAU_REQUIRE_ERROR_COUNT(2, result); - REQUIRE_EQ("Type 'BaseClass' could not be converted into 'number'", toString(result.errors.at(0))); - REQUIRE_EQ("Type 'number' could not be converted into 'BaseClass'", toString(result.errors[1])); + if (FFlag::LuauBetterTypeMismatchErrors) + { + REQUIRE_EQ("Expected this to be 'number', but got 'BaseClass'", toString(result.errors.at(0))); + REQUIRE_EQ("Expected this to be 'BaseClass', but got 'number'", toString(result.errors[1])); + } + else + { + REQUIRE_EQ("Type 'BaseClass' could not be converted into 'number'", toString(result.errors.at(0))); + REQUIRE_EQ("Type 'number' could not be converted into 'BaseClass'", toString(result.errors[1])); + } } TEST_CASE_FIXTURE(ExternTypeFixture, "optional_class_field_access_error") @@ -454,16 +465,25 @@ b(a) LUAU_REQUIRE_ERROR_COUNT(1, result); + if (FFlag::LuauSolverV2) { - const std::string expected = "Type 'Vector2' could not be converted into '{ read X: unknown, read Y: string }'; \n" - "this is because accessing `Y` results in `number` in the former type and `string` in the latter type, " - "and `number` is not a subtype of `string`"; + const std::string expected = FFlag::LuauBetterTypeMismatchErrors + ? "Expected this to be '{ read X: unknown, read Y: string }', but got 'Vector2'; \n" + "accessing `Y` results in `number` in the latter type and `string` in the former type, " + "and `number` is not a subtype of `string`" + : "Type 'Vector2' could not be converted into '{ read X: unknown, read Y: string }'; \n" + "this is because accessing `Y` results in `number` in the former type and `string` in the latter type, " + "and `number` is not a subtype of `string`"; CHECK_EQ(expected, toString(result.errors.at(0))); } else { - const std::string expected = R"(Type 'Vector2' could not be converted into '{- X: number, Y: string -}' + const std::string expected = FFlag::LuauBetterTypeMismatchErrors ? R"(Expected this to be '{- X: number, Y: string -}', but got 'Vector2' +caused by: + Property 'Y' is not compatible. +Expected this to be 'string', but got 'number')" + : R"(Type 'Vector2' could not be converted into '{- X: number, Y: string -}' caused by: Property 'Y' is not compatible. Type 'number' could not be converted into 'string')"; @@ -481,7 +501,10 @@ local a: ChildClass = i )"); LUAU_REQUIRE_ERROR_COUNT(1, result); - CHECK_EQ("Type 'ChildClass' from 'Test' could not be converted into 'ChildClass' from 'MainModule'", toString(result.errors.at(0))); + if (FFlag::LuauBetterTypeMismatchErrors) + CHECK_EQ("Expected this to be 'ChildClass' from 'MainModule', but got 'ChildClass' from 'Test'", toString(result.errors.at(0))); + else + CHECK_EQ("Type 'ChildClass' from 'Test' could not be converted into 'ChildClass' from 'MainModule'", toString(result.errors.at(0))); } TEST_CASE_FIXTURE(ExternTypeFixture, "intersections_of_unions_of_extern_types") @@ -546,14 +569,28 @@ local b: B = a LUAU_REQUIRE_ERRORS(result); if (FFlag::LuauSolverV2) - CHECK( - "Type 'A' could not be converted into 'B'; \n" - "this is because accessing `x` results in `ChildClass` in the former type and `BaseClass` in the latter type, and `ChildClass` is not " - "exactly `BaseClass`" == toString(result.errors.at(0)) - ); + { + if (FFlag::LuauBetterTypeMismatchErrors) + CHECK( + "Expected this to be 'B', but got 'A'; \n" + "accessing `x` results in `ChildClass` in the latter type and `BaseClass` in the former type, and `ChildClass` is not " + "exactly `BaseClass`" == toString(result.errors.at(0)) + ); + else + CHECK( + "Type 'A' could not be converted into 'B'; \n" + "this is because accessing `x` results in `ChildClass` in the former type and `BaseClass` in the latter type, and `ChildClass` is " + "not " + "exactly `BaseClass`" == toString(result.errors.at(0)) + ); + } else { - const std::string expected = R"(Type 'A' could not be converted into 'B' + const std::string expected = FFlag::LuauBetterTypeMismatchErrors ? R"(Expected this to be exactly 'B', but got 'A' +caused by: + Property 'x' is not compatible. +Expected this to be exactly 'BaseClass', but got 'ChildClass')" + : R"(Type 'A' could not be converted into 'B' caused by: Property 'x' is not compatible. Type 'ChildClass' could not be converted into 'BaseClass' in an invariant context)"; @@ -669,7 +706,16 @@ TEST_CASE_FIXTURE(ExternTypeFixture, "indexable_extern_types") )"); if (FFlag::LuauSolverV2) - CHECK("Type 'boolean' could not be converted into 'number | string'" == toString(result.errors.at(0))); + { + if (FFlag::LuauBetterTypeMismatchErrors) + CHECK("Expected this to be 'number | string', but got 'boolean'" == toString(result.errors.at(0))); + else + CHECK("Type 'boolean' could not be converted into 'number | string'" == toString(result.errors.at(0))); + } + else if (FFlag::LuauBetterTypeMismatchErrors) + CHECK_EQ( + toString(result.errors.at(0)), "Expected this to be 'number | string', but got 'boolean'; none of the union options are compatible" + ); else CHECK_EQ( toString(result.errors.at(0)), @@ -683,7 +729,16 @@ TEST_CASE_FIXTURE(ExternTypeFixture, "indexable_extern_types") )"); if (FFlag::LuauSolverV2) - CHECK("Type 'boolean' could not be converted into 'number | string'" == toString(result.errors.at(0))); + { + if (FFlag::LuauBetterTypeMismatchErrors) + CHECK("Expected this to be 'number | string', but got 'boolean'" == toString(result.errors.at(0))); + else + CHECK("Type 'boolean' could not be converted into 'number | string'" == toString(result.errors.at(0))); + } + else if (FFlag::LuauBetterTypeMismatchErrors) + CHECK_EQ( + toString(result.errors.at(0)), "Expected this to be 'number | string', but got 'boolean'; none of the union options are compatible" + ); else CHECK_EQ( toString(result.errors.at(0)), @@ -702,6 +757,8 @@ TEST_CASE_FIXTURE(ExternTypeFixture, "indexable_extern_types") { // Disabled for now. CLI-115686 } + else if (FFlag::LuauBetterTypeMismatchErrors) + CHECK_EQ(toString(result.errors.at(0)), "Expected this to be 'number', but got 'string'"); else CHECK_EQ(toString(result.errors.at(0)), "Type 'string' could not be converted into 'number'"); } @@ -710,7 +767,11 @@ TEST_CASE_FIXTURE(ExternTypeFixture, "indexable_extern_types") local x : IndexableClass local str : string = x.key )"); - CHECK_EQ(toString(result.errors.at(0)), "Type 'number' could not be converted into 'string'"); + + if (FFlag::LuauBetterTypeMismatchErrors) + CHECK_EQ(toString(result.errors.at(0)), "Expected this to be 'string', but got 'number'"); + else + CHECK_EQ(toString(result.errors.at(0)), "Type 'number' could not be converted into 'string'"); } // Check that we string key are rejected if the indexer's key type is not compatible with string @@ -728,6 +789,8 @@ TEST_CASE_FIXTURE(ExternTypeFixture, "indexable_extern_types") )"); if (FFlag::LuauSolverV2) CHECK_EQ(toString(result.errors.at(0)), "Key 'key' not found in class 'IndexableNumericKeyClass'"); + else if (FFlag::LuauBetterTypeMismatchErrors) + CHECK_EQ(toString(result.errors.at(0)), "Expected this to be 'number', but got 'string'"); else CHECK_EQ(toString(result.errors.at(0)), "Type 'string' could not be converted into 'number'"); } @@ -737,7 +800,11 @@ TEST_CASE_FIXTURE(ExternTypeFixture, "indexable_extern_types") local str : string x[str] = 1 -- Index with a non-const string )"); - CHECK_EQ(toString(result.errors.at(0)), "Type 'string' could not be converted into 'number'"); + + if (FFlag::LuauBetterTypeMismatchErrors) + CHECK_EQ(toString(result.errors.at(0)), "Expected this to be 'number', but got 'string'"); + else + CHECK_EQ(toString(result.errors.at(0)), "Type 'string' could not be converted into 'number'"); } { CheckResult result = check(R"( @@ -753,6 +820,8 @@ TEST_CASE_FIXTURE(ExternTypeFixture, "indexable_extern_types") )"); if (FFlag::LuauSolverV2) CHECK(toString(result.errors.at(0)) == "Key 'key' not found in class 'IndexableNumericKeyClass'"); + else if (FFlag::LuauBetterTypeMismatchErrors) + CHECK_EQ(toString(result.errors.at(0)), "Expected this to be 'number', but got 'string'"); else CHECK_EQ(toString(result.errors.at(0)), "Type 'string' could not be converted into 'number'"); } @@ -762,7 +831,11 @@ TEST_CASE_FIXTURE(ExternTypeFixture, "indexable_extern_types") local str : string local y = x[str] -- Index with a non-const string )"); - CHECK_EQ(toString(result.errors.at(0)), "Type 'string' could not be converted into 'number'"); + + if (FFlag::LuauBetterTypeMismatchErrors) + CHECK_EQ(toString(result.errors.at(0)), "Expected this to be 'number', but got 'string'"); + else + CHECK_EQ(toString(result.errors.at(0)), "Type 'string' could not be converted into 'number'"); } } @@ -1031,10 +1104,7 @@ TEST_CASE_FIXTURE(BuiltinsFixture, "extern_type_check_key_becomes_intersection") TEST_CASE_FIXTURE(BuiltinsFixture, "extern_type_check_key_superset") { - ScopedFastFlag sffs[] = { - {FFlag::LuauSolverV2, true}, - {FFlag::LuauPushTypeConstraint2, true}, - }; + ScopedFastFlag _{FFlag::LuauSolverV2, true}; loadDefinition(R"( declare extern type Foobar with diff --git a/tests/TypeInfer.definitions.test.cpp b/tests/TypeInfer.definitions.test.cpp index b5e621ea2..77a0c683e 100644 --- a/tests/TypeInfer.definitions.test.cpp +++ b/tests/TypeInfer.definitions.test.cpp @@ -9,7 +9,6 @@ using namespace Luau; -LUAU_FASTFLAG(LuauNoMoreComparisonTypeFunctions) LUAU_FASTINT(LuauTypeInferRecursionLimit) @@ -574,7 +573,6 @@ TEST_CASE_FIXTURE(BuiltinsFixture, "cli_142285_reduce_minted_union_func") { ScopedFastFlag sff[] = { {FFlag::LuauSolverV2, true}, - {FFlag::LuauNoMoreComparisonTypeFunctions, true}, }; CheckResult result = check(R"( diff --git a/tests/TypeInfer.functions.test.cpp b/tests/TypeInfer.functions.test.cpp index fb116cc66..bcd6836d7 100644 --- a/tests/TypeInfer.functions.test.cpp +++ b/tests/TypeInfer.functions.test.cpp @@ -24,12 +24,13 @@ LUAU_FASTFLAG(LuauCollapseShouldNotCrash) LUAU_FASTFLAG(LuauFormatUseLastPosition) LUAU_FASTFLAG(LuauNoScopeShallNotSubsumeAll) LUAU_FASTFLAG(LuauNewOverloadResolver2) -LUAU_FASTFLAG(LuauPushTypeConstraint2) +LUAU_FASTFLAG(LuauBetterTypeMismatchErrors) LUAU_FASTFLAG(LuauPushTypeConstraintIntersection) -LUAU_FASTFLAG(LuauPushTypeConstraintSingleton) LUAU_FASTFLAG(LuauPushTypeConstraintLambdas2) LUAU_FASTFLAG(LuauIncludeExplicitGenericPacks) -LUAU_FASTFLAG(LuauEGFixGenericsList) +LUAU_FASTFLAG(LuauInstantiationUsesGenericPolarity2) +LUAU_FASTFLAG(LuauPushTypeConstraintStripNilFromFunction) +LUAU_FASTFLAG(LuauCheckFunctionStatementTypes) TEST_SUITE_BEGIN("TypeInferFunctions"); @@ -1319,10 +1320,27 @@ f(function(a, b, c, ...) return a + b end) std::string expected; if (FFlag::LuauInstantiateInSubtyping) { - expected = "Type\n\t" - "'(number, number, a) -> number'" - "\ncould not be converted into\n\t" + if (FFlag::LuauBetterTypeMismatchErrors) + expected = "Expected this to be\n\t" + "'(number, number) -> number'" + "\nbut got\n\t" + "'(number, number, a) -> number'" + "\ncaused by:\n" + " Argument count mismatch. Function expects 3 arguments, but only 2 are specified"; + else + expected = "Type\n\t" + "'(number, number, a) -> number'" + "\ncould not be converted into\n\t" + "'(number, number) -> number'" + "\ncaused by:\n" + " Argument count mismatch. Function expects 3 arguments, but only 2 are specified"; + } + else if (FFlag::LuauBetterTypeMismatchErrors) + { + expected = "Expected this to be\n\t" "'(number, number) -> number'" + "\nbut got\n\t" + "'(number, number, *error-type*) -> number'" "\ncaused by:\n" " Argument count mismatch. Function expects 3 arguments, but only 2 are specified"; } @@ -1364,7 +1382,10 @@ f(function(x) return x * 2 end) )"); LUAU_REQUIRE_ERROR_COUNT(1, result); - CHECK_EQ("Type 'number' could not be converted into 'Table'", toString(result.errors[0])); + if (FFlag::LuauBetterTypeMismatchErrors) + CHECK_EQ("Expected this to be 'Table', but got 'number'", toString(result.errors[0])); + else + CHECK_EQ("Type 'number' could not be converted into 'Table'", toString(result.errors[0])); // Return type doesn't inference 'nil' result = check(R"( @@ -1531,12 +1552,19 @@ local b: B = a )"); LUAU_REQUIRE_ERROR_COUNT(1, result); - const std::string expected = "Type\n\t" - "'(number, number) -> string'" - "\ncould not be converted into\n\t" - "'(number) -> string'" - "\ncaused by:\n" - " Argument count mismatch. Function expects 2 arguments, but only 1 is specified"; + const std::string expected = FFlag::LuauBetterTypeMismatchErrors + ? "Expected this to be\n\t" + "'(number) -> string'" + "\nbut got\n\t" + "'(number, number) -> string'" + "\ncaused by:\n" + " Argument count mismatch. Function expects 2 arguments, but only 1 is specified" + : "Type\n\t" + "'(number, number) -> string'" + "\ncould not be converted into\n\t" + "'(number) -> string'" + "\ncaused by:\n" + " Argument count mismatch. Function expects 2 arguments, but only 1 is specified"; CHECK_EQ(expected, toString(result.errors[0])); } @@ -1554,13 +1582,20 @@ local b: B = a )"); LUAU_REQUIRE_ERROR_COUNT(1, result); - const std::string expected = "Type\n\t" - "'(number, number) -> string'" - "\ncould not be converted into\n\t" - "'(number, string) -> string'" - "\ncaused by:\n" - " Argument #2 type is not compatible.\n" - "Type 'string' could not be converted into 'number'"; + const std::string expected = FFlag::LuauBetterTypeMismatchErrors ? "Expected this to be\n\t" + "'(number, string) -> string'" + "\nbut got\n\t" + "'(number, number) -> string'" + "\ncaused by:\n" + " Argument #2 type is not compatible.\n" + "Expected this to be 'number', but got 'string'" + : "Type\n\t" + "'(number, number) -> string'" + "\ncould not be converted into\n\t" + "'(number, string) -> string'" + "\ncaused by:\n" + " Argument #2 type is not compatible.\n" + "Type 'string' could not be converted into 'number'"; CHECK_EQ(expected, toString(result.errors[0])); } @@ -1578,12 +1613,18 @@ local b: B = a )"); LUAU_REQUIRE_ERROR_COUNT(1, result); - const std::string expected = "Type\n\t" - "'(number, number) -> number'" - "\ncould not be converted into\n\t" - "'(number, number) -> (number, boolean)'" - "\ncaused by:\n" - " Function only returns 1 value, but 2 are required here"; + const std::string expected = FFlag::LuauBetterTypeMismatchErrors ? "Expected this to be\n\t" + "'(number, number) -> (number, boolean)'" + "\nbut got\n\t" + "'(number, number) -> number'" + "\ncaused by:\n" + " Function only returns 1 value, but 2 are required here" + : "Type\n\t" + "'(number, number) -> number'" + "\ncould not be converted into\n\t" + "'(number, number) -> (number, boolean)'" + "\ncaused by:\n" + " Function only returns 1 value, but 2 are required here"; CHECK_EQ(expected, toString(result.errors[0])); } @@ -1601,13 +1642,20 @@ local b: B = a )"); LUAU_REQUIRE_ERROR_COUNT(1, result); - const std::string expected = "Type\n\t" - "'(number, number) -> string'" - "\ncould not be converted into\n\t" - "'(number, number) -> number'" - "\ncaused by:\n" - " Return type is not compatible.\n" - "Type 'string' could not be converted into 'number'"; + const std::string expected = FFlag::LuauBetterTypeMismatchErrors ? "Expected this to be\n\t" + "'(number, number) -> number'" + "\nbut got\n\t" + "'(number, number) -> string'" + "\ncaused by:\n" + " Return type is not compatible.\n" + "Expected this to be 'number', but got 'string'" + : "Type\n\t" + "'(number, number) -> string'" + "\ncould not be converted into\n\t" + "'(number, number) -> number'" + "\ncaused by:\n" + " Return type is not compatible.\n" + "Type 'string' could not be converted into 'number'"; CHECK_EQ(expected, toString(result.errors[0])); } @@ -1625,13 +1673,20 @@ local b: B = a )"); LUAU_REQUIRE_ERROR_COUNT(1, result); - const std::string expected = "Type\n\t" - "'(number, number) -> (number, string)'" - "\ncould not be converted into\n\t" - "'(number, number) -> (number, boolean)'" - "\ncaused by:\n" - " Return #2 type is not compatible.\n" - "Type 'string' could not be converted into 'boolean'"; + const std::string expected = FFlag::LuauBetterTypeMismatchErrors ? "Expected this to be\n\t" + "'(number, number) -> (number, boolean)'" + "\nbut got\n\t" + "'(number, number) -> (number, string)'" + "\ncaused by:\n" + " Return #2 type is not compatible.\n" + "Expected this to be 'boolean', but got 'string'" + : "Type\n\t" + "'(number, number) -> (number, string)'" + "\ncould not be converted into\n\t" + "'(number, number) -> (number, boolean)'" + "\ncaused by:\n" + " Return #2 type is not compatible.\n" + "Type 'string' could not be converted into 'boolean'"; CHECK_EQ(expected, toString(result.errors[0])); } @@ -1703,8 +1758,16 @@ end else { LUAU_REQUIRE_ERROR_COUNT(2, result); - CHECK_EQ(toString(result.errors[0]), R"(Type 'string' could not be converted into 'number')"); - CHECK_EQ(toString(result.errors[1]), R"(Type 'string' could not be converted into 'number')"); + if (FFlag::LuauBetterTypeMismatchErrors) + { + CHECK_EQ(toString(result.errors[0]), R"(Expected this to be 'number', but got 'string')"); + CHECK_EQ(toString(result.errors[1]), R"(Expected this to be 'number', but got 'string')"); + } + else + { + CHECK_EQ(toString(result.errors[0]), R"(Type 'string' could not be converted into 'number')"); + CHECK_EQ(toString(result.errors[1]), R"(Type 'string' could not be converted into 'number')"); + } } } @@ -1753,6 +1816,8 @@ TEST_CASE_FIXTURE(Fixture, "inferred_higher_order_functions_are_quantified_at_th TEST_CASE_FIXTURE(BuiltinsFixture, "function_decl_non_self_unsealed_overwrite") { + ScopedFastFlag _{FFlag::LuauCheckFunctionStatementTypes, true}; + CheckResult result = check(R"( local t = { f = nil :: ((x: number) -> number)? } @@ -1768,9 +1833,27 @@ end if (FFlag::LuauSolverV2) { - LUAU_CHECK_ERROR_COUNT(1, result); + LUAU_CHECK_ERROR_COUNT(2, result); LUAU_CHECK_ERROR(result, WhereClauseNeeded); } + else if (FFlag::LuauBetterTypeMismatchErrors) + { + LUAU_REQUIRE_ERROR_COUNT(2, result); + CHECK_EQ(toString(result.errors[0]), R"(Expected this to be + '((number) -> number)?' +but got + '(string) -> string' +caused by: + None of the union options are compatible. For example: +Expected this to be + '(number) -> number' +but got + '(string) -> string' +caused by: + Argument #1 type is not compatible. +Expected this to be 'string', but got 'number')"); + CHECK_EQ(toString(result.errors[1]), R"(Expected this to be 'number', but got 'string')"); + } else { LUAU_REQUIRE_ERROR_COUNT(2, result); @@ -1815,15 +1898,30 @@ function t:b() return 2 end -- not OK LUAU_REQUIRE_ERROR_COUNT(1, result); - CHECK_EQ( - "Type\n\t" - "'(*error-type*) -> number'" - "\ncould not be converted into\n\t" - "'() -> number'\n" - "caused by:\n" - " Argument count mismatch. Function expects 1 argument, but none are specified", - toString(result.errors[0]) - ); + if (FFlag::LuauBetterTypeMismatchErrors) + { + CHECK_EQ( + "Expected this to be\n\t" + "'() -> number'" + "\nbut got\n\t" + "'(*error-type*) -> number'" + "\ncaused by:\n" + " Argument count mismatch. Function expects 1 argument, but none are specified", + toString(result.errors[0]) + ); + } + else + { + CHECK_EQ( + "Type\n\t" + "'(*error-type*) -> number'" + "\ncould not be converted into\n\t" + "'() -> number'\n" + "caused by:\n" + " Argument count mismatch. Function expects 1 argument, but none are specified", + toString(result.errors[0]) + ); + } } TEST_CASE_FIXTURE(Fixture, "too_few_arguments_variadic") @@ -2092,10 +2190,16 @@ z = y -- Not OK, so the line is colorable LUAU_REQUIRE_ERROR_COUNT(1, result); const std::string expected = - "Type\n\t" - R"('(("blue" | "red") -> ("blue" | "red") -> ("blue" | "red") -> boolean) & (("blue" | "red") -> ("blue") -> ("blue") -> false) & (("blue" | "red") -> ("red") -> ("red") -> false) & (("blue") -> ("blue") -> ("blue" | "red") -> false) & (("red") -> ("red") -> ("blue" | "red") -> false)')" - "\ncould not be converted into\n\t" - R"('("blue" | "red") -> ("blue" | "red") -> ("blue" | "red") -> false'; none of the intersection parts are compatible)"; + FFlag::LuauBetterTypeMismatchErrors + ? "Expected this to be\n\t" + R"('("blue" | "red") -> ("blue" | "red") -> ("blue" | "red") -> false')" + "\nbut got\n\t" + R"('(("blue" | "red") -> ("blue" | "red") -> ("blue" | "red") -> boolean) & (("blue" | "red") -> ("blue") -> ("blue") -> false) & (("blue" | "red") -> ("red") -> ("red") -> false) & (("blue") -> ("blue") -> ("blue" | "red") -> false) & (("red") -> ("red") -> ("blue" | "red") -> false)')" + "; none of the intersection parts are compatible" + : "Type\n\t" + R"('(("blue" | "red") -> ("blue" | "red") -> ("blue" | "red") -> boolean) & (("blue" | "red") -> ("blue") -> ("blue") -> false) & (("blue" | "red") -> ("red") -> ("red") -> false) & (("blue") -> ("blue") -> ("blue" | "red") -> false) & (("red") -> ("red") -> ("blue" | "red") -> false)')" + "\ncould not be converted into\n\t" + R"('("blue" | "red") -> ("blue" | "red") -> ("blue" | "red") -> false'; none of the intersection parts are compatible)"; CHECK_EQ(expected, toString(result.errors[0])); } @@ -2318,12 +2422,24 @@ TEST_CASE_FIXTURE(BuiltinsFixture, "param_1_and_2_both_takes_the_same_generic_bu LUAU_REQUIRE_ERROR_COUNT(2, result); - const std::string expected = R"(Type '{| x: number |}' could not be converted into 'vec2?' + if (FFlag::LuauBetterTypeMismatchErrors) + { + const std::string expected = R"(Expected this to be 'vec2?', but got '{| x: number |}' caused by: None of the union options are compatible. For example: Table type '{| x: number |}' not compatible with type 'vec2' because the former is missing field 'y')"; - CHECK_EQ(expected, toString(result.errors[0])); - CHECK_EQ("Type 'vec2' could not be converted into 'number'", toString(result.errors[1])); + CHECK_EQ(expected, toString(result.errors[0])); + CHECK_EQ("Expected this to be 'number', but got 'vec2'", toString(result.errors[1])); + } + else + { + const std::string expected = R"(Type '{| x: number |}' could not be converted into 'vec2?' +caused by: + None of the union options are compatible. For example: +Table type '{| x: number |}' not compatible with type 'vec2' because the former is missing field 'y')"; + CHECK_EQ(expected, toString(result.errors[0])); + CHECK_EQ("Type 'vec2' could not be converted into 'number'", toString(result.errors[1])); + } } } @@ -2350,8 +2466,16 @@ TEST_CASE_FIXTURE(BuiltinsFixture, "param_1_and_2_both_takes_the_same_generic_bu { LUAU_REQUIRE_ERROR_COUNT(2, result); - CHECK_EQ(toString(result.errors[0]), "Type 'string' could not be converted into 'number'"); - CHECK_EQ(toString(result.errors[1]), "Type 'number' could not be converted into 'boolean'"); + if (FFlag::LuauBetterTypeMismatchErrors) + { + CHECK_EQ(toString(result.errors[0]), "Expected this to be 'number', but got 'string'"); + CHECK_EQ(toString(result.errors[1]), "Expected this to be 'boolean', but got 'number'"); + } + else + { + CHECK_EQ(toString(result.errors[0]), "Type 'string' could not be converted into 'number'"); + CHECK_EQ(toString(result.errors[1]), "Type 'number' could not be converted into 'boolean'"); + } } } @@ -2391,7 +2515,6 @@ TEST_CASE_FIXTURE(Fixture, "generic_packs_are_not_variadic") ScopedFastFlag sffs[] = { {FFlag::LuauSolverV2, true}, {FFlag::LuauIncludeExplicitGenericPacks, true}, - {FFlag::LuauEGFixGenericsList, true}, }; CheckResult result = check(R"( @@ -2441,7 +2564,11 @@ TEST_CASE_FIXTURE(BuiltinsFixture, "num_is_solved_before_num_or_str") LUAU_REQUIRE_ERROR_COUNT(1, result); - CHECK_EQ("Type 'string' could not be converted into 'number'", toString(result.errors[0])); + if (FFlag::LuauBetterTypeMismatchErrors) + CHECK_EQ("Expected this to be 'number', but got 'string'", toString(result.errors[0])); + else + CHECK_EQ("Type 'string' could not be converted into 'number'", toString(result.errors[0])); + CHECK_EQ("() -> number", toString(requireType("num_or_str"))); } @@ -2462,7 +2589,11 @@ TEST_CASE_FIXTURE(BuiltinsFixture, "num_is_solved_after_num_or_str") )"); LUAU_REQUIRE_ERROR_COUNT(1, result); - CHECK_EQ("Type 'string' could not be converted into 'number'", toString(result.errors[0])); + + if (FFlag::LuauBetterTypeMismatchErrors) + CHECK_EQ("Expected this to be 'number', but got 'string'", toString(result.errors[0])); + else + CHECK_EQ("Type 'string' could not be converted into 'number'", toString(result.errors[0])); CHECK_EQ("() -> number", toString(requireType("num_or_str"))); } @@ -2537,9 +2668,18 @@ end CHECK(get(result.errors[0])); // This check is unstable between different machines and different runs of DCR because it depends on string equality between // blocked type numbers, which is not guaranteed. - bool r = toString(result.errors[1]) == "Type pack '*blocked-tp-1*' could not be converted into 'boolean'; type *blocked-tp-1*.tail() " - "(*blocked-tp-1*) is not a subtype of boolean (boolean)"; - CHECK(r); + if (FFlag::LuauBetterTypeMismatchErrors) + { + bool r = toString(result.errors[1]) == "Expected this to be 'boolean', but got '*blocked-tp-1*'; type *blocked-tp-1*.tail() " + "(*blocked-tp-1*) is not a subtype of boolean (boolean)"; + CHECK(r); + } + else + { + bool r = toString(result.errors[1]) == "Type pack '*blocked-tp-1*' could not be converted into 'boolean'; type *blocked-tp-1*.tail() " + "(*blocked-tp-1*) is not a subtype of boolean (boolean)"; + CHECK(r); + } CHECK( toString(result.errors[2]) == "Operator '-' could not be applied to operands of types unknown and number; there is no corresponding overload for __sub" @@ -3369,7 +3509,6 @@ TEST_CASE_FIXTURE(Fixture, "oss_2065_bidirectional_inference_function_call") { ScopedFastFlag sffs[] = { {FFlag::LuauSolverV2, true}, - {FFlag::LuauPushTypeConstraint2, true}, {FFlag::LuauPushTypeConstraintLambdas2, true}, {FFlag::LuauPushTypeConstraintIntersection, true}, {FFlag::DebugLuauAssertOnForcedConstraint, true}, @@ -3394,7 +3533,6 @@ TEST_CASE_FIXTURE(Fixture, "bidirectionally_infer_lambda_with_partially_resolved { ScopedFastFlag sffs[] = { {FFlag::LuauSolverV2, true}, - {FFlag::LuauPushTypeConstraint2, true}, {FFlag::LuauPushTypeConstraintLambdas2, true}, {FFlag::DebugLuauAssertOnForcedConstraint, true}, }; @@ -3418,7 +3556,6 @@ TEST_CASE_FIXTURE(Fixture, "bidirectional_inference_goes_through_ifelse") { ScopedFastFlag sffs[] = { {FFlag::LuauSolverV2, true}, - {FFlag::LuauPushTypeConstraint2, true}, {FFlag::LuauPushTypeConstraintLambdas2, true}, {FFlag::DebugLuauAssertOnForcedConstraint, true}, }; @@ -3605,4 +3742,104 @@ TEST_CASE_FIXTURE(Fixture, "overload_selection_unambiguous_with_constraint") CHECK_EQ("(string) -> ()", toString(requireType("g"))); } +TEST_CASE_FIXTURE(Fixture, "oss_2118") +{ + ScopedFastFlag _{FFlag::LuauInstantiationUsesGenericPolarity2, true}; + + LUAU_REQUIRE_NO_ERRORS(check(R"( + local foo:

(constructor: (P) -> any) -> (P) -> any = (nil :: any) + local fn = foo(function (value: { test: true }) + return value.test + end) + )")); + + CHECK_EQ("({ test: true }) -> any", toString(requireType("fn"))); +} + +TEST_CASE_FIXTURE(BuiltinsFixture, "oss_2125") +{ + ScopedFastFlag sffs[] = { + {FFlag::LuauSolverV2, true}, + {FFlag::LuauInstantiationUsesGenericPolarity2, true}, + }; + + LUAU_REQUIRE_NO_ERRORS(check(R"( + export type function CombineTableAndSetIndexer(a: type, b: type, c: type) + local t = {} + + for key, value in a:properties() do + t[key] = value.read + end + + if b.tag == "table" then + for key, value in b:properties() do + t[key] = value.read + end + end + + return types.newtable(t :: any, { index = types.number, readresult = c, writeresult = c }) + end + + type SpecialProperties = { + test: string?, + } + + local function component( + constructor: (props: Properties) -> () + ): ( + CombineTableAndSetIndexer + ) -> () + return function(props: Properties) end + end + + local mrrp = component(function(thing: { + meow: number, + }) end) + + mrrp({ + meow = 5, + }) + + )")); +} + +TEST_CASE_FIXTURE(BuiltinsFixture, "bidirectional_lambda_inference_applies_nilable_functions") +{ + ScopedFastFlag sffs[] = { + {FFlag::LuauSolverV2, true}, + {FFlag::LuauPushTypeConstraintLambdas2, true}, + {FFlag::LuauPushTypeConstraintStripNilFromFunction, true}, + {FFlag::DebugLuauAssertOnForcedConstraint, true}, + }; + + LUAU_REQUIRE_NO_ERRORS(check(R"( + local listdir: (string, ((string) -> boolean)?) -> { string } = nil :: any + listdir("my_directory", function (path) + print(path) + return true + end) + )")); + + CHECK_EQ("string", toString(requireTypeAtPosition({3, 19}))); +} + +TEST_CASE_FIXTURE(Fixture, "function_statement_with_incorrect_function_type") +{ + ScopedFastFlag _{FFlag::LuauCheckFunctionStatementTypes, true}; + + CheckResult result = check(R"( + local Library: { isnan: (number) -> number } = {} :: any + + function Library.isnan(s: string): boolean + return s == "NaN" + end + )"); + + LUAU_REQUIRE_ERROR_COUNT(1, result); + auto err = get(result.errors[0]); + REQUIRE(err); + CHECK_EQ("(number) -> number", toString(err->wantedType)); + CHECK_EQ("(string) -> boolean", toString(err->givenType)); +} + TEST_SUITE_END(); diff --git a/tests/TypeInfer.generics.test.cpp b/tests/TypeInfer.generics.test.cpp index b296fdbe3..495a1462b 100644 --- a/tests/TypeInfer.generics.test.cpp +++ b/tests/TypeInfer.generics.test.cpp @@ -13,9 +13,9 @@ LUAU_FASTFLAG(LuauIntersectNotNil) LUAU_FASTFLAG(DebugLuauAssertOnForcedConstraint) LUAU_FASTFLAG(DebugLuauStringSingletonBasedOnQuotes) LUAU_FASTFLAG(LuauUseTopTableForTableClearAndIsFrozen) -LUAU_FASTFLAG(LuauEGFixGenericsList) LUAU_FASTFLAG(LuauIncludeExplicitGenericPacks) -LUAU_FASTFLAG(LuauInstantiationUsesGenericPolarity) +LUAU_FASTFLAG(LuauBetterTypeMismatchErrors) +LUAU_FASTFLAG(LuauInstantiationUsesGenericPolarity2) using namespace Luau; @@ -65,7 +65,7 @@ TEST_CASE_FIXTURE(Fixture, "check_generic_local_function2") TEST_CASE_FIXTURE(Fixture, "unions_and_generics") { - ScopedFastFlag _{FFlag::LuauInstantiationUsesGenericPolarity, true}; + ScopedFastFlag _{FFlag::LuauInstantiationUsesGenericPolarity2, true}; CheckResult result = check(R"( type foo = (T | {T}) -> T @@ -926,6 +926,18 @@ y.a.c = y CHECK_EQ(toString(mismatch2->givenType), "number"); CHECK_EQ(toString(mismatch2->wantedType), "string"); } + else if (FFlag::LuauBetterTypeMismatchErrors) + { + LUAU_REQUIRE_ERROR_COUNT(2, result); + const std::string expected = R"(Expected this to be exactly 'T', but got 'y' +caused by: + Property 'a' is not compatible. +Expected this to be exactly 'U', but got '{| c: T?, d: number |}' +caused by: + Property 'd' is not compatible. +Expected this to be exactly 'string', but got 'number')"; + CHECK_EQ(expected, toString(result.errors[0])); + } else { LUAU_REQUIRE_ERROR_COUNT(2, result); @@ -1084,6 +1096,10 @@ TEST_CASE_FIXTURE(Fixture, "generic_argument_pack_type_inferred_from_return") CHECK_EQ(toString(tm->wantedType), "string"); CHECK_EQ(toString(tm->givenType), "number"); } + else if (FFlag::LuauBetterTypeMismatchErrors) + { + CHECK_EQ(toString(result.errors[0]), R"(Expected this to be 'string', but got 'number')"); + } else { CHECK_EQ(toString(result.errors[0]), R"(Type 'number' could not be converted into 'string')"); @@ -1158,7 +1174,10 @@ wrapper(foo, test2, "3") -- not ok (type mismatch, string instead of number) { CHECK_EQ(toString(result.errors[0]), R"(Argument count mismatch. Function 'wrapper' expects 3 arguments, but 4 are specified)"); CHECK_EQ(toString(result.errors[1]), R"(Argument count mismatch. Function 'wrapper' expects 3 arguments, but only 2 are specified)"); - CHECK_EQ(toString(result.errors[2]), R"(Type 'string' could not be converted into 'number')"); + if (FFlag::LuauBetterTypeMismatchErrors) + CHECK_EQ(toString(result.errors[2]), R"(Expected this to be 'number', but got 'string')"); + else + CHECK_EQ(toString(result.errors[2]), R"(Type 'string' could not be converted into 'number')"); } } @@ -2094,7 +2113,6 @@ TEST_CASE_FIXTURE(BuiltinsFixture, "table_isfrozen_and_clear_work_on_any_table") TEST_CASE_FIXTURE(Fixture, "cli_179086_dont_ignore_explicit_variadics") { ScopedFastFlag _[] = { - {FFlag::LuauEGFixGenericsList, true}, {FFlag::LuauIncludeExplicitGenericPacks, true}, }; @@ -2118,7 +2136,6 @@ TEST_CASE_FIXTURE(Fixture, "cli_179086_dont_ignore_explicit_variadics") TEST_CASE_FIXTURE(BuiltinsFixture, "oss_2075_generic_packs_should_not_be_dropped") { ScopedFastFlag _[] = { - {FFlag::LuauEGFixGenericsList, true}, {FFlag::LuauIncludeExplicitGenericPacks, true}, }; diff --git a/tests/TypeInfer.intersectionTypes.test.cpp b/tests/TypeInfer.intersectionTypes.test.cpp index 38d938bad..847e278e0 100644 --- a/tests/TypeInfer.intersectionTypes.test.cpp +++ b/tests/TypeInfer.intersectionTypes.test.cpp @@ -10,6 +10,8 @@ using namespace Luau; LUAU_FASTFLAG(LuauSolverV2) +LUAU_FASTFLAG(LuauBetterTypeMismatchErrors) +LUAU_FASTFLAG(LuauCheckFunctionStatementTypes) TEST_SUITE_BEGIN("IntersectionTypes"); @@ -332,6 +334,8 @@ TEST_CASE_FIXTURE(Fixture, "table_intersection_write_sealed") TEST_CASE_FIXTURE(Fixture, "table_intersection_write_sealed_indirect") { + ScopedFastFlag _{FFlag::LuauCheckFunctionStatementTypes, true}; + CheckResult result = check(R"( type X = { x: (number) -> number } type Y = { y: (string) -> string } @@ -345,31 +349,42 @@ TEST_CASE_FIXTURE(Fixture, "table_intersection_write_sealed_indirect") end )"); + LUAU_REQUIRE_ERROR_COUNT(4, result); if (FFlag::LuauSolverV2) { - LUAU_REQUIRE_ERROR_COUNT(3, result); CHECK_EQ(toString(result.errors[0]), "Cannot add property 'z' to table 'X & Y'"); - // I'm not writing this as a `toString` check, those are awful. auto err1 = get(result.errors[1]); REQUIRE(err1); CHECK_EQ("number", toString(err1->givenType)); CHECK_EQ("string", toString(err1->wantedType)); - CHECK_EQ(toString(result.errors[2]), "Cannot add property 'w' to table 'X & Y'"); + auto err2 = get(result.errors[2]); + REQUIRE(err2); + CHECK_EQ("(string, number) -> string", toString(err2->givenType)); + CHECK_EQ("(string) -> string", toString(err2->wantedType)); + CHECK_EQ(toString(result.errors[3]), "Cannot add property 'w' to table 'X & Y'"); } else { - LUAU_REQUIRE_ERROR_COUNT(4, result); - - const std::string expected = "Type\n\t" - "'(string, number) -> string'" - "\ncould not be converted into\n\t" - "'(string) -> string'\n" - "caused by:\n" - " Argument count mismatch. Function expects 2 arguments, but only 1 is specified"; + const std::string expected = FFlag::LuauBetterTypeMismatchErrors + ? "Expected this to be\n\t" + "'(string) -> string'" + "\nbut got\n\t" + "'(string, number) -> string'" + "\ncaused by:\n" + " Argument count mismatch. Function expects 2 arguments, but only 1 is specified" + : "Type\n\t" + "'(string, number) -> string'" + "\ncould not be converted into\n\t" + "'(string) -> string'\n" + "caused by:\n" + " Argument count mismatch. Function expects 2 arguments, but only 1 is specified"; CHECK_EQ(expected, toString(result.errors[0])); CHECK_EQ(toString(result.errors[1]), "Cannot add property 'z' to table 'X & Y'"); - CHECK_EQ(toString(result.errors[2]), "Type 'number' could not be converted into 'string'"); + if (FFlag::LuauBetterTypeMismatchErrors) + CHECK_EQ(toString(result.errors[2]), "Expected this to be 'string', but got 'number'"); + else + CHECK_EQ(toString(result.errors[2]), "Type 'number' could not be converted into 'string'"); CHECK_EQ(toString(result.errors[3]), "Cannot add property 'w' to table 'X & Y'"); } } @@ -391,16 +406,26 @@ TEST_CASE_FIXTURE(Fixture, "table_write_sealed_indirect") )"); LUAU_REQUIRE_ERROR_COUNT(4, result); - const std::string expected = "Type\n\t" - "'(string, number) -> string'" - "\ncould not be converted into\n\t" - "'(string) -> string'\n" - "caused by:\n" - " Argument count mismatch. Function expects 2 arguments, but only 1 is specified"; + const std::string expected = FFlag::LuauBetterTypeMismatchErrors + ? "Expected this to be\n\t" + "'(string) -> string'" + "\nbut got\n\t" + "'(string, number) -> string'" + "\ncaused by:\n" + " Argument count mismatch. Function expects 2 arguments, but only 1 is specified" + : "Type\n\t" + "'(string, number) -> string'" + "\ncould not be converted into\n\t" + "'(string) -> string'\n" + "caused by:\n" + " Argument count mismatch. Function expects 2 arguments, but only 1 is specified"; CHECK_EQ(expected, toString(result.errors[0])); CHECK_EQ(toString(result.errors[1]), "Cannot add property 'z' to table 'XY'"); - CHECK_EQ(toString(result.errors[2]), "Type 'number' could not be converted into 'string'"); + if (FFlag::LuauBetterTypeMismatchErrors) + CHECK_EQ(toString(result.errors[2]), "Expected this to be 'string', but got 'number'"); + else + CHECK_EQ(toString(result.errors[2]), "Type 'number' could not be converted into 'string'"); CHECK_EQ(toString(result.errors[3]), "Cannot add property 'w' to table 'XY'"); } @@ -429,14 +454,29 @@ local a: XYZ = 3 if (FFlag::LuauSolverV2) { - const std::string expected = "Type " - "'number'" - " could not be converted into " - "'X & Y & Z'; \n" - "this is because \n\t" - " * the 1st component of the intersection is `X`, and `number` is not a subtype of `X`\n\t" - " * the 2nd component of the intersection is `Y`, and `number` is not a subtype of `Y`\n\t" - " * the 3rd component of the intersection is `Z`, and `number` is not a subtype of `Z`"; + const std::string expected = FFlag::LuauBetterTypeMismatchErrors + ? "Expected this to be 'X & Y & Z', but got 'number'; \n" + "this is because \n\t" + " * the 1st component of the intersection is `X`, and `number` is not a subtype of `X`\n\t" + " * the 2nd component of the intersection is `Y`, and `number` is not a subtype of `Y`\n\t" + " * the 3rd component of the intersection is `Z`, and `number` is not a subtype of `Z`" + : "Type " + "'number'" + " could not be converted into " + "'X & Y & Z'; \n" + "this is because \n\t" + " * the 1st component of the intersection is `X`, and `number` is not a subtype of `X`\n\t" + " * the 2nd component of the intersection is `Y`, and `number` is not a subtype of `Y`\n\t" + " * the 3rd component of the intersection is `Z`, and `number` is not a subtype of `Z`"; + + CHECK_EQ(expected, toString(result.errors[0])); + } + else if (FFlag::LuauBetterTypeMismatchErrors) + { + const std::string expected = R"(Expected this to be 'X & Y & Z', but got 'number' +caused by: + Not all intersection parts are compatible. +Expected this to be 'X', but got 'number')"; CHECK_EQ(expected, toString(result.errors[0])); } @@ -468,16 +508,24 @@ end if (FFlag::LuauSolverV2) { - const std::string expected = "Type " - "'X & Y & Z'" - " could not be converted into " - "'number'; \n" - "this is because \n\t" - " * the 1st component of the intersection is `X`, which is not a subtype of `number`\n\t" - " * the 2nd component of the intersection is `Y`, which is not a subtype of `number`\n\t" - " * the 3rd component of the intersection is `Z`, which is not a subtype of `number`"; + const std::string expected = FFlag::LuauBetterTypeMismatchErrors + ? "Expected this to be 'number', but got 'X & Y & Z'; \n" + "this is because \n\t" + " * the 1st component of the intersection is `X`, which is not a subtype of `number`\n\t" + " * the 2nd component of the intersection is `Y`, which is not a subtype of `number`\n\t" + " * the 3rd component of the intersection is `Z`, which is not a subtype of `number`" + : "Type " + "'X & Y & Z'" + " could not be converted into " + "'number'; \n" + "this is because \n\t" + " * the 1st component of the intersection is `X`, which is not a subtype of `number`\n\t" + " * the 2nd component of the intersection is `Y`, which is not a subtype of `number`\n\t" + " * the 3rd component of the intersection is `Z`, which is not a subtype of `number`"; CHECK_EQ(expected, toString(result.errors[0])); } + else if (FFlag::LuauBetterTypeMismatchErrors) + CHECK_EQ(toString(result.errors[0]), R"(Expected this to be 'number', but got 'X & Y & Z'; none of the intersection parts are compatible)"); else CHECK_EQ( toString(result.errors[0]), R"(Type 'X & Y & Z' could not be converted into 'number'; none of the intersection parts are compatible)" @@ -525,15 +573,22 @@ TEST_CASE_FIXTURE(Fixture, "intersect_bool_and_false") if (FFlag::LuauSolverV2) { - const std::string expected = "Type " - "'boolean & false'" - " could not be converted into " - "'true'; \n" - "this is because \n\t" - " * the 1st component of the intersection is `boolean`, which is not a subtype of `true`\n\t" - " * the 2nd component of the intersection is `false`, which is not a subtype of `true`"; + const std::string expected = FFlag::LuauBetterTypeMismatchErrors + ? "Expected this to be 'true', but got 'boolean & false'; \n" + "this is because \n\t" + " * the 1st component of the intersection is `boolean`, which is not a subtype of `true`\n\t" + " * the 2nd component of the intersection is `false`, which is not a subtype of `true`" + : "Type " + "'boolean & false'" + " could not be converted into " + "'true'; \n" + "this is because \n\t" + " * the 1st component of the intersection is `boolean`, which is not a subtype of `true`\n\t" + " * the 2nd component of the intersection is `false`, which is not a subtype of `true`"; CHECK_EQ(expected, toString(result.errors[0])); } + else if (FFlag::LuauBetterTypeMismatchErrors) + CHECK_EQ(toString(result.errors[0]), "Expected this to be 'true', but got 'boolean & false'; none of the intersection parts are compatible"); else CHECK_EQ( toString(result.errors[0]), "Type 'boolean & false' could not be converted into 'true'; none of the intersection parts are compatible" @@ -554,16 +609,26 @@ TEST_CASE_FIXTURE(Fixture, "intersect_false_and_bool_and_false") // TODO: odd stringification of `false & (boolean & false)`.) if (FFlag::LuauSolverV2) { - const std::string expected = "Type " - "'boolean & false & false'" - " could not be converted into " - "'true'; \n" - "this is because \n\t" - " * the 1st component of the intersection is `false`, which is not a subtype of `true`\n\t" - " * the 2nd component of the intersection is `boolean`, which is not a subtype of `true`\n\t" - " * the 3rd component of the intersection is `false`, which is not a subtype of `true`"; + const std::string expected = FFlag::LuauBetterTypeMismatchErrors + ? "Expected this to be 'true', but got 'boolean & false & false'; \n" + "this is because \n\t" + " * the 1st component of the intersection is `false`, which is not a subtype of `true`\n\t" + " * the 2nd component of the intersection is `boolean`, which is not a subtype of `true`\n\t" + " * the 3rd component of the intersection is `false`, which is not a subtype of `true`" + : "Type " + "'boolean & false & false'" + " could not be converted into " + "'true'; \n" + "this is because \n\t" + " * the 1st component of the intersection is `false`, which is not a subtype of `true`\n\t" + " * the 2nd component of the intersection is `boolean`, which is not a subtype of `true`\n\t" + " * the 3rd component of the intersection is `false`, which is not a subtype of `true`"; CHECK_EQ(expected, toString(result.errors[0])); } + else if (FFlag::LuauBetterTypeMismatchErrors) + CHECK_EQ( + toString(result.errors[0]), "Expected this to be 'true', but got 'boolean & false & false'; none of the intersection parts are compatible" + ); else CHECK_EQ( toString(result.errors[0]), @@ -583,34 +648,79 @@ TEST_CASE_FIXTURE(Fixture, "intersect_saturate_overloaded_functions") if (FFlag::LuauSolverV2) { const std::string expected1 = - "Type\n\t" - "'((number?) -> number?) & ((string?) -> string?)'" - "\ncould not be converted into\n\t" - "'(nil) -> nil'; \n" - "this is because \n\t" - " * in the 1st component of the intersection, the function returns the 1st entry in the type pack which has the 1st component of the " - "union as `number` and it returns the 1st entry in the type pack is `nil`, and `number` is not a subtype of `nil`\n\t" - " * in the 2nd component of the intersection, the function returns the 1st entry in the type pack which has the 1st component of the " - "union as `string` and it returns the 1st entry in the type pack is `nil`, and `string` is not a subtype of `nil`"; + FFlag::LuauBetterTypeMismatchErrors + ? "Expected this to be\n\t" + "'(nil) -> nil'" + "\nbut got\n\t" + "'((number?) -> number?) & ((string?) -> string?)'" + "; \nthis is because \n\t" + " * in the 1st component of the intersection, the function returns the 1st entry in the type pack which has the 1st component of " + "the " + "union as `number` and it returns the 1st entry in the type pack is `nil`, and `number` is not a subtype of `nil`\n\t" + " * in the 2nd component of the intersection, the function returns the 1st entry in the type pack which has the 1st component of " + "the " + "union as `string` and it returns the 1st entry in the type pack is `nil`, and `string` is not a subtype of `nil`" + : "Type\n\t" + "'((number?) -> number?) & ((string?) -> string?)'" + "\ncould not be converted into\n\t" + "'(nil) -> nil'; \n" + "this is because \n\t" + " * in the 1st component of the intersection, the function returns the 1st entry in the type pack which has the 1st component of " + "the " + "union as `number` and it returns the 1st entry in the type pack is `nil`, and `number` is not a subtype of `nil`\n\t" + " * in the 2nd component of the intersection, the function returns the 1st entry in the type pack which has the 1st component of " + "the " + "union as `string` and it returns the 1st entry in the type pack is `nil`, and `string` is not a subtype of `nil`"; const std::string expected2 = - "Type\n\t" - "'((number?) -> number?) & ((string?) -> string?)'" - "\ncould not be converted into\n\t" - "'(number) -> number'; \n" - "this is because \n\t" - " * in the 1st component of the intersection, the function returns the 1st entry in the type pack which has the 2nd component of the " - "union as `nil` and it returns the 1st entry in the type pack is `number`, and `nil` is not a subtype of `number`\n\t" - " * in the 2nd component of the intersection, the function returns the 1st entry in the type pack which has the 1st component of the " - "union as `string` and it returns the 1st entry in the type pack is `number`, and `string` is not a subtype of `number`\n\t" - " * in the 2nd component of the intersection, the function returns the 1st entry in the type pack which has the 2nd component of the " - "union as `nil` and it returns the 1st entry in the type pack is `number`, and `nil` is not a subtype of `number`\n\t" - " * in the 2nd component of the intersection, the function takes the 1st entry in the type pack which is `string?` and it takes the 1st " - "entry in the type pack is `number`, and `string?` is not a supertype of `number`"; + FFlag::LuauBetterTypeMismatchErrors + ? "Expected this to be\n\t" + "'(number) -> number'" + "\nbut got\n\t" + "'((number?) -> number?) & ((string?) -> string?)'" + "; \nthis is because \n\t" + " * in the 1st component of the intersection, the function returns the 1st entry in the type pack which has the 2nd component of " + "the " + "union as `nil` and it returns the 1st entry in the type pack is `number`, and `nil` is not a subtype of `number`\n\t" + " * in the 2nd component of the intersection, the function returns the 1st entry in the type pack which has the 1st component of " + "the " + "union as `string` and it returns the 1st entry in the type pack is `number`, and `string` is not a subtype of `number`\n\t" + " * in the 2nd component of the intersection, the function returns the 1st entry in the type pack which has the 2nd component of " + "the " + "union as `nil` and it returns the 1st entry in the type pack is `number`, and `nil` is not a subtype of `number`\n\t" + " * in the 2nd component of the intersection, the function takes the 1st entry in the type pack which is `string?` and it takes " + "the 1st " + "entry in the type pack is `number`, and `string?` is not a supertype of `number`" + : "Type\n\t" + "'((number?) -> number?) & ((string?) -> string?)'" + "\ncould not be converted into\n\t" + "'(number) -> number'; \n" + "this is because \n\t" + " * in the 1st component of the intersection, the function returns the 1st entry in the type pack which has the 2nd component of " + "the " + "union as `nil` and it returns the 1st entry in the type pack is `number`, and `nil` is not a subtype of `number`\n\t" + " * in the 2nd component of the intersection, the function returns the 1st entry in the type pack which has the 1st component of " + "the " + "union as `string` and it returns the 1st entry in the type pack is `number`, and `string` is not a subtype of `number`\n\t" + " * in the 2nd component of the intersection, the function returns the 1st entry in the type pack which has the 2nd component of " + "the " + "union as `nil` and it returns the 1st entry in the type pack is `number`, and `nil` is not a subtype of `number`\n\t" + " * in the 2nd component of the intersection, the function takes the 1st entry in the type pack which is `string?` and it takes " + "the 1st " + "entry in the type pack is `number`, and `string?` is not a supertype of `number`"; CHECK_EQ(expected1, toString(result.errors[0])); CHECK_EQ(expected2, toString(result.errors[1])); } + else if (FFlag::LuauBetterTypeMismatchErrors) + { + LUAU_REQUIRE_ERROR_COUNT(1, result); + const std::string expected = R"(Expected this to be + '(number) -> number' +but got + '((number?) -> number?) & ((string?) -> string?)'; none of the intersection parts are compatible)"; + CHECK_EQ(expected, toString(result.errors[0])); + } else { LUAU_REQUIRE_ERROR_COUNT(1, result); @@ -636,10 +746,16 @@ TEST_CASE_FIXTURE(Fixture, "union_saturate_overloaded_functions") LUAU_REQUIRE_ERROR_COUNT(1, result); - const std::string expected = "Type\n\t" - "'((number) -> number) & ((string) -> string)'" - "\ncould not be converted into\n\t" - "'(boolean | number) -> boolean | number'; none of the intersection parts are compatible"; + const std::string expected = FFlag::LuauBetterTypeMismatchErrors + ? "Expected this to be\n\t" + "'(boolean | number) -> boolean | number'" + "\nbut got\n\t" + "'((number) -> number) & ((string) -> string)'" + "; none of the intersection parts are compatible" + : "Type\n\t" + "'((number) -> number) & ((string) -> string)'" + "\ncould not be converted into\n\t" + "'(boolean | number) -> boolean | number'; none of the intersection parts are compatible"; CHECK_EQ(expected, toString(result.errors[0])); } @@ -656,15 +772,29 @@ TEST_CASE_FIXTURE(Fixture, "intersection_of_tables") if (FFlag::LuauSolverV2) { - const std::string expected = "Type " - "'{ p: number?, q: number?, r: number? } & { p: number?, q: string? }'" - " could not be converted into " - "'{ p: nil }'; \n" - "this is because \n\t" - " * in the 1st component of the intersection, accessing `p` has the 1st component of the union as `number` and " - "accessing `p` results in `nil`, and `number` is not exactly `nil`\n\t" - " * in the 2nd component of the intersection, accessing `p` has the 1st component of the union as `number` and " - "accessing `p` results in `nil`, and `number` is not exactly `nil`"; + const std::string expected = + FFlag::LuauBetterTypeMismatchErrors + ? "Expected this to be '{ p: nil }', but got '{ p: number?, q: number?, r: number? } & { p: number?, q: string? }'" + "; \nthis is because \n\t" + " * in the 1st component of the intersection, accessing `p` has the 1st component of the union as `number` and " + "accessing `p` results in `nil`, and `number` is not exactly `nil`\n\t" + " * in the 2nd component of the intersection, accessing `p` has the 1st component of the union as `number` and " + "accessing `p` results in `nil`, and `number` is not exactly `nil`" + : "Type " + "'{ p: number?, q: number?, r: number? } & { p: number?, q: string? }'" + " could not be converted into " + "'{ p: nil }'; \n" + "this is because \n\t" + " * in the 1st component of the intersection, accessing `p` has the 1st component of the union as `number` and " + "accessing `p` results in `nil`, and `number` is not exactly `nil`\n\t" + " * in the 2nd component of the intersection, accessing `p` has the 1st component of the union as `number` and " + "accessing `p` results in `nil`, and `number` is not exactly `nil`"; + CHECK_EQ(expected, toString(result.errors[0])); + } + else if (FFlag::LuauBetterTypeMismatchErrors) + { + const std::string expected = + R"(Expected this to be '{ p: nil }', but got '{ p: number?, q: number?, r: number? } & { p: number?, q: string? }'; none of the intersection parts are compatible)"; CHECK_EQ(expected, toString(result.errors[0])); } else @@ -686,23 +816,51 @@ TEST_CASE_FIXTURE(Fixture, "intersection_of_tables_with_top_properties") if (FFlag::LuauSolverV2) { - const std::string expected = "Type\n\t" - "'{ p: number?, q: any } & { p: unknown, q: string? }'" - "\ncould not be converted into\n\t" - "'{ p: string?, q: number? }'; \n" - "this is because \n\t" - " * in the 1st component of the intersection, accessing `p` has the 1st component of the union as `number` and " - "accessing `p` results in `string?`, and `number` is not exactly `string?`\n\t" - " * in the 1st component of the intersection, accessing `p` results in `number?` and accessing `p` has the 1st " - "component of the union as `string`, and `number?` is not exactly `string`\n\t" - " * in the 1st component of the intersection, accessing `q` results in `any` and accessing `q` results in " - "`number?`, and `any` is not exactly `number?`\n\t" - " * in the 2nd component of the intersection, accessing `p` results in `unknown` and accessing `p` results in " - "`string?`, and `unknown` is not exactly `string?`\n\t" - " * in the 2nd component of the intersection, accessing `q` has the 1st component of the union as `string` and " - "accessing `q` results in `number?`, and `string` is not exactly `number?`\n\t" - " * in the 2nd component of the intersection, accessing `q` results in `string?` and accessing `q` has the 1st " - "component of the union as `number`, and `string?` is not exactly `number`"; + const std::string expected = + FFlag::LuauBetterTypeMismatchErrors + ? "Expected this to be\n\t" + "'{ p: string?, q: number? }'" + "\nbut got\n\t" + "'{ p: number?, q: any } & { p: unknown, q: string? }'" + "; \nthis is because \n\t" + " * in the 1st component of the intersection, accessing `p` has the 1st component of the union as `number` and " + "accessing `p` results in `string?`, and `number` is not exactly `string?`\n\t" + " * in the 1st component of the intersection, accessing `p` results in `number?` and accessing `p` has the 1st " + "component of the union as `string`, and `number?` is not exactly `string`\n\t" + " * in the 1st component of the intersection, accessing `q` results in `any` and accessing `q` results in " + "`number?`, and `any` is not exactly `number?`\n\t" + " * in the 2nd component of the intersection, accessing `p` results in `unknown` and accessing `p` results in " + "`string?`, and `unknown` is not exactly `string?`\n\t" + " * in the 2nd component of the intersection, accessing `q` has the 1st component of the union as `string` and " + "accessing `q` results in `number?`, and `string` is not exactly `number?`\n\t" + " * in the 2nd component of the intersection, accessing `q` results in `string?` and accessing `q` has the 1st " + "component of the union as `number`, and `string?` is not exactly `number`" + : "Type\n\t" + "'{ p: number?, q: any } & { p: unknown, q: string? }'" + "\ncould not be converted into\n\t" + "'{ p: string?, q: number? }'; \n" + "this is because \n\t" + " * in the 1st component of the intersection, accessing `p` has the 1st component of the union as `number` and " + "accessing `p` results in `string?`, and `number` is not exactly `string?`\n\t" + " * in the 1st component of the intersection, accessing `p` results in `number?` and accessing `p` has the 1st " + "component of the union as `string`, and `number?` is not exactly `string`\n\t" + " * in the 1st component of the intersection, accessing `q` results in `any` and accessing `q` results in " + "`number?`, and `any` is not exactly `number?`\n\t" + " * in the 2nd component of the intersection, accessing `p` results in `unknown` and accessing `p` results in " + "`string?`, and `unknown` is not exactly `string?`\n\t" + " * in the 2nd component of the intersection, accessing `q` has the 1st component of the union as `string` and " + "accessing `q` results in `number?`, and `string` is not exactly `number?`\n\t" + " * in the 2nd component of the intersection, accessing `q` results in `string?` and accessing `q` has the 1st " + "component of the union as `number`, and `string?` is not exactly `number`"; + CHECK_EQ(expected, toString(result.errors[0])); + } + else if (FFlag::LuauBetterTypeMismatchErrors) + { + LUAU_REQUIRE_ERROR_COUNT(1, result); + const std::string expected = R"(Expected this to be + '{ p: string?, q: number? }' +but got + '{ p: number?, q: any } & { p: unknown, q: string? }'; none of the intersection parts are compatible)"; CHECK_EQ(expected, toString(result.errors[0])); } else @@ -740,48 +898,115 @@ TEST_CASE_FIXTURE(Fixture, "overloaded_functions_returning_intersections") if (FFlag::LuauSolverV2) { const std::string expected1 = - "Type\n\t" - "'((number?) -> { p: number } & { q: number }) & ((string?) -> { p: number } & { r: number })'" - "\ncould not be converted into\n\t" - "'(nil) -> { p: number, q: number, r: number }'; \n" - "this is because \n\t" - " * in the 1st component of the intersection, the function returns the 1st entry in the type pack which has the 1st component of the " - "intersection as `{ p: number }` and it returns the 1st entry in the type pack is `{ p: number, q: number, r: number }`, and `{ p: " - "number }` is not a subtype of `{ p: number, q: number, r: number }`\n\t" - " * in the 1st component of the intersection, the function returns the 1st entry in the type pack which has the 2nd component of the " - "intersection as `{ q: number }` and it returns the 1st entry in the type pack is `{ p: number, q: number, r: number }`, and `{ q: " - "number }` is not a subtype of `{ p: number, q: number, r: number }`\n\t" - " * in the 2nd component of the intersection, the function returns the 1st entry in the type pack which has the 1st component of the " - "intersection as `{ p: number }` and it returns the 1st entry in the type pack is `{ p: number, q: number, r: number }`, and `{ p: " - "number }` is not a subtype of `{ p: number, q: number, r: number }`\n\t" - " * in the 2nd component of the intersection, the function returns the 1st entry in the type pack which has the 2nd component of the " - "intersection as `{ r: number }` and it returns the 1st entry in the type pack is `{ p: number, q: number, r: number }`, and `{ r: " - "number }` is not a subtype of `{ p: number, q: number, r: number }`"; + FFlag::LuauBetterTypeMismatchErrors + ? "Expected this to be\n\t" + "'(nil) -> { p: number, q: number, r: number }'" + "\nbut got\n\t" + "'((number?) -> { p: number } & { q: number }) & ((string?) -> { p: number } & { r: number })'" + "; \nthis is because \n\t" + " * in the 1st component of the intersection, the function returns the 1st entry in the type pack which has the 1st component of " + "the " + "intersection as `{ p: number }` and it returns the 1st entry in the type pack is `{ p: number, q: number, r: number }`, and `{ p: " + "number }` is not a subtype of `{ p: number, q: number, r: number }`\n\t" + " * in the 1st component of the intersection, the function returns the 1st entry in the type pack which has the 2nd component of " + "the " + "intersection as `{ q: number }` and it returns the 1st entry in the type pack is `{ p: number, q: number, r: number }`, and `{ q: " + "number }` is not a subtype of `{ p: number, q: number, r: number }`\n\t" + " * in the 2nd component of the intersection, the function returns the 1st entry in the type pack which has the 1st component of " + "the " + "intersection as `{ p: number }` and it returns the 1st entry in the type pack is `{ p: number, q: number, r: number }`, and `{ p: " + "number }` is not a subtype of `{ p: number, q: number, r: number }`\n\t" + " * in the 2nd component of the intersection, the function returns the 1st entry in the type pack which has the 2nd component of " + "the " + "intersection as `{ r: number }` and it returns the 1st entry in the type pack is `{ p: number, q: number, r: number }`, and `{ r: " + "number }` is not a subtype of `{ p: number, q: number, r: number }`" + : "Type\n\t" + "'((number?) -> { p: number } & { q: number }) & ((string?) -> { p: number } & { r: number })'" + "\ncould not be converted into\n\t" + "'(nil) -> { p: number, q: number, r: number }'; \n" + "this is because \n\t" + " * in the 1st component of the intersection, the function returns the 1st entry in the type pack which has the 1st component of " + "the " + "intersection as `{ p: number }` and it returns the 1st entry in the type pack is `{ p: number, q: number, r: number }`, and `{ p: " + "number }` is not a subtype of `{ p: number, q: number, r: number }`\n\t" + " * in the 1st component of the intersection, the function returns the 1st entry in the type pack which has the 2nd component of " + "the " + "intersection as `{ q: number }` and it returns the 1st entry in the type pack is `{ p: number, q: number, r: number }`, and `{ q: " + "number }` is not a subtype of `{ p: number, q: number, r: number }`\n\t" + " * in the 2nd component of the intersection, the function returns the 1st entry in the type pack which has the 1st component of " + "the " + "intersection as `{ p: number }` and it returns the 1st entry in the type pack is `{ p: number, q: number, r: number }`, and `{ p: " + "number }` is not a subtype of `{ p: number, q: number, r: number }`\n\t" + " * in the 2nd component of the intersection, the function returns the 1st entry in the type pack which has the 2nd component of " + "the " + "intersection as `{ r: number }` and it returns the 1st entry in the type pack is `{ p: number, q: number, r: number }`, and `{ r: " + "number }` is not a subtype of `{ p: number, q: number, r: number }`"; const std::string expected2 = - "Type\n\t" - "'((number?) -> { p: number } & { q: number }) & ((string?) -> { p: number } & { r: number })'" - "\ncould not be converted into\n\t" - "'(number?) -> { p: number, q: number, r: number }'; \n" - "this is because \n\t" - " * in the 1st component of the intersection, the function returns the 1st entry in the type pack which has the 1st component of the " - "intersection as `{ p: number }` and it returns the 1st entry in the type pack is `{ p: number, q: number, r: number }`, and `{ p: " - "number }` is not a subtype of `{ p: number, q: number, r: number }`\n\t" - " * in the 1st component of the intersection, the function returns the 1st entry in the type pack which has the 2nd component of the " - "intersection as `{ q: number }` and it returns the 1st entry in the type pack is `{ p: number, q: number, r: number }`, and `{ q: " - "number }` is not a subtype of `{ p: number, q: number, r: number }`\n\t" - " * in the 2nd component of the intersection, the function returns the 1st entry in the type pack which has the 1st component of the " - "intersection as `{ p: number }` and it returns the 1st entry in the type pack is `{ p: number, q: number, r: number }`, and `{ p: " - "number }` is not a subtype of `{ p: number, q: number, r: number }`\n\t" - " * in the 2nd component of the intersection, the function returns the 1st entry in the type pack which has the 2nd component of the " - "intersection as `{ r: number }` and it returns the 1st entry in the type pack is `{ p: number, q: number, r: number }`, and `{ r: " - "number }` is not a subtype of `{ p: number, q: number, r: number }`\n\t" - " * in the 2nd component of the intersection, the function takes the 1st entry in the type pack which is `string?` and it takes the 1st " - "entry in the type pack has the 1st component of the union as `number`, and `string?` is not a supertype of `number`"; + FFlag::LuauBetterTypeMismatchErrors + ? "Expected this to be\n\t" + "'(number?) -> { p: number, q: number, r: number }'" + "\nbut got\n\t" + "'((number?) -> { p: number } & { q: number }) & ((string?) -> { p: number } & { r: number })'" + "; \nthis is because \n\t" + " * in the 1st component of the intersection, the function returns the 1st entry in the type pack which has the 1st component of " + "the " + "intersection as `{ p: number }` and it returns the 1st entry in the type pack is `{ p: number, q: number, r: number }`, and `{ p: " + "number }` is not a subtype of `{ p: number, q: number, r: number }`\n\t" + " * in the 1st component of the intersection, the function returns the 1st entry in the type pack which has the 2nd component of " + "the " + "intersection as `{ q: number }` and it returns the 1st entry in the type pack is `{ p: number, q: number, r: number }`, and `{ q: " + "number }` is not a subtype of `{ p: number, q: number, r: number }`\n\t" + " * in the 2nd component of the intersection, the function returns the 1st entry in the type pack which has the 1st component of " + "the " + "intersection as `{ p: number }` and it returns the 1st entry in the type pack is `{ p: number, q: number, r: number }`, and `{ p: " + "number }` is not a subtype of `{ p: number, q: number, r: number }`\n\t" + " * in the 2nd component of the intersection, the function returns the 1st entry in the type pack which has the 2nd component of " + "the " + "intersection as `{ r: number }` and it returns the 1st entry in the type pack is `{ p: number, q: number, r: number }`, and `{ r: " + "number }` is not a subtype of `{ p: number, q: number, r: number }`\n\t" + " * in the 2nd component of the intersection, the function takes the 1st entry in the type pack which is `string?` and it takes " + "the 1st " + "entry in the type pack has the 1st component of the union as `number`, and `string?` is not a supertype of `number`" + : "Type\n\t" + "'((number?) -> { p: number } & { q: number }) & ((string?) -> { p: number } & { r: number })'" + "\ncould not be converted into\n\t" + "'(number?) -> { p: number, q: number, r: number }'; \n" + "this is because \n\t" + " * in the 1st component of the intersection, the function returns the 1st entry in the type pack which has the 1st component of " + "the " + "intersection as `{ p: number }` and it returns the 1st entry in the type pack is `{ p: number, q: number, r: number }`, and `{ p: " + "number }` is not a subtype of `{ p: number, q: number, r: number }`\n\t" + " * in the 1st component of the intersection, the function returns the 1st entry in the type pack which has the 2nd component of " + "the " + "intersection as `{ q: number }` and it returns the 1st entry in the type pack is `{ p: number, q: number, r: number }`, and `{ q: " + "number }` is not a subtype of `{ p: number, q: number, r: number }`\n\t" + " * in the 2nd component of the intersection, the function returns the 1st entry in the type pack which has the 1st component of " + "the " + "intersection as `{ p: number }` and it returns the 1st entry in the type pack is `{ p: number, q: number, r: number }`, and `{ p: " + "number }` is not a subtype of `{ p: number, q: number, r: number }`\n\t" + " * in the 2nd component of the intersection, the function returns the 1st entry in the type pack which has the 2nd component of " + "the " + "intersection as `{ r: number }` and it returns the 1st entry in the type pack is `{ p: number, q: number, r: number }`, and `{ r: " + "number }` is not a subtype of `{ p: number, q: number, r: number }`\n\t" + " * in the 2nd component of the intersection, the function takes the 1st entry in the type pack which is `string?` and it takes " + "the 1st " + "entry in the type pack has the 1st component of the union as `number`, and `string?` is not a supertype of `number`"; CHECK_EQ(expected1, toString(result.errors[0])); CHECK_EQ(expected2, toString(result.errors[1])); } + else if (FFlag::LuauBetterTypeMismatchErrors) + { + LUAU_REQUIRE_ERROR_COUNT(1, result); + CHECK_EQ( + R"(Expected this to be + '(number?) -> { p: number, q: number, r: number }' +but got + '((number?) -> { p: number } & { q: number }) & ((string?) -> { p: number } & { r: number })'; none of the intersection parts are compatible)", + toString(result.errors[0]) + ); + } else { LUAU_REQUIRE_ERROR_COUNT(1, result); @@ -809,6 +1034,15 @@ TEST_CASE_FIXTURE(Fixture, "overloaded_functions_mentioning_generic") { LUAU_REQUIRE_ERROR_COUNT(0, result); } + else if (FFlag::LuauBetterTypeMismatchErrors) + { + LUAU_REQUIRE_ERROR_COUNT(1, result); + const std::string expected = R"(Expected this to be + '(number?) -> a' +but got + '((number?) -> a | number) & ((string?) -> a | string)'; none of the intersection parts are compatible)"; + CHECK_EQ(expected, toString(result.errors[0])); + } else { LUAU_REQUIRE_ERROR_COUNT(1, result); @@ -836,6 +1070,15 @@ TEST_CASE_FIXTURE(Fixture, "overloaded_functions_mentioning_generics") { LUAU_REQUIRE_NO_ERRORS(result); } + else if (FFlag::LuauBetterTypeMismatchErrors) + { + LUAU_REQUIRE_ERROR_COUNT(1, result); + const std::string expected = R"(Expected this to be + '(a?) -> (a & c) | b' +but got + '((a?) -> a | b) & ((c?) -> b | c)'; none of the intersection parts are compatible)"; + CHECK_EQ(expected, toString(result.errors[0])); + } else { LUAU_REQUIRE_ERROR_COUNT(1, result); @@ -871,38 +1114,91 @@ TEST_CASE_FIXTURE(Fixture, "overloaded_functions_mentioning_generic_packs") CHECK_EQ(toString(tm2->givenType), "((number?, a...) -> (number?, b...)) & ((string?, a...) -> (string?, b...))"); const std::string expected1 = - "Type\n\t" - "'((number?, a...) -> (number?, b...)) & ((string?, a...) -> (string?, b...))'" - "\ncould not be converted into\n\t" - "'(nil, a...) -> (nil, b...)'; \n" - "this is because \n\t" - " * in the 1st component of the intersection, the function returns the 1st entry in the type pack which has the 1st component of the " - "union as `number` and it returns the 1st entry in the type pack is `nil`, and `number` is not a subtype of `nil`\n\t" - " * in the 2nd component of the intersection, the function returns the 1st entry in the type pack which has the 1st component of the " - "union as `string` and it returns the 1st entry in the type pack is `nil`, and `string` is not a subtype of `nil`"; + FFlag::LuauBetterTypeMismatchErrors + ? "Expected this to be\n\t" + "'(nil, a...) -> (nil, b...)'" + "\nbut got\n\t" + "'((number?, a...) -> (number?, b...)) & ((string?, a...) -> (string?, b...))'" + "; \nthis is because \n\t" + " * in the 1st component of the intersection, the function returns the 1st entry in the type pack which has the 1st component of " + "the " + "union as `number` and it returns the 1st entry in the type pack is `nil`, and `number` is not a subtype of `nil`\n\t" + " * in the 2nd component of the intersection, the function returns the 1st entry in the type pack which has the 1st component of " + "the " + "union as `string` and it returns the 1st entry in the type pack is `nil`, and `string` is not a subtype of `nil`" + : "Type\n\t" + "'((number?, a...) -> (number?, b...)) & ((string?, a...) -> (string?, b...))'" + "\ncould not be converted into\n\t" + "'(nil, a...) -> (nil, b...)'; \n" + "this is because \n\t" + " * in the 1st component of the intersection, the function returns the 1st entry in the type pack which has the 1st component of " + "the " + "union as `number` and it returns the 1st entry in the type pack is `nil`, and `number` is not a subtype of `nil`\n\t" + " * in the 2nd component of the intersection, the function returns the 1st entry in the type pack which has the 1st component of " + "the " + "union as `string` and it returns the 1st entry in the type pack is `nil`, and `string` is not a subtype of `nil`"; const std::string expected2 = - "Type\n\t" - "'((number?, a...) -> (number?, b...)) & ((string?, a...) -> (string?, b...))'" - "\ncould not be converted into\n\t" - "'(nil, b...) -> (nil, a...)'; \n" - "this is because \n\t" - " * in the 1st component of the intersection, the function returns a tail of `b...` and it returns a tail of `a...`, and `b...` is not a " - "subtype of `a...`\n\t" - " * in the 1st component of the intersection, the function returns the 1st entry in the type pack which has the 1st component of the " - "union as `number` and it returns the 1st entry in the type pack is `nil`, and `number` is not a subtype of `nil`\n\t" - " * in the 1st component of the intersection, the function takes a tail of `a...` and it takes a tail of `b...`, and `a...` is not a " - "supertype of `b...`\n\t" - " * in the 2nd component of the intersection, the function returns a tail of `b...` and it returns a tail of `a...`, and `b...` is not a " - "subtype of `a...`\n\t" - " * in the 2nd component of the intersection, the function returns the 1st entry in the type pack which has the 1st component of the " - "union as `string` and it returns the 1st entry in the type pack is `nil`, and `string` is not a subtype of `nil`\n\t" - " * in the 2nd component of the intersection, the function takes a tail of `a...` and it takes a tail of `b...`, and `a...` is not a " - "supertype of `b...`"; + FFlag::LuauBetterTypeMismatchErrors + ? "Expected this to be\n\t" + "'(nil, b...) -> (nil, a...)'" + "\nbut got\n\t" + "'((number?, a...) -> (number?, b...)) & ((string?, a...) -> (string?, b...))'" + "; \nthis is because \n\t" + " * in the 1st component of the intersection, the function returns a tail of `b...` and it returns a tail of `a...`, and `b...` is " + "not a " + "subtype of `a...`\n\t" + " * in the 1st component of the intersection, the function returns the 1st entry in the type pack which has the 1st component of " + "the " + "union as `number` and it returns the 1st entry in the type pack is `nil`, and `number` is not a subtype of `nil`\n\t" + " * in the 1st component of the intersection, the function takes a tail of `a...` and it takes a tail of `b...`, and `a...` is not " + "a " + "supertype of `b...`\n\t" + " * in the 2nd component of the intersection, the function returns a tail of `b...` and it returns a tail of `a...`, and `b...` is " + "not a " + "subtype of `a...`\n\t" + " * in the 2nd component of the intersection, the function returns the 1st entry in the type pack which has the 1st component of " + "the " + "union as `string` and it returns the 1st entry in the type pack is `nil`, and `string` is not a subtype of `nil`\n\t" + " * in the 2nd component of the intersection, the function takes a tail of `a...` and it takes a tail of `b...`, and `a...` is not " + "a " + "supertype of `b...`" + : "Type\n\t" + "'((number?, a...) -> (number?, b...)) & ((string?, a...) -> (string?, b...))'" + "\ncould not be converted into\n\t" + "'(nil, b...) -> (nil, a...)'; \n" + "this is because \n\t" + " * in the 1st component of the intersection, the function returns a tail of `b...` and it returns a tail of `a...`, and `b...` is " + "not a " + "subtype of `a...`\n\t" + " * in the 1st component of the intersection, the function returns the 1st entry in the type pack which has the 1st component of " + "the " + "union as `number` and it returns the 1st entry in the type pack is `nil`, and `number` is not a subtype of `nil`\n\t" + " * in the 1st component of the intersection, the function takes a tail of `a...` and it takes a tail of `b...`, and `a...` is not " + "a " + "supertype of `b...`\n\t" + " * in the 2nd component of the intersection, the function returns a tail of `b...` and it returns a tail of `a...`, and `b...` is " + "not a " + "subtype of `a...`\n\t" + " * in the 2nd component of the intersection, the function returns the 1st entry in the type pack which has the 1st component of " + "the " + "union as `string` and it returns the 1st entry in the type pack is `nil`, and `string` is not a subtype of `nil`\n\t" + " * in the 2nd component of the intersection, the function takes a tail of `a...` and it takes a tail of `b...`, and `a...` is not " + "a " + "supertype of `b...`"; CHECK_EQ(expected1, toString(result.errors[0])); CHECK_EQ(expected2, toString(result.errors[1])); } + else if (FFlag::LuauBetterTypeMismatchErrors) + { + LUAU_REQUIRE_ERROR_COUNT(1, result); + const std::string expected = R"(Expected this to be + '(nil, b...) -> (nil, a...)' +but got + '((number?, a...) -> (number?, b...)) & ((string?, a...) -> (string?, b...))'; none of the intersection parts are compatible)"; + CHECK_EQ(expected, toString(result.errors[0])); + } else { LUAU_REQUIRE_ERROR_COUNT(1, result); @@ -930,10 +1226,15 @@ TEST_CASE_FIXTURE(Fixture, "overloadeded_functions_with_unknown_result") LUAU_REQUIRE_ERROR_COUNT(1, result); - const std::string expected = "Type\n\t" - "'((nil) -> unknown) & ((number) -> number)'" - "\ncould not be converted into\n\t" - "'(number?) -> number?'; none of the intersection parts are compatible"; + const std::string expected = FFlag::LuauBetterTypeMismatchErrors ? "Expected this to be\n\t" + "'(number?) -> number?'" + "\nbut got\n\t" + "'((nil) -> unknown) & ((number) -> number)'" + "; none of the intersection parts are compatible" + : "Type\n\t" + "'((nil) -> unknown) & ((number) -> number)'" + "\ncould not be converted into\n\t" + "'(number?) -> number?'; none of the intersection parts are compatible"; CHECK_EQ(expected, toString(result.errors[0])); } @@ -953,10 +1254,15 @@ TEST_CASE_FIXTURE(Fixture, "overloadeded_functions_with_unknown_arguments") LUAU_REQUIRE_ERROR_COUNT(1, result); - const std::string expected = "Type\n\t" - "'((number) -> number?) & ((unknown) -> string?)'" - "\ncould not be converted into\n\t" - "'(number?) -> nil'; none of the intersection parts are compatible"; + const std::string expected = FFlag::LuauBetterTypeMismatchErrors ? "Expected this to be\n\t" + "'(number?) -> nil'" + "\nbut got\n\t" + "'((number) -> number?) & ((unknown) -> string?)'" + "; none of the intersection parts are compatible" + : "Type\n\t" + "'((number) -> number?) & ((unknown) -> string?)'" + "\ncould not be converted into\n\t" + "'(number?) -> nil'; none of the intersection parts are compatible"; CHECK_EQ(expected, toString(result.errors[0])); } @@ -974,32 +1280,73 @@ TEST_CASE_FIXTURE(Fixture, "overloadeded_functions_with_never_result") if (FFlag::LuauSolverV2) { const std::string expected1 = - "Type\n\t" - "'((nil) -> never) & ((number) -> number)'" - "\ncould not be converted into\n\t" - "'(number?) -> number'; \n" - "this is because \n\t" - " * in the 1st component of the intersection, the function takes the 1st entry in the type pack which is `number` and it takes the 1st " - "entry in the type pack has the 2nd component of the union as `nil`, and `number` is not a supertype of `nil`\n\t" - " * in the 2nd component of the intersection, the function takes the 1st entry in the type pack which is `nil` and it takes the 1st " - "entry in the type pack has the 1st component of the union as `number`, and `nil` is not a supertype of `number`"; + FFlag::LuauBetterTypeMismatchErrors + ? "Expected this to be\n\t" + "'(number?) -> number'" + "\nbut got\n\t" + "'((nil) -> never) & ((number) -> number)'" + "; \nthis is because \n\t" + " * in the 1st component of the intersection, the function takes the 1st entry in the type pack which is `number` and it takes the " + "1st " + "entry in the type pack has the 2nd component of the union as `nil`, and `number` is not a supertype of `nil`\n\t" + " * in the 2nd component of the intersection, the function takes the 1st entry in the type pack which is `nil` and it takes the " + "1st " + "entry in the type pack has the 1st component of the union as `number`, and `nil` is not a supertype of `number`" + : "Type\n\t" + "'((nil) -> never) & ((number) -> number)'" + "\ncould not be converted into\n\t" + "'(number?) -> number'; \n" + "this is because \n\t" + " * in the 1st component of the intersection, the function takes the 1st entry in the type pack which is `number` and it takes the " + "1st " + "entry in the type pack has the 2nd component of the union as `nil`, and `number` is not a supertype of `nil`\n\t" + " * in the 2nd component of the intersection, the function takes the 1st entry in the type pack which is `nil` and it takes the " + "1st " + "entry in the type pack has the 1st component of the union as `number`, and `nil` is not a supertype of `number`"; const std::string expected2 = - "Type\n\t" - "'((nil) -> never) & ((number) -> number)'" - "\ncould not be converted into\n\t" - "'(number?) -> never'; \n" - "this is because \n\t" - " * in the 1st component of the intersection, the function returns the 1st entry in the type pack which is `number` and it returns the " - "1st entry in the type pack is `never`, and `number` is not a subtype of `never`\n\t" - " * in the 1st component of the intersection, the function takes the 1st entry in the type pack which is `number` and it takes the 1st " - "entry in the type pack has the 2nd component of the union as `nil`, and `number` is not a supertype of `nil`\n\t" - " * in the 2nd component of the intersection, the function takes the 1st entry in the type pack which is `nil` and it takes the 1st " - "entry in the type pack has the 1st component of the union as `number`, and `nil` is not a supertype of `number`"; + FFlag::LuauBetterTypeMismatchErrors + ? "Expected this to be\n\t" + "'(number?) -> never'" + "\nbut got\n\t" + "'((nil) -> never) & ((number) -> number)'" + "; \nthis is because \n\t" + " * in the 1st component of the intersection, the function returns the 1st entry in the type pack which is `number` and it returns " + "the " + "1st entry in the type pack is `never`, and `number` is not a subtype of `never`\n\t" + " * in the 1st component of the intersection, the function takes the 1st entry in the type pack which is `number` and it takes the " + "1st " + "entry in the type pack has the 2nd component of the union as `nil`, and `number` is not a supertype of `nil`\n\t" + " * in the 2nd component of the intersection, the function takes the 1st entry in the type pack which is `nil` and it takes the " + "1st " + "entry in the type pack has the 1st component of the union as `number`, and `nil` is not a supertype of `number`" + : "Type\n\t" + "'((nil) -> never) & ((number) -> number)'" + "\ncould not be converted into\n\t" + "'(number?) -> never'; \n" + "this is because \n\t" + " * in the 1st component of the intersection, the function returns the 1st entry in the type pack which is `number` and it returns " + "the " + "1st entry in the type pack is `never`, and `number` is not a subtype of `never`\n\t" + " * in the 1st component of the intersection, the function takes the 1st entry in the type pack which is `number` and it takes the " + "1st " + "entry in the type pack has the 2nd component of the union as `nil`, and `number` is not a supertype of `nil`\n\t" + " * in the 2nd component of the intersection, the function takes the 1st entry in the type pack which is `nil` and it takes the " + "1st " + "entry in the type pack has the 1st component of the union as `number`, and `nil` is not a supertype of `number`"; CHECK_EQ(expected1, toString(result.errors[0])); CHECK_EQ(expected2, toString(result.errors[1])); } + else if (FFlag::LuauBetterTypeMismatchErrors) + { + LUAU_REQUIRE_ERROR_COUNT(1, result); + const std::string expected = R"(Expected this to be + '(number?) -> never' +but got + '((nil) -> never) & ((number) -> number)'; none of the intersection parts are compatible)"; + CHECK_EQ(expected, toString(result.errors[0])); + } else { LUAU_REQUIRE_ERROR_COUNT(1, result); @@ -1025,36 +1372,85 @@ TEST_CASE_FIXTURE(Fixture, "overloadeded_functions_with_never_arguments") if (FFlag::LuauSolverV2) { const std::string expected1 = - "Type\n\t" - "'((never) -> string?) & ((number) -> number?)'" - "\ncould not be converted into\n\t" - "'(never) -> nil'; \n" - "this is because \n\t" - " * in the 1st component of the intersection, the function returns the 1st entry in the type pack which has the 1st component of the " - "union as `number` and it returns the 1st entry in the type pack is `nil`, and `number` is not a subtype of `nil`\n\t" - " * in the 2nd component of the intersection, the function returns the 1st entry in the type pack which has the 1st component of the " - "union as `string` and it returns the 1st entry in the type pack is `nil`, and `string` is not a subtype of `nil`"; + FFlag::LuauBetterTypeMismatchErrors + ? "Expected this to be\n\t" + "'(never) -> nil'" + "\nbut got\n\t" + "'((never) -> string?) & ((number) -> number?)'" + "; \nthis is because \n\t" + " * in the 1st component of the intersection, the function returns the 1st entry in the type pack which has the 1st component of " + "the " + "union as `number` and it returns the 1st entry in the type pack is `nil`, and `number` is not a subtype of `nil`\n\t" + " * in the 2nd component of the intersection, the function returns the 1st entry in the type pack which has the 1st component of " + "the " + "union as `string` and it returns the 1st entry in the type pack is `nil`, and `string` is not a subtype of `nil`" + : "Type\n\t" + "'((never) -> string?) & ((number) -> number?)'" + "\ncould not be converted into\n\t" + "'(never) -> nil'; \n" + "this is because \n\t" + " * in the 1st component of the intersection, the function returns the 1st entry in the type pack which has the 1st component of " + "the " + "union as `number` and it returns the 1st entry in the type pack is `nil`, and `number` is not a subtype of `nil`\n\t" + " * in the 2nd component of the intersection, the function returns the 1st entry in the type pack which has the 1st component of " + "the " + "union as `string` and it returns the 1st entry in the type pack is `nil`, and `string` is not a subtype of `nil`"; const std::string expected2 = - "Type\n\t" - "'((never) -> string?) & ((number) -> number?)'" - "\ncould not be converted into\n\t" - "'(number?) -> nil'; \n" - "this is because \n\t" - " * in the 1st component of the intersection, the function returns the 1st entry in the type pack which has the 1st component of the " - "union as `number` and it returns the 1st entry in the type pack is `nil`, and `number` is not a subtype of `nil`\n\t" - " * in the 1st component of the intersection, the function takes the 1st entry in the type pack which is `number` and it takes the 1st " - "entry in the type pack has the 2nd component of the union as `nil`, and `number` is not a supertype of `nil`\n\t" - " * in the 2nd component of the intersection, the function returns the 1st entry in the type pack which has the 1st component of the " - "union as `string` and it returns the 1st entry in the type pack is `nil`, and `string` is not a subtype of `nil`\n\t" - " * in the 2nd component of the intersection, the function takes the 1st entry in the type pack which is `never` and it takes the 1st " - "entry in the type pack has the 1st component of the union as `number`, and `never` is not a supertype of `number`\n\t" - " * in the 2nd component of the intersection, the function takes the 1st entry in the type pack which is `never` and it takes the 1st " - "entry in the type pack has the 2nd component of the union as `nil`, and `never` is not a supertype of `nil`"; + FFlag::LuauBetterTypeMismatchErrors + ? "Expected this to be\n\t" + "'(number?) -> nil'" + "\nbut got\n\t" + "'((never) -> string?) & ((number) -> number?)'" + "; \nthis is because \n\t" + " * in the 1st component of the intersection, the function returns the 1st entry in the type pack which has the 1st component of " + "the " + "union as `number` and it returns the 1st entry in the type pack is `nil`, and `number` is not a subtype of `nil`\n\t" + " * in the 1st component of the intersection, the function takes the 1st entry in the type pack which is `number` and it takes the " + "1st " + "entry in the type pack has the 2nd component of the union as `nil`, and `number` is not a supertype of `nil`\n\t" + " * in the 2nd component of the intersection, the function returns the 1st entry in the type pack which has the 1st component of " + "the " + "union as `string` and it returns the 1st entry in the type pack is `nil`, and `string` is not a subtype of `nil`\n\t" + " * in the 2nd component of the intersection, the function takes the 1st entry in the type pack which is `never` and it takes the " + "1st " + "entry in the type pack has the 1st component of the union as `number`, and `never` is not a supertype of `number`\n\t" + " * in the 2nd component of the intersection, the function takes the 1st entry in the type pack which is `never` and it takes the " + "1st " + "entry in the type pack has the 2nd component of the union as `nil`, and `never` is not a supertype of `nil`" + : "Type\n\t" + "'((never) -> string?) & ((number) -> number?)'" + "\ncould not be converted into\n\t" + "'(number?) -> nil'; \n" + "this is because \n\t" + " * in the 1st component of the intersection, the function returns the 1st entry in the type pack which has the 1st component of " + "the " + "union as `number` and it returns the 1st entry in the type pack is `nil`, and `number` is not a subtype of `nil`\n\t" + " * in the 1st component of the intersection, the function takes the 1st entry in the type pack which is `number` and it takes the " + "1st " + "entry in the type pack has the 2nd component of the union as `nil`, and `number` is not a supertype of `nil`\n\t" + " * in the 2nd component of the intersection, the function returns the 1st entry in the type pack which has the 1st component of " + "the " + "union as `string` and it returns the 1st entry in the type pack is `nil`, and `string` is not a subtype of `nil`\n\t" + " * in the 2nd component of the intersection, the function takes the 1st entry in the type pack which is `never` and it takes the " + "1st " + "entry in the type pack has the 1st component of the union as `number`, and `never` is not a supertype of `number`\n\t" + " * in the 2nd component of the intersection, the function takes the 1st entry in the type pack which is `never` and it takes the " + "1st " + "entry in the type pack has the 2nd component of the union as `nil`, and `never` is not a supertype of `nil`"; CHECK_EQ(expected1, toString(result.errors[0])); CHECK_EQ(expected2, toString(result.errors[1])); } + else if (FFlag::LuauBetterTypeMismatchErrors) + { + LUAU_REQUIRE_ERROR_COUNT(1, result); + const std::string expected = R"(Expected this to be + '(number?) -> nil' +but got + '((never) -> string?) & ((number) -> number?)'; none of the intersection parts are compatible)"; + CHECK_EQ(expected, toString(result.errors[0])); + } else { LUAU_REQUIRE_ERROR_COUNT(1, result); @@ -1080,10 +1476,16 @@ TEST_CASE_FIXTURE(Fixture, "overloadeded_functions_with_overlapping_results_and_ LUAU_REQUIRE_ERROR_COUNT(1, result); - const std::string expected = "Type\n\t" - "'((number?) -> (...number)) & ((string?) -> number | string)'" - "\ncould not be converted into\n\t" - "'(number | string) -> (number, number?)'; none of the intersection parts are compatible"; + const std::string expected = FFlag::LuauBetterTypeMismatchErrors + ? "Expected this to be\n\t" + "'(number | string) -> (number, number?)'" + "\nbut got\n\t" + "'((number?) -> (...number)) & ((string?) -> number | string)'" + "; none of the intersection parts are compatible" + : "Type\n\t" + "'((number?) -> (...number)) & ((string?) -> number | string)'" + "\ncould not be converted into\n\t" + "'(number | string) -> (number, number?)'; none of the intersection parts are compatible"; CHECK(expected == toString(result.errors[0])); } @@ -1102,6 +1504,14 @@ TEST_CASE_FIXTURE(Fixture, "overloadeded_functions_with_weird_typepacks_1") { LUAU_REQUIRE_NO_ERRORS(result); } + else if (FFlag::LuauBetterTypeMismatchErrors) + { + LUAU_REQUIRE_ERROR_COUNT(1, result); + CHECK_EQ( + toString(result.errors[0]), + "Expected this to be '() -> ()', but got '(() -> (a...)) & (() -> (b...))'; none of the intersection parts are compatible" + ); + } else { LUAU_REQUIRE_ERROR_COUNT(1, result); @@ -1131,6 +1541,14 @@ TEST_CASE_FIXTURE(Fixture, "overloadeded_functions_with_weird_typepacks_2") CHECK_EQ(toString(tm->wantedType), "() -> ()"); CHECK_EQ(toString(tm->givenType), "((a...) -> ()) & ((b...) -> ())"); } + else if (FFlag::LuauBetterTypeMismatchErrors) + { + LUAU_REQUIRE_ERROR_COUNT(1, result); + CHECK_EQ( + toString(result.errors[0]), + "Expected this to be '() -> ()', but got '((a...) -> ()) & ((b...) -> ())'; none of the intersection parts are compatible" + ); + } else { LUAU_REQUIRE_ERROR_COUNT(1, result); @@ -1160,6 +1578,15 @@ TEST_CASE_FIXTURE(Fixture, "overloadeded_functions_with_weird_typepacks_3") CHECK_EQ(toString(tm->wantedType), "() -> number"); CHECK_EQ(toString(tm->givenType), "(() -> (a...)) & (() -> (number?, a...))"); } + else if (FFlag::LuauBetterTypeMismatchErrors) + { + LUAU_REQUIRE_ERROR_COUNT(1, result); + const std::string expected = R"(Expected this to be + '() -> number' +but got + '(() -> (a...)) & (() -> (number?, a...))'; none of the intersection parts are compatible)"; + CHECK_EQ(expected, toString(result.errors[0])); + } else { LUAU_REQUIRE_ERROR_COUNT(1, result); @@ -1191,21 +1618,53 @@ TEST_CASE_FIXTURE(Fixture, "overloadeded_functions_with_weird_typepacks_4") CHECK_EQ(toString(tm->wantedType), "(number?) -> ()"); CHECK_EQ(toString(tm->givenType), "((a...) -> ()) & ((number, a...) -> number)"); const std::string expected = - "Type\n\t" - "'((a...) -> ()) & ((number, a...) -> number)'" - "\ncould not be converted into\n\t" - "'(number?) -> ()'; \n" - "this is because \n\t" - " * in the 1st component of the intersection, the function takes a tail of `a...` and it takes the portion of the type pack starting at " - "index 0 to the end`number?`, and `a...` is not a supertype of `number?`\n\t" - " * in the 2nd component of the intersection, the function returns is `number` and it returns `()`, and `number` is not a subtype of " - "`()`\n\t" - " * in the 2nd component of the intersection, the function takes a tail of `a...` and it takes `number?`, and `a...` is not a supertype " - "of `number?`\n\t" - " * in the 2nd component of the intersection, the function takes the 1st entry in the type pack which is `number` and it takes the 1st " - "entry in the type pack has the 2nd component of the union as `nil`, and `number` is not a supertype of `nil`"; + FFlag::LuauBetterTypeMismatchErrors + ? "Expected this to be\n\t" + "'(number?) -> ()'" + "\nbut got\n\t" + "'((a...) -> ()) & ((number, a...) -> number)'" + "; \nthis is because \n\t" + " * in the 1st component of the intersection, the function takes a tail of `a...` and it takes the portion of the type pack " + "starting at " + "index 0 to the end`number?`, and `a...` is not a supertype of `number?`\n\t" + " * in the 2nd component of the intersection, the function returns is `number` and it returns `()`, and `number` is not a subtype " + "of " + "`()`\n\t" + " * in the 2nd component of the intersection, the function takes a tail of `a...` and it takes `number?`, and `a...` is not a " + "supertype " + "of `number?`\n\t" + " * in the 2nd component of the intersection, the function takes the 1st entry in the type pack which is `number` and it takes the " + "1st " + "entry in the type pack has the 2nd component of the union as `nil`, and `number` is not a supertype of `nil`" + : "Type\n\t" + "'((a...) -> ()) & ((number, a...) -> number)'" + "\ncould not be converted into\n\t" + "'(number?) -> ()'; \n" + "this is because \n\t" + " * in the 1st component of the intersection, the function takes a tail of `a...` and it takes the portion of the type pack " + "starting at " + "index 0 to the end`number?`, and `a...` is not a supertype of `number?`\n\t" + " * in the 2nd component of the intersection, the function returns is `number` and it returns `()`, and `number` is not a subtype " + "of " + "`()`\n\t" + " * in the 2nd component of the intersection, the function takes a tail of `a...` and it takes `number?`, and `a...` is not a " + "supertype " + "of `number?`\n\t" + " * in the 2nd component of the intersection, the function takes the 1st entry in the type pack which is `number` and it takes the " + "1st " + "entry in the type pack has the 2nd component of the union as `nil`, and `number` is not a supertype of `nil`"; CHECK(expected == toString(result.errors[0])); } + else if (FFlag::LuauBetterTypeMismatchErrors) + { + CHECK_EQ( + R"(Expected this to be + '(number?) -> ()' +but got + '((a...) -> ()) & ((number, a...) -> number)'; none of the intersection parts are compatible)", + toString(result.errors[0]) + ); + } else { CHECK_EQ( diff --git a/tests/TypeInfer.loops.test.cpp b/tests/TypeInfer.loops.test.cpp index 424069cb2..7c1a7a726 100644 --- a/tests/TypeInfer.loops.test.cpp +++ b/tests/TypeInfer.loops.test.cpp @@ -18,7 +18,9 @@ using namespace Luau; LUAU_FASTFLAG(LuauSolverV2) LUAU_FASTFLAG(LuauNoScopeShallNotSubsumeAll) LUAU_FASTFLAG(LuauCheckForInWithSubtyping3) -LUAU_FASTFLAG(LuauInstantiationUsesGenericPolarity) +LUAU_FASTFLAG(LuauInstantiationUsesGenericPolarity2) +LUAU_FASTFLAG(LuauBetterTypeMismatchErrors) +LUAU_FASTFLAG(LuauPropagateTypeAnnotationsInForInLoops) TEST_SUITE_BEGIN("TypeInferLoops"); @@ -273,6 +275,8 @@ TEST_CASE_FIXTURE(BuiltinsFixture, "for_in_loop_with_zero_iterators_dcr") TEST_CASE_FIXTURE(BuiltinsFixture, "for_in_with_a_custom_iterator_should_type_check") { + ScopedFastFlag _{FFlag::LuauPropagateTypeAnnotationsInForInLoops, true}; + CheckResult result = check(R"( local function range(l, h): () -> number return function() @@ -285,10 +289,10 @@ TEST_CASE_FIXTURE(BuiltinsFixture, "for_in_with_a_custom_iterator_should_type_ch end )"); - if (FFlag::LuauSolverV2) - LUAU_REQUIRE_NO_ERRORS(result); - else + if (FFlag::LuauPropagateTypeAnnotationsInForInLoops) LUAU_REQUIRE_ERROR_COUNT(1, result); + else + LUAU_REQUIRE_NO_ERRORS(result); } TEST_CASE_FIXTURE(Fixture, "for_in_loop_on_error") @@ -953,7 +957,10 @@ TEST_CASE_FIXTURE(Fixture, "for_loop_lower_bound_is_string_2") )"); LUAU_REQUIRE_ERROR_COUNT(1, result); - CHECK_EQ("Type 'number' could not be converted into 'never'", toString(result.errors[0])); + if (FFlag::LuauBetterTypeMismatchErrors) + CHECK_EQ("Expected this to be unreachable, but got 'number'", toString(result.errors[0])); + else + CHECK_EQ("Type 'number' could not be converted into 'never'", toString(result.errors[0])); } TEST_CASE_FIXTURE(Fixture, "for_loop_lower_bound_is_string_3") @@ -1538,4 +1545,120 @@ end )")); } +TEST_CASE_FIXTURE(BuiltinsFixture, "any_type_in_for_loop_should_propagate") +{ + ScopedFastFlag _{FFlag::LuauPropagateTypeAnnotationsInForInLoops, true}; + + CheckResult result = check(R"( + --!strict + function my_iter(): any + return {} + end + + for index: number, value: string in my_iter() do + print(index, value) + end + )"); + + LUAU_REQUIRE_NO_ERRORS(result); + + CHECK("number" == toString(requireTypeAtPosition(Position{7, 18}))); + CHECK("string" == toString(requireTypeAtPosition(Position{7, 25}))); +} + +TEST_CASE_FIXTURE(BuiltinsFixture, "explicit_types_in_for_loop_should_propagate") +{ + ScopedFastFlag _{FFlag::LuauPropagateTypeAnnotationsInForInLoops, true}; + + CheckResult result = check(R"( + --!strict + function my_iter(): {[number]: string} + return {} + end + + for index: number, value: string in my_iter() do + print(index, value) + end + )"); + + LUAU_REQUIRE_NO_ERRORS(result); + + CHECK("number" == toString(requireTypeAtPosition(Position{7, 18}))); + CHECK("string" == toString(requireTypeAtPosition(Position{7, 25}))); +} + +TEST_CASE_FIXTURE(BuiltinsFixture, "incorrect_type_annotation_types_in_loop_should_propagate_with_errors") +{ + ScopedFastFlag _{FFlag::LuauPropagateTypeAnnotationsInForInLoops, true}; + + CheckResult result = check(R"( + --!strict + function my_iter(): any + return {} + end + for index: number, value: string in my_iter() do + index = "" + print(index) + end + )"); + + LUAU_REQUIRE_ERROR_COUNT(1, result); + + auto err = get(result.errors[0]); + REQUIRE(err); + CHECK_EQ("number", toString(err->wantedType)); + CHECK_EQ("string", toString(err->givenType)); +} + +TEST_CASE_FIXTURE(BuiltinsFixture, "for_in_loop_annotations_apply_to_function_expressions") +{ + ScopedFastFlag _{FFlag::LuauPropagateTypeAnnotationsInForInLoops, true}; + + CheckResult result = check(R"( + --!strict + function my_iter(): any + return {} + end + + local function takesString(s: string) end + + for index: number in my_iter() do + takesString(index) + end + )"); + + LUAU_REQUIRE_ERROR_COUNT(1, result); + + auto err = get(result.errors[0]); + REQUIRE(err); + CHECK_EQ("string", toString(err->wantedType)); + CHECK_EQ("number", toString(err->givenType)); +} + +TEST_CASE_FIXTURE(BuiltinsFixture, "for_in_loop_annotations_apply_inside_lambdas") +{ + ScopedFastFlag _{FFlag::LuauPropagateTypeAnnotationsInForInLoops, true}; + + CheckResult result = check(R"( + --!strict + function my_iter(): any + return {} + end + + for index: number in my_iter() do + local fn = function() + index = "" + end + fn() + end + )"); + + LUAU_REQUIRE_ERROR_COUNT(1, result); + + auto err = get(result.errors[0]); + REQUIRE(err); + CHECK_EQ("number", toString(err->wantedType)); + CHECK_EQ("string", toString(err->givenType)); +} + TEST_SUITE_END(); diff --git a/tests/TypeInfer.modules.test.cpp b/tests/TypeInfer.modules.test.cpp index 26f24ab24..4d491ff42 100644 --- a/tests/TypeInfer.modules.test.cpp +++ b/tests/TypeInfer.modules.test.cpp @@ -15,6 +15,7 @@ LUAU_FASTFLAG(LuauInstantiateInSubtyping) LUAU_FASTFLAG(LuauSolverV2) LUAU_FASTFLAG(DebugLuauMagicTypes) LUAU_FASTINT(LuauSolverConstraintLimit) +LUAU_FASTFLAG(LuauBetterTypeMismatchErrors) using namespace Luau; @@ -259,7 +260,10 @@ a = tbl.abc.def CheckResult result = getFrontend().check("game/B"); LUAU_REQUIRE_ERROR_COUNT(1, result); - CHECK_EQ("Type 'number' could not be converted into 'string'", toString(result.errors[0])); + if (FFlag::LuauBetterTypeMismatchErrors) + CHECK_EQ("Expected this to be 'string', but got 'number'", toString(result.errors[0])); + else + CHECK_EQ("Type 'number' could not be converted into 'string'", toString(result.errors[0])); } TEST_CASE_FIXTURE(BuiltinsFixture, "general_require_type_mismatch") @@ -274,7 +278,10 @@ local tbl: string = require(game.A) CheckResult result = getFrontend().check("game/B"); LUAU_REQUIRE_ERROR_COUNT(1, result); - CHECK_EQ("Type '{ def: number }' could not be converted into 'string'", toString(result.errors[0])); + if (FFlag::LuauBetterTypeMismatchErrors) + CHECK_EQ("Expected this to be 'string', but got '{ def: number }'", toString(result.errors[0])); + else + CHECK_EQ("Type '{ def: number }' could not be converted into 'string'", toString(result.errors[0])); } TEST_CASE_FIXTURE(Fixture, "bound_free_table_export_is_ok") @@ -460,11 +467,24 @@ local b: B.T = a if (FFlag::LuauSolverV2) { - const std::string expected = "Type 'T' from 'game/A' could not be converted into 'T' from 'game/B'; \n" - "this is because accessing `x` results in `number` in the former type and `string` in the latter type, and " - "`number` is not exactly `string`"; + const std::string expected = + FFlag::LuauBetterTypeMismatchErrors + ? "Expected this to be 'T' from 'game/B', but got 'T' from 'game/A'; \n" + "accessing `x` results in `number` in the latter type and `string` in the former type, and " + "`number` is not exactly `string`" + : "Type 'T' from 'game/A' could not be converted into 'T' from 'game/B'; \n" + "this is because accessing `x` results in `number` in the former type and `string` in the latter type, and " + "`number` is not exactly `string`"; CHECK(expected == toString(result.errors[0])); } + else if (FFlag::LuauBetterTypeMismatchErrors) + { + const std::string expected = R"(Expected this to be exactly 'T' from 'game/B', but got 'T' from 'game/A' +caused by: + Property 'x' is not compatible. +Expected this to be exactly 'string', but got 'number')"; + CHECK_EQ(expected, toString(result.errors[0])); + } else { const std::string expected = R"(Type 'T' from 'game/A' could not be converted into 'T' from 'game/B' @@ -506,11 +526,24 @@ local b: B.T = a if (FFlag::LuauSolverV2) { - const std::string expected = "Type 'T' from 'game/B' could not be converted into 'T' from 'game/C'; \n" - "this is because accessing `x` results in `number` in the former type and `string` in the latter type, and " - "`number` is not exactly `string`"; + const std::string expected = + FFlag::LuauBetterTypeMismatchErrors + ? "Expected this to be 'T' from 'game/C', but got 'T' from 'game/B'; \n" + "accessing `x` results in `number` in the latter type and `string` in the former type, and " + "`number` is not exactly `string`" + : "Type 'T' from 'game/B' could not be converted into 'T' from 'game/C'; \n" + "this is because accessing `x` results in `number` in the former type and `string` in the latter type, and " + "`number` is not exactly `string`"; CHECK(expected == toString(result.errors[0])); } + else if (FFlag::LuauBetterTypeMismatchErrors) + { + const std::string expected = R"(Expected this to be exactly 'T' from 'game/C', but got 'T' from 'game/B' +caused by: + Property 'x' is not compatible. +Expected this to be exactly 'string', but got 'number')"; + CHECK_EQ(expected, toString(result.errors[0])); + } else { const std::string expected = R"(Type 'T' from 'game/B' could not be converted into 'T' from 'game/C' diff --git a/tests/TypeInfer.oop.test.cpp b/tests/TypeInfer.oop.test.cpp index 7f5eabfac..904dae8f1 100644 --- a/tests/TypeInfer.oop.test.cpp +++ b/tests/TypeInfer.oop.test.cpp @@ -15,9 +15,11 @@ using namespace Luau; +LUAU_FASTFLAG(LuauHandleFunctionOversaturation) +LUAU_FASTFLAG(LuauIndexInMetatableSubtyping) +LUAU_FASTFLAG(LuauPushTypeConstraintLambdas2) LUAU_FASTFLAG(LuauSolverV2) LUAU_FASTFLAG(LuauTrackFreeInteriorTypePacks) -LUAU_FASTFLAG(LuauIndexInMetatableSubtyping) TEST_SUITE_BEGIN("TypeInferOOP"); @@ -168,6 +170,40 @@ TEST_CASE_FIXTURE(Fixture, "inferring_hundreds_of_self_calls_should_not_suffocat CHECK_GE(50, module->internalTypes.types.size()); } +TEST_CASE_FIXTURE(Fixture, "pass_too_many_arguments") +{ + ScopedFastFlag sff[] = { + {FFlag::LuauSolverV2, true}, + {FFlag::LuauPushTypeConstraintLambdas2, true}, + {FFlag::LuauHandleFunctionOversaturation, true}, + }; + + CheckResult result = check(R"( + type T = { + method: (T, number) -> number + } + + function makeT(): T + return { + method=function(self, number) + return number * 2 + end + } + end + + local a = makeT() + a:method(5, 7) + )"); + + LUAU_CHECK_ERROR_COUNT(1, result); + + const CountMismatch* countMismatch = get(result.errors.at(0)); + REQUIRE_MESSAGE(countMismatch, "Expected CountMismatch but got " << result.errors.at(0)); + + CHECK(countMismatch->expected == 2); + CHECK(countMismatch->actual == 3); +} + TEST_CASE_FIXTURE(BuiltinsFixture, "object_constructor_can_refer_to_method_of_self") { // CLI-30902 diff --git a/tests/TypeInfer.operators.test.cpp b/tests/TypeInfer.operators.test.cpp index 3f092d7e3..0c518e082 100644 --- a/tests/TypeInfer.operators.test.cpp +++ b/tests/TypeInfer.operators.test.cpp @@ -19,10 +19,9 @@ using namespace Luau; LUAU_FASTFLAG(LuauSolverV2) LUAU_FASTFLAG(LuauNoScopeShallNotSubsumeAll) -LUAU_FASTFLAG(LuauTrackUniqueness) -LUAU_FASTFLAG(LuauNoMoreComparisonTypeFunctions) LUAU_FASTFLAG(LuauSolverAgnosticStringification) LUAU_FASTFLAG(LuauUnknownGlobalFixSuggestion) +LUAU_FASTFLAG(LuauBetterTypeMismatchErrors) TEST_SUITE_BEGIN("TypeInferOperators"); @@ -541,7 +540,10 @@ TEST_CASE_FIXTURE(BuiltinsFixture, "compound_assign_mismatch_metatable") )"); LUAU_REQUIRE_ERROR_COUNT(1, result); - CHECK("Type 'number' could not be converted into 'V2'" == toString(result.errors[0])); + if (FFlag::LuauBetterTypeMismatchErrors) + CHECK("Expected this to be 'V2', but got 'number'" == toString(result.errors[0])); + else + CHECK("Type 'number' could not be converted into 'V2'" == toString(result.errors[0])); } TEST_CASE_FIXTURE(Fixture, "CallOrOfFunctions") @@ -1321,7 +1323,6 @@ TEST_CASE_FIXTURE(Fixture, "unrelated_primitives_cannot_be_compared") { ScopedFastFlag sff[] = { {FFlag::LuauSolverV2, true}, - {FFlag::LuauNoMoreComparisonTypeFunctions, true}, }; CheckResult result = check(R"( @@ -1444,12 +1445,8 @@ end )"); // FIXME(CLI-165431): fixing subtyping revealed an overload selection problems - if (FFlag::LuauSolverV2 && FFlag::LuauNoScopeShallNotSubsumeAll && FFlag::LuauTrackUniqueness) + if (FFlag::LuauSolverV2 && FFlag::LuauNoScopeShallNotSubsumeAll) LUAU_REQUIRE_NO_ERRORS(result); - else if (FFlag::LuauSolverV2 && FFlag::LuauNoScopeShallNotSubsumeAll) - { - LUAU_REQUIRE_ERROR_COUNT(2, result); - } else LUAU_REQUIRE_NO_ERRORS(result); } diff --git a/tests/TypeInfer.primitives.test.cpp b/tests/TypeInfer.primitives.test.cpp index 90e66e594..1155d4443 100644 --- a/tests/TypeInfer.primitives.test.cpp +++ b/tests/TypeInfer.primitives.test.cpp @@ -7,6 +7,8 @@ #include "doctest.h" +LUAU_FASTFLAG(LuauBetterTypeMismatchErrors) + using namespace Luau; TEST_SUITE_BEGIN("TypeInferPrimitives"); @@ -82,12 +84,18 @@ TEST_CASE_FIXTURE(Fixture, "check_methods_of_number") if (FFlag::LuauSolverV2) { CHECK("Expected type table, got 'number' instead" == toString(result.errors[0])); - CHECK("Type 'number' could not be converted into 'string'" == toString(result.errors[1])); + if (FFlag::LuauBetterTypeMismatchErrors) + CHECK("Expected this to be 'string', but got 'number'" == toString(result.errors[1])); + else + CHECK("Type 'number' could not be converted into 'string'" == toString(result.errors[1])); } else { CHECK_EQ(toString(result.errors[0]), "Cannot add method to non-table type 'number'"); - CHECK_EQ(toString(result.errors[1]), "Type 'number' could not be converted into 'string'"); + if (FFlag::LuauBetterTypeMismatchErrors) + CHECK("Expected this to be 'string', but got 'number'" == toString(result.errors[1])); + else + CHECK_EQ(toString(result.errors[1]), "Type 'number' could not be converted into 'string'"); } } diff --git a/tests/TypeInfer.provisional.test.cpp b/tests/TypeInfer.provisional.test.cpp index 0b2d27ece..ef790bac8 100644 --- a/tests/TypeInfer.provisional.test.cpp +++ b/tests/TypeInfer.provisional.test.cpp @@ -17,8 +17,8 @@ LUAU_FASTINT(LuauTarjanChildLimit) LUAU_FASTINT(LuauTypeInferIterationLimit) LUAU_FASTINT(LuauTypeInferRecursionLimit) LUAU_FASTINT(LuauTypeInferTypePackLoopLimit) -LUAU_FASTFLAG(LuauNoMoreComparisonTypeFunctions) LUAU_FASTFLAG(LuauAddRefinementToAssertions) +LUAU_FASTFLAG(LuauBetterTypeMismatchErrors) LUAU_FASTFLAG(LuauNewOverloadResolver2) TEST_SUITE_BEGIN("ProvisionalTests"); @@ -214,7 +214,10 @@ TEST_CASE_FIXTURE(Fixture, "while_body_are_also_refined") LUAU_REQUIRE_ERROR_COUNT(1, result); - CHECK_EQ("Type 'Node?' could not be converted into 'Node'", toString(result.errors[0])); + if (FFlag::LuauBetterTypeMismatchErrors) + CHECK_EQ("Expected this to be 'Node', but got 'Node?'", toString(result.errors[0])); + else + CHECK_EQ("Type 'Node?' could not be converted into 'Node'", toString(result.errors[0])); } // Originally from TypeInfer.test.cpp. @@ -842,6 +845,19 @@ TEST_CASE_FIXTURE(Fixture, "assign_table_with_refined_property_with_a_similar_ty if (FFlag::LuauSolverV2) LUAU_REQUIRE_NO_ERRORS(result); // This is wrong. We should be rejecting this assignment. + else if (FFlag::LuauBetterTypeMismatchErrors) + { + LUAU_REQUIRE_ERROR_COUNT(1, result); + const std::string expected = + R"(Expected this to be exactly + '{ x: number }' +but got + '{ x: number? }' +caused by: + Property 'x' is not compatible. +Expected this to be exactly 'number', but got 'number?')"; + CHECK_EQ(expected, toString(result.errors[0])); + } else { LUAU_REQUIRE_ERROR_COUNT(1, result); @@ -1295,8 +1311,6 @@ TEST_CASE_FIXTURE(Fixture, "table_containing_non_final_type_is_erroneously_cache // CLI-111113 TEST_CASE_FIXTURE(Fixture, "we_cannot_infer_functions_that_return_inconsistently") { - ScopedFastFlag sff{FFlag::LuauNoMoreComparisonTypeFunctions, true}; - CheckResult result = check(R"( function find_first(tbl: {T}, el) for i, e in tbl do diff --git a/tests/TypeInfer.refinements.test.cpp b/tests/TypeInfer.refinements.test.cpp index e8691c3c4..62f7f41d3 100644 --- a/tests/TypeInfer.refinements.test.cpp +++ b/tests/TypeInfer.refinements.test.cpp @@ -12,13 +12,12 @@ LUAU_FASTFLAG(LuauSolverV2) LUAU_FASTFLAG(LuauFunctionCallsAreNotNilable) LUAU_FASTFLAG(LuauRefineNoRefineAlways2) LUAU_FASTFLAG(LuauRefineDistributesOverUnions) -LUAU_FASTFLAG(LuauNoMoreComparisonTypeFunctions) LUAU_FASTFLAG(LuauNumericUnaryOpsDontProduceNegationRefinements) -LUAU_FASTFLAG(LuauConsiderErrorSuppressionInTypes) LUAU_FASTFLAG(LuauAddRefinementToAssertions) LUAU_FASTFLAG(DebugLuauAssertOnForcedConstraint) LUAU_FASTFLAG(LuauNormalizationPreservesAny) LUAU_FASTFLAG(LuauRefineNoRefineAlways2) +LUAU_FASTFLAG(LuauBetterTypeMismatchErrors) LUAU_FASTFLAG(LuauFixSubtypingOfNegations) using namespace Luau; @@ -519,21 +518,30 @@ TEST_CASE_FIXTURE(BuiltinsFixture, "call_an_incompatible_function_after_using_ty end )"); - if (FFlag::LuauSolverV2 && FFlag::LuauConsiderErrorSuppressionInTypes) + if (FFlag::LuauSolverV2) { LUAU_REQUIRE_ERROR_COUNT(1, result); - CHECK("Type 'string' could not be converted into 'number'" == toString(result.errors[0])); + if (FFlag::LuauBetterTypeMismatchErrors) + CHECK("Expected this to be 'number', but got 'string'" == toString(result.errors[0])); + else + CHECK("Type 'string' could not be converted into 'number'" == toString(result.errors[0])); CHECK(Location{{7, 18}, {7, 19}} == result.errors[0].location); } else { LUAU_REQUIRE_ERROR_COUNT(2, result); - CHECK("Type 'string' could not be converted into 'number'" == toString(result.errors[0])); + if (FFlag::LuauBetterTypeMismatchErrors) + CHECK("Expected this to be 'number', but got 'string'" == toString(result.errors[0])); + else + CHECK("Type 'string' could not be converted into 'number'" == toString(result.errors[0])); CHECK(Location{{7, 18}, {7, 19}} == result.errors[0].location); - CHECK("Type 'string' could not be converted into 'number'" == toString(result.errors[1])); + if (FFlag::LuauBetterTypeMismatchErrors) + CHECK("Expected this to be 'number', but got 'string'" == toString(result.errors[1])); + else + CHECK("Type 'string' could not be converted into 'number'" == toString(result.errors[1])); CHECK(Location{{13, 18}, {13, 19}} == result.errors[1].location); } } @@ -2237,7 +2245,6 @@ TEST_CASE_FIXTURE(BuiltinsFixture, "luau_polyfill_isindexkey_refine_conjunction" { ScopedFastFlag sffs[] = { {FFlag::LuauSolverV2, true}, - {FFlag::LuauNoMoreComparisonTypeFunctions, true}, }; CheckResult result = check(R"( @@ -2256,7 +2263,6 @@ TEST_CASE_FIXTURE(BuiltinsFixture, "check_refinement_to_primitive_and_compare") { ScopedFastFlag sffs[] = { {FFlag::LuauSolverV2, true}, - {FFlag::LuauNoMoreComparisonTypeFunctions, true}, }; CheckResult result = check(R"( diff --git a/tests/TypeInfer.singletons.test.cpp b/tests/TypeInfer.singletons.test.cpp index fbb34bc26..bb09a915e 100644 --- a/tests/TypeInfer.singletons.test.cpp +++ b/tests/TypeInfer.singletons.test.cpp @@ -8,9 +8,8 @@ using namespace Luau; LUAU_FASTFLAG(LuauSolverV2) -LUAU_FASTFLAG(LuauPushTypeConstraint2) -LUAU_FASTFLAG(LuauPushTypeConstraintSingleton) LUAU_FASTFLAG(LuauPushTypeConstraintIndexer) +LUAU_FASTFLAG(LuauBetterTypeMismatchErrors) TEST_SUITE_BEGIN("TypeSingletons"); @@ -69,7 +68,10 @@ TEST_CASE_FIXTURE(Fixture, "bool_singletons_mismatch") )"); LUAU_REQUIRE_ERROR_COUNT(1, result); - CHECK_EQ("Type 'false' could not be converted into 'true'", toString(result.errors[0])); + if (FFlag::LuauBetterTypeMismatchErrors) + CHECK_EQ("Expected this to be 'true', but got 'false'", toString(result.errors[0])); + else + CHECK_EQ("Type 'false' could not be converted into 'true'", toString(result.errors[0])); } TEST_CASE_FIXTURE(Fixture, "string_singletons_mismatch") @@ -79,7 +81,10 @@ TEST_CASE_FIXTURE(Fixture, "string_singletons_mismatch") )"); LUAU_REQUIRE_ERROR_COUNT(1, result); - CHECK_EQ("Type '\"bar\"' could not be converted into '\"foo\"'", toString(result.errors[0])); + if (FFlag::LuauBetterTypeMismatchErrors) + CHECK_EQ("Expected this to be '\"foo\"', but got '\"bar\"'", toString(result.errors[0])); + else + CHECK_EQ("Type '\"bar\"' could not be converted into '\"foo\"'", toString(result.errors[0])); } TEST_CASE_FIXTURE(Fixture, "string_singletons_escape_chars") @@ -89,7 +94,10 @@ TEST_CASE_FIXTURE(Fixture, "string_singletons_escape_chars") )"); LUAU_REQUIRE_ERROR_COUNT(1, result); - CHECK_EQ(R"(Type '"\000\r"' could not be converted into '"\n"')", toString(result.errors[0])); + if (FFlag::LuauBetterTypeMismatchErrors) + CHECK_EQ(R"(Expected this to be '"\n"', but got '"\000\r"')", toString(result.errors[0])); + else + CHECK_EQ(R"(Type '"\000\r"' could not be converted into '"\n"')", toString(result.errors[0])); } TEST_CASE_FIXTURE(Fixture, "bool_singleton_subtype") @@ -140,7 +148,10 @@ TEST_CASE_FIXTURE(Fixture, "function_call_with_singletons_mismatch") )"); LUAU_REQUIRE_ERROR_COUNT(1, result); - CHECK_EQ("Type '\"bar\"' could not be converted into '\"foo\"'", toString(result.errors[0])); + if (FFlag::LuauBetterTypeMismatchErrors) + CHECK_EQ("Expected this to be '\"foo\"', but got '\"bar\"'", toString(result.errors[0])); + else + CHECK_EQ("Type '\"bar\"' could not be converted into '\"foo\"'", toString(result.errors[0])); } TEST_CASE_FIXTURE(Fixture, "overloaded_function_call_with_singletons") @@ -191,7 +202,10 @@ TEST_CASE_FIXTURE(Fixture, "overloaded_function_call_with_singletons_mismatch") } else { - CHECK_EQ("Type 'number' could not be converted into 'string'", toString(result.errors[0])); + if (FFlag::LuauBetterTypeMismatchErrors) + CHECK_EQ("Expected this to be 'string', but got 'number'", toString(result.errors[0])); + else + CHECK_EQ("Type 'number' could not be converted into 'string'", toString(result.errors[0])); CHECK_EQ("Other overloads are also not viable: (false, number) -> ()", toString(result.errors[1])); } } @@ -218,7 +232,17 @@ TEST_CASE_FIXTURE(Fixture, "enums_using_singletons_mismatch") LUAU_REQUIRE_ERROR_COUNT(1, result); if (FFlag::LuauSolverV2) - CHECK("Type '\"bang\"' could not be converted into '\"bar\" | \"baz\" | \"foo\"'" == toString(result.errors[0])); + { + if (FFlag::LuauBetterTypeMismatchErrors) + CHECK("Expected this to be '\"bar\" | \"baz\" | \"foo\"', but got '\"bang\"'" == toString(result.errors[0])); + else + CHECK("Type '\"bang\"' could not be converted into '\"bar\" | \"baz\" | \"foo\"'" == toString(result.errors[0])); + } + else if (FFlag::LuauBetterTypeMismatchErrors) + CHECK_EQ( + "Expected this to be '\"bar\" | \"baz\" | \"foo\"', but got '\"bang\"'; none of the union options are compatible", + toString(result.errors[0]) + ); else CHECK_EQ( "Type '\"bang\"' could not be converted into '\"bar\" | \"baz\" | \"foo\"'; none of the union options are compatible", @@ -341,7 +365,10 @@ TEST_CASE_FIXTURE(Fixture, "table_properties_singleton_strings_mismatch") )"); LUAU_REQUIRE_ERROR_COUNT(1, result); - CHECK_EQ("Type 'number' could not be converted into 'string'", toString(result.errors[0])); + if (FFlag::LuauBetterTypeMismatchErrors) + CHECK_EQ("Expected this to be 'string', but got 'number'", toString(result.errors[0])); + else + CHECK_EQ("Type 'number' could not be converted into 'string'", toString(result.errors[0])); } TEST_CASE_FIXTURE(Fixture, "table_properties_alias_or_parens_is_indexer") @@ -399,8 +426,6 @@ TEST_CASE_FIXTURE(Fixture, "table_properties_type_error_escapes") TEST_CASE_FIXTURE(Fixture, "error_detailed_tagged_union_mismatch_string") { - ScopedFastFlag _{FFlag::LuauPushTypeConstraint2, true}; - CheckResult result = check(R"( type Cat = { tag: 'cat', catfood: string } type Dog = { tag: 'dog', dogfood: string } @@ -415,6 +440,14 @@ local a: Animal = { tag = 'cat', cafood = 'something' } R"(Table type '{ cafood: string, tag: "cat" }' not compatible with type 'Cat' because the former is missing field 'catfood')" == toString(result.errors[0]) ); + else if (FFlag::LuauBetterTypeMismatchErrors) + { + const std::string expected = R"(Expected this to be 'Cat | Dog', but got 'a' +caused by: + None of the union options are compatible. For example: +Table type 'a' not compatible with type 'Cat' because the former is missing field 'catfood')"; + CHECK_EQ(expected, toString(result.errors[0])); + } else { const std::string expected = R"(Type 'a' could not be converted into 'Cat | Dog' @@ -437,7 +470,20 @@ local a: Result = { success = false, result = 'something' } LUAU_REQUIRE_ERROR_COUNT(1, result); if (FFlag::LuauSolverV2) - CHECK("Type '{ result: string, success: boolean }' could not be converted into 'Bad | Good'" == toString(result.errors[0])); + { + if (FFlag::LuauBetterTypeMismatchErrors) + CHECK("Expected this to be 'Bad | Good', but got '{ result: string, success: boolean }'" == toString(result.errors[0])); + else + CHECK("Type '{ result: string, success: boolean }' could not be converted into 'Bad | Good'" == toString(result.errors[0])); + } + else if (FFlag::LuauBetterTypeMismatchErrors) + { + const std::string expected = R"(Expected this to be 'Bad | Good', but got 'a' +caused by: + None of the union options are compatible. For example: +Table type 'a' not compatible with type 'Bad' because the former is missing field 'error')"; + CHECK_EQ(expected, toString(result.errors[0])); + } else { const std::string expected = R"(Type 'a' could not be converted into 'Bad | Good' @@ -464,10 +510,14 @@ TEST_CASE_FIXTURE(Fixture, "parametric_tagged_union_alias") LUAU_REQUIRE_ERROR_COUNT(1, result); - const std::string expectedError = "Type\n\t" - "'{ result: string, success: boolean }'" - "\ncould not be converted into\n\t" - "'Err | Ok'"; + const std::string expectedError = FFlag::LuauBetterTypeMismatchErrors ? "Expected this to be\n\t" + "'Err | Ok'" + "\nbut got\n\t" + "'{ result: string, success: boolean }'" + : "Type\n\t" + "'{ result: string, success: boolean }'" + "\ncould not be converted into\n\t" + "'Err | Ok'"; CHECK(toString(result.errors[0]) == expectedError); } @@ -665,8 +715,6 @@ TEST_CASE_FIXTURE(BuiltinsFixture, "singletons_stick_around_under_assignment") TEST_CASE_FIXTURE(Fixture, "tagged_union_in_ternary") { - ScopedFastFlag _{FFlag::LuauPushTypeConstraint2, true}; - LUAU_REQUIRE_NO_ERRORS(check(R"( type Result = { type: "ok", value: unknown } | { type: "error" } @@ -680,8 +728,6 @@ TEST_CASE_FIXTURE(Fixture, "tagged_union_in_ternary") TEST_CASE_FIXTURE(Fixture, "table_literal_with_singleton_union_values") { - ScopedFastFlag _{FFlag::LuauPushTypeConstraint2, true}; - CheckResult result = check(R"( local t1: {[string]: "a" | "b"} = { a = "a", b = "b" } local t2: {[string]: "a" | true} = { a = "a", b = true } @@ -693,8 +739,6 @@ TEST_CASE_FIXTURE(Fixture, "table_literal_with_singleton_union_values") TEST_CASE_FIXTURE(Fixture, "singleton_type_mismatch_via_variable") { - ScopedFastFlag _{FFlag::LuauPushTypeConstraint2, true}; - CheckResult result = check(R"( local c = "c" local x: "a" = c @@ -713,8 +757,6 @@ TEST_CASE_FIXTURE(Fixture, "singleton_type_mismatch_via_variable") TEST_CASE_FIXTURE(Fixture, "cli_163481_any_indexer_pushes_type") { - ScopedFastFlag _{FFlag::LuauPushTypeConstraint2, true}; - LUAU_REQUIRE_NO_ERRORS(check(R"( --!strict @@ -733,11 +775,7 @@ TEST_CASE_FIXTURE(Fixture, "cli_163481_any_indexer_pushes_type") TEST_CASE_FIXTURE(Fixture, "oss_2010") { - ScopedFastFlag sffs[] = { - {FFlag::LuauSolverV2, true}, - {FFlag::LuauPushTypeConstraint2, true}, - {FFlag::LuauPushTypeConstraintSingleton, true}, - }; + ScopedFastFlag _{FFlag::LuauSolverV2, true}; LUAU_REQUIRE_NO_ERRORS(check(R"( local function foo(my_enum: "" | T): T @@ -754,7 +792,6 @@ TEST_CASE_FIXTURE(Fixture, "oss_1773") { ScopedFastFlag sffs[] = { {FFlag::LuauSolverV2, true}, - {FFlag::LuauPushTypeConstraint2, true}, {FFlag::LuauPushTypeConstraintIndexer, true}, }; @@ -785,7 +822,6 @@ TEST_CASE_FIXTURE(Fixture, "bidirectionally_infer_indexers_errored") { ScopedFastFlag sffs[] = { {FFlag::LuauSolverV2, true}, - {FFlag::LuauPushTypeConstraint2, true}, {FFlag::LuauPushTypeConstraintIndexer, true}, }; @@ -809,8 +845,6 @@ TEST_CASE_FIXTURE(Fixture, "bidirectionally_infer_indexers_errored") TEST_CASE_FIXTURE(Fixture, "oss_2018") { - ScopedFastFlag _{FFlag::LuauPushTypeConstraint2, true}; - LUAU_REQUIRE_NO_ERRORS(check(R"( local rule: { rule: "AppendTextComment" } | { rule: "Other" } = { rule = "AppendTextComment" } )")); diff --git a/tests/TypeInfer.tables.test.cpp b/tests/TypeInfer.tables.test.cpp index 16078770b..c3e1a5ca6 100644 --- a/tests/TypeInfer.tables.test.cpp +++ b/tests/TypeInfer.tables.test.cpp @@ -24,11 +24,8 @@ LUAU_FASTFLAG(LuauFixIndexerSubtypingOrdering) LUAU_FASTFLAG(DebugLuauAssertOnForcedConstraint) LUAU_FASTINT(LuauPrimitiveInferenceInTableLimit) LUAU_FASTFLAG(LuauNoScopeShallNotSubsumeAll) -LUAU_FASTFLAG(LuauExtendSealedTableUpperBounds) -LUAU_FASTFLAG(LuauPushTypeConstraint2) LUAU_FASTFLAG(LuauCacheDuplicateHasPropConstraints) LUAU_FASTFLAG(LuauPushTypeConstraintIntersection) -LUAU_FASTFLAG(LuauPushTypeConstraintSingleton) LUAU_FASTFLAG(LuauPushTypeConstraintLambdas2) LUAU_FASTFLAG(LuauNewOverloadResolver2) LUAU_FASTFLAG(LuauGetmetatableError) @@ -36,6 +33,7 @@ LUAU_FASTFLAG(LuauSuppressIndexingIntoError) LUAU_FASTFLAG(LuauPushTypeConstriantAlwaysCompletes) LUAU_FASTFLAG(LuauMarkUnscopedGenericsAsSolved) LUAU_FASTFLAG(LuauUseFastSubtypeForIndexerWithName) +LUAU_FASTFLAG(LuauBetterTypeMismatchErrors) LUAU_FASTFLAG(LuauFixIndexingUnionWithNonTable) TEST_SUITE_BEGIN("TableTests"); @@ -531,7 +529,12 @@ TEST_CASE_FIXTURE(Fixture, "table_param_width_subtyping_3") CHECK(result.errors[0].location == Location{Position{6, 8}, Position{6, 9}}); if (FFlag::LuauSolverV2) - CHECK(toString(result.errors[0]) == "Type 'T' could not be converted into '{ read baz: unknown }'"); + { + if (FFlag::LuauBetterTypeMismatchErrors) + CHECK(toString(result.errors[0]) == "Expected this to be '{ read baz: unknown }', but got 'T'"); + else + CHECK(toString(result.errors[0]) == "Type 'T' could not be converted into '{ read baz: unknown }'"); + } else { TypeError& err = result.errors[0]; @@ -916,9 +919,13 @@ TEST_CASE_FIXTURE(Fixture, "sealed_table_indexers_must_unify") if (FFlag::LuauSolverV2) { - std::string expected = "Type '{number}' could not be converted into '{string}'; \n" - "this is because the result of indexing is `number` in the former type and `string` in the latter type, " - "and `number` is not exactly `string`"; + std::string expected = FFlag::LuauBetterTypeMismatchErrors + ? "Expected this to be '{string}', but got '{number}'; \n" + "the result of indexing is `number` in the latter type and `string` in the former type, " + "and `number` is not exactly `string`" + : "Type '{number}' could not be converted into '{string}'; \n" + "this is because the result of indexing is `number` in the former type and `string` in the latter type, " + "and `number` is not exactly `string`"; auto actual = toString(result.errors[0]); CHECK_EQ(expected, actual); } @@ -1792,10 +1799,14 @@ TEST_CASE_FIXTURE(Fixture, "table_subtyping_with_missing_props_dont_report_multi if (FFlag::LuauSolverV2) { - std::string expected = "Type\n\t" - "'{ x: number }'\n" - "could not be converted into\n\t" - "'{ x: number, y: number, z: number }'"; + std::string expected = FFlag::LuauBetterTypeMismatchErrors ? "Expected this to be\n\t" + "'{ x: number, y: number, z: number }'" + "\nbut got\n\t" + "'{ x: number }'" + : "Type\n\t" + "'{ x: number }'\n" + "could not be converted into\n\t" + "'{ x: number, y: number, z: number }'"; CHECK_EQ(expected, toString(result.errors[0])); } else @@ -1888,10 +1899,16 @@ TEST_CASE_FIXTURE(Fixture, "type_mismatch_on_massive_table_is_cut_short") CHECK("{ a: number, b: number, c: number, d: number, e: number, ... 1 more ... }" == toString(requireType("t"))); CHECK_EQ("number", toString(tm->givenType)); - CHECK_EQ( - "Type 'number' could not be converted into '{ a: number, b: number, c: number, d: number, e: number, ... 1 more ... }'", - toString(result.errors[0]) - ); + if (FFlag::LuauBetterTypeMismatchErrors) + CHECK_EQ( + "Expected this to be '{ a: number, b: number, c: number, d: number, e: number, ... 1 more ... }', but got 'number'", + toString(result.errors[0]) + ); + else + CHECK_EQ( + "Type 'number' could not be converted into '{ a: number, b: number, c: number, d: number, e: number, ... 1 more ... }'", + toString(result.errors[0]) + ); } TEST_CASE_FIXTURE(Fixture, "ok_to_set_nil_even_on_non_lvalue_base_expr") @@ -1925,7 +1942,10 @@ TEST_CASE_FIXTURE(Fixture, "ok_to_set_nil_even_on_non_lvalue_base_expr") LUAU_REQUIRE_ERROR_COUNT(1, result); CHECK_EQ(Location{{2, 27}, {2, 30}}, result.errors[0].location); - CHECK_EQ("Type 'nil' could not be converted into 'boolean'", toString(result.errors[0])); + if (FFlag::LuauBetterTypeMismatchErrors) + CHECK_EQ("Expected this to be 'boolean', but got 'nil'", toString(result.errors[0])); + else + CHECK_EQ("Type 'nil' could not be converted into 'boolean'", toString(result.errors[0])); loadDefinition(R"( declare class FancyHashtable @@ -1950,7 +1970,10 @@ TEST_CASE_FIXTURE(Fixture, "ok_to_set_nil_even_on_non_lvalue_base_expr") LUAU_REQUIRE_ERROR_COUNT(1, result); CHECK_EQ(result.errors[0].location, Location{{2, 31}, {2, 34}}); - CHECK_EQ(toString(result.errors[0]), "Type 'nil' could not be converted into 'string'"); + if (FFlag::LuauBetterTypeMismatchErrors) + CHECK_EQ("Expected this to be 'string', but got 'nil'", toString(result.errors[0])); + else + CHECK_EQ(toString(result.errors[0]), "Type 'nil' could not be converted into 'string'"); } TEST_CASE_FIXTURE(Fixture, "ok_to_set_nil_on_generic_map") @@ -2383,11 +2406,26 @@ local b: B = a if (FFlag::LuauSolverV2) { - CHECK( - "Type 'A' could not be converted into 'B'; \n" - "this is because accessing `y` results in `number` in the former type and `string` in the latter type, and `number` is not exactly " - "`string`" == toString(result.errors.at(0)) - ); + if (FFlag::LuauBetterTypeMismatchErrors) + CHECK( + "Expected this to be 'B', but got 'A'; \n" + "accessing `y` results in `number` in the latter type and `string` in the former type, and `number` is not exactly " + "`string`" == toString(result.errors.at(0)) + ); + else + CHECK( + "Type 'A' could not be converted into 'B'; \n" + "this is because accessing `y` results in `number` in the former type and `string` in the latter type, and `number` is not exactly " + "`string`" == toString(result.errors.at(0)) + ); + } + else if (FFlag::LuauBetterTypeMismatchErrors) + { + const std::string expected = R"(Expected this to be exactly 'B', but got 'A' +caused by: + Property 'y' is not compatible. +Expected this to be exactly 'string', but got 'number')"; + CHECK_EQ(expected, toString(result.errors[0])); } else { @@ -2416,11 +2454,29 @@ local b: B = a if (FFlag::LuauSolverV2) { - CHECK( - "Type 'A' could not be converted into 'B'; \n" - "this is because accessing `b.y` results in `number` in the former type and `string` in the latter type, and `number` is not exactly " - "`string`" == toString(result.errors.at(0)) - ); + if (FFlag::LuauBetterTypeMismatchErrors) + CHECK( + "Expected this to be 'B', but got 'A'; \n" + "accessing `b.y` results in `number` in the latter type and `string` in the former type, and `number` is not exactly " + "`string`" == toString(result.errors.at(0)) + ); + else + CHECK( + "Type 'A' could not be converted into 'B'; \n" + "this is because accessing `b.y` results in `number` in the former type and `string` in the latter type, and `number` is not exactly " + "`string`" == toString(result.errors.at(0)) + ); + } + else if (FFlag::LuauBetterTypeMismatchErrors) + { + const std::string expected = R"(Expected this to be exactly 'B', but got 'A' +caused by: + Property 'b' is not compatible. +Expected this to be exactly 'BS', but got 'AS' +caused by: + Property 'y' is not compatible. +Expected this to be exactly 'string', but got 'number')"; + CHECK_EQ(expected, toString(result.errors[0])); } else { @@ -2440,6 +2496,7 @@ TEST_CASE_FIXTURE(BuiltinsFixture, "error_detailed_metatable_prop") ScopedFastFlag sff[] = { {FFlag::LuauInstantiateInSubtyping, true}, }; + CheckResult result = check(R"( local a1 = setmetatable({ x = 2, y = 3 }, { __call = function(s) end }); local b1 = setmetatable({ x = 2, y = "hello" }, { __call = function(s) end }); @@ -2450,8 +2507,16 @@ local b2 = setmetatable({ x = 2, y = 4 }, { __call = function(s, t) end }); local c2: typeof(a2) = b2 )"); - const std::string expected1 = - R"(Type 'b1' could not be converted into 'a1' + const std::string expected1 = FFlag::LuauBetterTypeMismatchErrors ? R"(Expected this to be 'a1', but got 'b1' +caused by: + Expected this to be exactly + '{| x: number, y: number |}' +but got + '{| x: number, y: string |}' +caused by: + Property 'y' is not compatible. +Expected this to be exactly 'number', but got 'string')" + : R"(Type 'b1' could not be converted into 'a1' caused by: Type '{| x: number, y: string |}' @@ -2460,8 +2525,19 @@ could not be converted into caused by: Property 'y' is not compatible. Type 'string' could not be converted into 'number' in an invariant context)"; - const std::string expected2 = - R"(Type 'b2' could not be converted into 'a2' + const std::string expected2 = FFlag::LuauBetterTypeMismatchErrors ? R"(Expected this to be 'a2', but got 'b2' +caused by: + Expected this to be exactly + '{| __call: (a) -> () |}' +but got + '{| __call: (a, b) -> () |}' +caused by: + Property '__call' is not compatible. +Expected this to be exactly + '(a) -> ()' +but got + '(a, b) -> ()'; different number of generic type parameters)" + : R"(Type 'b2' could not be converted into 'a2' caused by: Type '{| __call: (a, b) -> () |}' @@ -2484,50 +2560,34 @@ could not be converted into // // Second, nil <: unknown, so we consider that parameter to be optional. LUAU_REQUIRE_ERROR_COUNT(1, result); - CHECK( - "Type 'b1' could not be converted into 'a1'; \n" - "this is because in the table portion, accessing `y` results in `string` in the former type and `number` in the latter type, and " - "`string` is not exactly `number`" == toString(result.errors[0]) - ); + if (FFlag::LuauBetterTypeMismatchErrors) + { + CHECK( + "Expected this to be 'a1', but got 'b1'; \n" + "in the table portion, accessing `y` results in `string` in the latter type and `number` in the former type, and " + "`string` is not exactly `number`" == toString(result.errors[0]) + ); + } + else + { + CHECK( + "Type 'b1' could not be converted into 'a1'; \n" + "this is because in the table portion, accessing `y` results in `string` in the former type and `number` in the latter type, and " + "`string` is not exactly `number`" == toString(result.errors[0]) + ); + } } else if (FFlag::LuauInstantiateInSubtyping) { LUAU_REQUIRE_ERROR_COUNT(2, result); CHECK_EQ(expected1, toString(result.errors[0])); - - const std::string expected3 = R"(Type 'b2' could not be converted into 'a2' -caused by: - Type - '{| __call: (a, b) -> () |}' -could not be converted into - '{| __call: (a) -> () |}' -caused by: - Property '__call' is not compatible. -Type - '(a, b) -> ()' -could not be converted into - '(a) -> ()'; different number of generic type parameters)"; - CHECK_EQ(expected2, toString(result.errors[1])); } else { LUAU_REQUIRE_ERROR_COUNT(2, result); CHECK_EQ(expected1, toString(result.errors[0])); - - std::string expected3 = R"(Type 'b2' could not be converted into 'a2' -caused by: - Type - '{ __call: (a, b) -> () }' -could not be converted into - '{ __call: (a) -> () }' -caused by: - Property '__call' is not compatible. -Type - '(a, b) -> ()' -could not be converted into - '(a) -> ()'; different number of generic type parameters)"; - CHECK_EQ(expected3, toString(result.errors[1])); + CHECK_EQ(expected2, toString(result.errors[1])); } } @@ -2545,11 +2605,30 @@ TEST_CASE_FIXTURE(Fixture, "error_detailed_indexer_key") if (FFlag::LuauSolverV2) { - CHECK( - "Type 'A' could not be converted into 'B'; \n" - "this is because the index type is `number` in the former type and `string` in the latter type, and `number` is not exactly `string`" == - toString(result.errors[0]) - ); + if (FFlag::LuauBetterTypeMismatchErrors) + { + CHECK( + "Expected this to be 'B', but got 'A'; \n" + "the index type is `number` in the latter type and `string` in the former type, and `number` is not exactly `string`" == + toString(result.errors[0]) + ); + } + else + { + CHECK( + "Type 'A' could not be converted into 'B'; \n" + "this is because the index type is `number` in the former type and `string` in the latter type, and `number` is not exactly " + "`string`" == toString(result.errors[0]) + ); + } + } + else if (FFlag::LuauBetterTypeMismatchErrors) + { + const std::string expected = R"(Expected this to be exactly 'B', but got 'A' +caused by: + Property '[indexer key]' is not compatible. +Expected this to be exactly 'string', but got 'number')"; + CHECK_EQ(expected, toString(result.errors[0])); } else { @@ -2575,11 +2654,26 @@ TEST_CASE_FIXTURE(Fixture, "error_detailed_indexer_value") if (FFlag::LuauSolverV2) { - CHECK( - "Type 'A' could not be converted into 'B'; \n" - "this is because the result of indexing is `number` in the former type and `string` in the latter type, and `number` is not exactly " - "`string`" == toString(result.errors[0]) - ); + if (FFlag::LuauBetterTypeMismatchErrors) + CHECK( + "Expected this to be 'B', but got 'A'; \n" + "the result of indexing is `number` in the latter type and `string` in the former type, and `number` is not exactly `string`" == + toString(result.errors[0]) + ); + else + CHECK( + "Type 'A' could not be converted into 'B'; \n" + "this is because the result of indexing is `number` in the former type and `string` in the latter type, and `number` is not exactly " + "`string`" == toString(result.errors[0]) + ); + } + else if (FFlag::LuauBetterTypeMismatchErrors) + { + const std::string expected = R"(Expected this to be exactly 'B', but got 'A' +caused by: + Property '[indexer value]' is not compatible. +Expected this to be exactly 'string', but got 'number')"; + CHECK_EQ(expected, toString(result.errors[0])); } else { @@ -2626,11 +2720,28 @@ local y: number = tmp.p.y LUAU_REQUIRE_ERROR_COUNT(1, result); if (FFlag::LuauSolverV2) - CHECK( - "Type 'tmp' could not be converted into 'HasSuper'; \n" - "this is because accessing `p` results in `{ x: number, y: number }` in the former type and `Super` in the latter type, and `{ x: " - "number, y: number }` is not exactly `Super`" == toString(result.errors[0]) - ); + { + if (FFlag::LuauBetterTypeMismatchErrors) + CHECK( + "Expected this to be 'HasSuper', but got 'tmp'; \n" + "accessing `p` results in `{ x: number, y: number }` in the latter type and `Super` in the former type, and `{ x: " + "number, y: number }` is not exactly `Super`" == toString(result.errors[0]) + ); + else + CHECK( + "Type 'tmp' could not be converted into 'HasSuper'; \n" + "this is because accessing `p` results in `{ x: number, y: number }` in the former type and `Super` in the latter type, and `{ x: " + "number, y: number }` is not exactly `Super`" == toString(result.errors[0]) + ); + } + else if (FFlag::LuauBetterTypeMismatchErrors) + { + const std::string expected = R"(Expected this to be exactly 'HasSuper', but got 'tmp' +caused by: + Property 'p' is not compatible. +Table type '{| x: number, y: number |}' not compatible with type 'Super' because the former has extra field 'y')"; + CHECK_EQ(expected, toString(result.errors[0])); + } else { const std::string expected = R"(Type 'tmp' could not be converted into 'HasSuper' @@ -2783,7 +2894,6 @@ TEST_CASE_FIXTURE(BuiltinsFixture, "give_up_after_one_metatable_index_look_up") TEST_CASE_FIXTURE(Fixture, "confusing_indexing") { ScopedFastFlag sffs[] = { - {FFlag::LuauPushTypeConstraint2, true}, {FFlag::LuauPushTypeConstraintIntersection, true}, }; @@ -3598,9 +3708,18 @@ TEST_CASE_FIXTURE(Fixture, "mixed_tables_with_implicit_numbered_keys") else { LUAU_REQUIRE_ERROR_COUNT(3, result); - CHECK_EQ("Type 'number' could not be converted into 'string'", toString(result.errors[0])); - CHECK_EQ("Type 'number' could not be converted into 'string'", toString(result.errors[1])); - CHECK_EQ("Type 'number' could not be converted into 'string'", toString(result.errors[2])); + if (FFlag::LuauBetterTypeMismatchErrors) + { + CHECK_EQ("Expected this to be 'string', but got 'number'", toString(result.errors[0])); + CHECK_EQ("Expected this to be 'string', but got 'number'", toString(result.errors[1])); + CHECK_EQ("Expected this to be 'string', but got 'number'", toString(result.errors[2])); + } + else + { + CHECK_EQ("Type 'number' could not be converted into 'string'", toString(result.errors[0])); + CHECK_EQ("Type 'number' could not be converted into 'string'", toString(result.errors[1])); + CHECK_EQ("Type 'number' could not be converted into 'string'", toString(result.errors[2])); + } } } @@ -3725,6 +3844,36 @@ TEST_CASE_FIXTURE(Fixture, "scalar_is_not_a_subtype_of_a_compatible_polymorphic_ CHECK("typeof(string)" == toString(tm4->givenType)); CHECK("t1 where t1 = { read absolutely_no_scalar_has_this_method: (t1) -> (a...) }" == toString(tm4->wantedType)); } + else if (FFlag::LuauBetterTypeMismatchErrors) + { + LUAU_REQUIRE_ERROR_COUNT(3, result); + + const std::string expected1 = + R"(Expected this to be 't1 where t1 = {- absolutely_no_scalar_has_this_method: (t1) -> (a...) -}', but got 'string' +caused by: + The given type's metatable does not satisfy the requirements. +Table type 'typeof(string)' not compatible with type 't1 where t1 = {- absolutely_no_scalar_has_this_method: (t1) -> (a...) -}' because the former is missing field 'absolutely_no_scalar_has_this_method')"; + CHECK_EQ(expected1, toString(result.errors[0])); + + const std::string expected2 = + R"(Expected this to be 't1 where t1 = {- absolutely_no_scalar_has_this_method: (t1) -> (a...) -}', but got '"bar"' +caused by: + The given type's metatable does not satisfy the requirements. +Table type 'typeof(string)' not compatible with type 't1 where t1 = {- absolutely_no_scalar_has_this_method: (t1) -> (a...) -}' because the former is missing field 'absolutely_no_scalar_has_this_method')"; + CHECK_EQ(expected2, toString(result.errors[1])); + + const std::string expected3 = R"(Expected this to be + 't1 where t1 = {- absolutely_no_scalar_has_this_method: (t1) -> (a...) -}' +but got + '"bar" | "baz"' +caused by: + Not all union options are compatible. +Expected this to be 't1 where t1 = {- absolutely_no_scalar_has_this_method: (t1) -> (a...) -}', but got '"bar"' +caused by: + The given type's metatable does not satisfy the requirements. +Table type 'typeof(string)' not compatible with type 't1 where t1 = {- absolutely_no_scalar_has_this_method: (t1) -> (a...) -}' because the former is missing field 'absolutely_no_scalar_has_this_method')"; + CHECK_EQ(expected3, toString(result.errors[2])); + } else { LUAU_REQUIRE_ERROR_COUNT(3, result); @@ -3794,6 +3943,19 @@ TEST_CASE_FIXTURE(Fixture, "a_free_shape_cannot_turn_into_a_scalar_if_it_is_not_ CHECK(toString(result.errors[2]) == "Parameter 's' is required to be a subtype of 'string' here."); CHECK_EQ("(never) -> string", toString(requireType("f"))); } + else if (FFlag::LuauBetterTypeMismatchErrors) + { + LUAU_REQUIRE_ERROR_COUNT(1, result); + + const std::string expected = + R"(Expected this to be 'string', but got 't1 where t1 = {+ absolutely_no_scalar_has_this_method: (t1) -> (a, b...) +}' +caused by: + The given type's metatable does not satisfy the requirements. +Table type 'typeof(string)' not compatible with type 't1 where t1 = {+ absolutely_no_scalar_has_this_method: (t1) -> (a, b...) +}' because the former is missing field 'absolutely_no_scalar_has_this_method')"; + CHECK_EQ(expected, toString(result.errors[0])); + + CHECK_EQ("(t1) -> string where t1 = {+ absolutely_no_scalar_has_this_method: (t1) -> (a, b...) +}", toString(requireType("f"))); + } else { LUAU_REQUIRE_ERROR_COUNT(1, result); @@ -5019,12 +5181,19 @@ TEST_CASE_FIXTURE(BuiltinsFixture, "subtyping_with_a_metatable_table_path") CHECK(result.errors.at(2).location == Location{{3, 8}, {5, 11}}); CHECK("Type function instance setmetatable is uninhabited" == toString(result.errors.at(2))); - CHECK( - "Type pack '{ @metatable { }, { } & { } }' could not be converted into 'setmetatable'; \n" - "this is because the 1st entry in the type pack is `{ @metatable { }, { } & { } }` and in the 1st entry in the type packreduces to " - "`never`, and `{ @metatable { }, { } & { } }` is not a subtype of `never`" == - toString(result.errors.at(3)) - ); + if (FFlag::LuauBetterTypeMismatchErrors) + CHECK( + "Expected this to be 'setmetatable', but got '{ @metatable { }, { } & { } }'; \n" + "the 1st entry in the type pack is `{ @metatable { }, { } & { } }` and in the 1st entry in the type packreduces to " + "`never`, and `{ @metatable { }, { } & { } }` is not a subtype of `never`" == toString(result.errors.at(3)) + ); + else + CHECK( + "Type pack '{ @metatable { }, { } & { } }' could not be converted into 'setmetatable'; \n" + "this is because the 1st entry in the type pack is `{ @metatable { }, { } & { } }` and in the 1st entry in the type packreduces " + "to " + "`never`, and `{ @metatable { }, { } & { } }` is not a subtype of `never`" == toString(result.errors.at(3)) + ); } else { @@ -5037,12 +5206,21 @@ TEST_CASE_FIXTURE(BuiltinsFixture, "subtyping_with_a_metatable_table_path") CHECK_EQ(result.errors[1].location, Location{{3, 8}, {5, 11}}); CHECK_EQ("Type function instance setmetatable is uninhabited", toString(result.errors[1])); - CHECK_EQ( - "Type pack '{ @metatable { }, { } & { } }' could not be converted into 'setmetatable'; \n" - "this is because the 1st entry in the type pack is `{ @metatable { }, { } & { } }` and in the 1st entry in the type packreduces to " - "`never`, and `{ @metatable { }, { } & { } }` is not a subtype of `never`", - toString(result.errors[2]) - ); + if (FFlag::LuauBetterTypeMismatchErrors) + CHECK_EQ( + "Expected this to be 'setmetatable', but got '{ @metatable { }, { } & { } }'; \n" + "the 1st entry in the type pack is `{ @metatable { }, { } & { } }` and in the 1st entry in the type packreduces to " + "`never`, and `{ @metatable { }, { } & { } }` is not a subtype of `never`", + toString(result.errors[2]) + ); + else + CHECK_EQ( + "Type pack '{ @metatable { }, { } & { } }' could not be converted into 'setmetatable'; \n" + "this is because the 1st entry in the type pack is `{ @metatable { }, { } & { } }` and in the 1st entry in the type packreduces " + "to " + "`never`, and `{ @metatable { }, { } & { } }` is not a subtype of `never`", + toString(result.errors[2]) + ); } } @@ -5460,7 +5638,7 @@ TEST_CASE_FIXTURE(Fixture, "deeply_nested_classish_inference") TEST_CASE_FIXTURE(Fixture, "bigger_nested_table_causes_big_type_error") { - ScopedFastFlag sffs[] = {{FFlag::LuauSolverV2, true}, {FFlag::LuauPushTypeConstraint2, true}}; + ScopedFastFlag _{FFlag::LuauSolverV2, true}; auto result = check(R"( type File = { @@ -5840,10 +6018,7 @@ TEST_CASE_FIXTURE(Fixture, "narrow_table_literal_check_call_singleton") TEST_CASE_FIXTURE(BuiltinsFixture, "oss_1450") { - ScopedFastFlag sffs[] = { - {FFlag::LuauSolverV2, true}, - {FFlag::LuauPushTypeConstraint2, true}, - }; + ScopedFastFlag _{FFlag::LuauSolverV2, true}; CheckResult results = check(R"( local keycodes = { @@ -5981,7 +6156,6 @@ TEST_CASE_FIXTURE(Fixture, "free_types_with_sealed_table_upper_bounds_can_still_ { ScopedFastFlag sff[] = { {FFlag::LuauSolverV2, true}, - {FFlag::LuauExtendSealedTableUpperBounds, true}, }; CheckResult result = check(R"( @@ -6049,8 +6223,6 @@ TEST_CASE_FIXTURE(BuiltinsFixture, "oss_1935") TEST_CASE_FIXTURE(Fixture, "result_like_tagged_union") { - ScopedFastFlag _{FFlag::LuauPushTypeConstraint2, true}; - LUAU_REQUIRE_NO_ERRORS(check(R"( --!strict local function retry(func: (...any) -> ...any): { type: "ok", value: any } | { type: "failed" } @@ -6069,11 +6241,7 @@ return retry TEST_CASE_FIXTURE(Fixture, "oss_1924") { - ScopedFastFlag sffs[] = { - {FFlag::LuauSolverV2, true}, - {FFlag::LuauPushTypeConstraint2, true}, - {FFlag::LuauPushTypeConstraintSingleton, true}, - }; + ScopedFastFlag _{FFlag::LuauSolverV2, true}; CheckResult result = check(R"( local t: { [string]: "s" } = { @@ -6090,8 +6258,6 @@ TEST_CASE_FIXTURE(Fixture, "oss_1924") TEST_CASE_FIXTURE(BuiltinsFixture, "cli_167052") { - ScopedFastFlag _{FFlag::LuauPushTypeConstraint2, true}; - LUAU_REQUIRE_NO_ERRORS(check(R"( local Children = newproxy() local Macro: { [ string | typeof(Children) ]: true } = { @@ -6170,7 +6336,6 @@ TEST_CASE_FIXTURE(Fixture, "bidirectional_inference_works_through_intersections" { ScopedFastFlag sffs[] = { {FFlag::LuauSolverV2, true}, - {FFlag::LuauPushTypeConstraint2, true}, {FFlag::LuauPushTypeConstraintIntersection, true}, }; @@ -6184,7 +6349,6 @@ TEST_CASE_FIXTURE(Fixture, "bidirectional_inference_intersection_other_intersect { ScopedFastFlag sffs[] = { {FFlag::LuauSolverV2, true}, - {FFlag::LuauPushTypeConstraint2, true}, {FFlag::LuauPushTypeConstraintIntersection, true}, }; @@ -6261,7 +6425,6 @@ TEST_CASE_FIXTURE(Fixture, "array_of_callbacks_bidirectionally_inferred") { ScopedFastFlag sffs[] = { {FFlag::LuauSolverV2, true}, - {FFlag::LuauPushTypeConstraint2, true}, {FFlag::LuauPushTypeConstraintLambdas2, true}, {FFlag::DebugLuauAssertOnForcedConstraint, true}, }; @@ -6285,7 +6448,6 @@ TEST_CASE_FIXTURE(BuiltinsFixture, "oss_1483") { ScopedFastFlag sffs[] = { {FFlag::LuauSolverV2, true}, - {FFlag::LuauPushTypeConstraint2, true}, {FFlag::LuauPushTypeConstraintLambdas2, true}, {FFlag::DebugLuauAssertOnForcedConstraint, true}, }; @@ -6308,7 +6470,6 @@ TEST_CASE_FIXTURE(Fixture, "oss_1910") { ScopedFastFlag sffs[] = { {FFlag::LuauSolverV2, true}, - {FFlag::LuauPushTypeConstraint2, true}, {FFlag::LuauPushTypeConstraintLambdas2, true}, {FFlag::DebugLuauAssertOnForcedConstraint, true}, }; @@ -6331,7 +6492,6 @@ TEST_CASE_FIXTURE(BuiltinsFixture, "bidirectional_inference_variadic_type_pack") { ScopedFastFlag sffs[] = { {FFlag::LuauSolverV2, true}, - {FFlag::LuauPushTypeConstraint2, true}, {FFlag::LuauPushTypeConstraintLambdas2, true}, {FFlag::DebugLuauAssertOnForcedConstraint, true}, }; @@ -6356,7 +6516,6 @@ TEST_CASE_FIXTURE(Fixture, "table_with_intersection_containing_lambda") { ScopedFastFlag sffs[] = { {FFlag::LuauSolverV2, true}, - {FFlag::LuauPushTypeConstraint2, true}, {FFlag::LuauPushTypeConstraintLambdas2, true}, {FFlag::LuauPushTypeConstraintIntersection, true}, {FFlag::DebugLuauAssertOnForcedConstraint, true}, @@ -6450,7 +6609,6 @@ TEST_CASE_FIXTURE(BuiltinsFixture, "oss_1684") { ScopedFastFlag sffs[] = { {FFlag::LuauSolverV2, true}, - {FFlag::LuauPushTypeConstraint2, true}, {FFlag::LuauPushTypeConstraintLambdas2, true}, {FFlag::LuauPushTypeConstraintIntersection, true}, {FFlag::DebugLuauAssertOnForcedConstraint, true}, @@ -6484,7 +6642,6 @@ TEST_CASE_FIXTURE(BuiltinsFixture, "oss_2094_push_type_constraint_should_always_ { ScopedFastFlag sffs[] = { {FFlag::LuauSolverV2, true}, - {FFlag::LuauPushTypeConstraint2, true}, {FFlag::LuauPushTypeConstriantAlwaysCompletes, true}, {FFlag::LuauMarkUnscopedGenericsAsSolved, true}, {FFlag::DebugLuauAssertOnForcedConstraint, true}, @@ -6512,7 +6669,6 @@ TEST_CASE_FIXTURE(Fixture, "table_access_indexer_via_name_expr") { ScopedFastFlag sffs[] = { {FFlag::LuauSolverV2, true}, - {FFlag::LuauPushTypeConstraint2, true}, {FFlag::LuauUseFastSubtypeForIndexerWithName, true}, }; @@ -6530,7 +6686,6 @@ TEST_CASE_FIXTURE(Fixture, "table_access_indexer_fails_with_missing_key") { ScopedFastFlag sffs[] = { {FFlag::LuauSolverV2, true}, - {FFlag::LuauPushTypeConstraint2, true}, {FFlag::LuauUseFastSubtypeForIndexerWithName, true}, }; diff --git a/tests/TypeInfer.test.cpp b/tests/TypeInfer.test.cpp index 3e51340cf..163bf5691 100644 --- a/tests/TypeInfer.test.cpp +++ b/tests/TypeInfer.test.cpp @@ -29,11 +29,12 @@ LUAU_FASTINT(LuauTypeInferRecursionLimit) LUAU_FASTFLAG(LuauDfgAllowUpdatesInLoops) LUAU_FASTFLAG(DebugLuauMagicTypes) LUAU_FASTFLAG(LuauMissingFollowMappedGenericPacks) -LUAU_FASTFLAG(LuauEGFixGenericsList) LUAU_FASTFLAG(LuauTryToOptimizeSetTypeUnification) -LUAU_FASTFLAG(LuauConsiderErrorSuppressionInTypes) LUAU_FASTFLAG(LuauMetatableAvoidSingletonUnion) LUAU_FASTFLAG(LuauUnknownGlobalFixSuggestion) +LUAU_FASTFLAG(LuauBetterTypeMismatchErrors) +LUAU_FASTFLAG(DebugLuauForbidInternalTypes) +LUAU_FASTFLAG(LuauAvoidMintingMultipleBlockedTypesForGlobals) using namespace Luau; @@ -1158,22 +1159,39 @@ TEST_CASE_FIXTURE(Fixture, "cli_50041_committing_txnlog_in_apollo_client_error") LUAU_REQUIRE_ERROR_COUNT(1, result); const std::string expected = - "Type 'Policies' from 'MainModule' could not be converted into 'Policies' from 'MainModule'" - "\ncaused by:\n" - " Property 'getStoreFieldName' is not compatible.\n" - "Type\n\t" - "'(Policies, FieldSpecifier & { from: number? }) -> ('a, b...)'" - "\ncould not be converted into\n\t" - "'(Policies, FieldSpecifier) -> string'" - "\ncaused by:\n" - " Argument #2 type is not compatible.\n" - "Type\n\t" - "'FieldSpecifier'" - "\ncould not be converted into\n\t" - "'FieldSpecifier & { from: number? }'" - "\ncaused by:\n" - " Not all intersection parts are compatible.\n" - "Table type 'FieldSpecifier' not compatible with type '{ from: number? }' because the former has extra field 'fieldName'"; + FFlag::LuauBetterTypeMismatchErrors + ? "Expected this to be exactly 'Policies' from 'MainModule', but got 'Policies' from 'MainModule'" + "\ncaused by:\n" + " Property 'getStoreFieldName' is not compatible.\n" + "Expected this to be exactly\n\t" + "'(Policies, FieldSpecifier) -> string'" + "\nbut got\n\t" + "'(Policies, FieldSpecifier & { from: number? }) -> ('a, b...)'" + "\ncaused by:\n" + " Argument #2 type is not compatible.\n" + "Expected this to be exactly\n\t" + "'FieldSpecifier & { from: number? }'" + "\nbut got\n\t" + "'FieldSpecifier'" + "\ncaused by:\n" + " Not all intersection parts are compatible.\n" + "Table type 'FieldSpecifier' not compatible with type '{ from: number? }' because the former has extra field 'fieldName'" + : "Type 'Policies' from 'MainModule' could not be converted into 'Policies' from 'MainModule'" + "\ncaused by:\n" + " Property 'getStoreFieldName' is not compatible.\n" + "Type\n\t" + "'(Policies, FieldSpecifier & { from: number? }) -> ('a, b...)'" + "\ncould not be converted into\n\t" + "'(Policies, FieldSpecifier) -> string'" + "\ncaused by:\n" + " Argument #2 type is not compatible.\n" + "Type\n\t" + "'FieldSpecifier'" + "\ncould not be converted into\n\t" + "'FieldSpecifier & { from: number? }'" + "\ncaused by:\n" + " Not all intersection parts are compatible.\n" + "Table type 'FieldSpecifier' not compatible with type '{ from: number? }' because the former has extra field 'fieldName'"; CHECK_EQ(expected, toString(result.errors[0])); } else @@ -2632,8 +2650,6 @@ TEST_CASE_FIXTURE(Fixture, "constraint_generation_recursion_limit") // https://github.com/luau-lang/luau/issues/1971 TEST_CASE_FIXTURE(Fixture, "nested_functions_can_depend_on_outer_generics") { - ScopedFastFlag sff{FFlag::LuauEGFixGenericsList, true}; - CheckResult result = check(R"( function name

(arg1: P) return function(what: P) return what end @@ -2697,7 +2713,6 @@ export type t12 = { TEST_CASE_FIXTURE(BuiltinsFixture, "any_type_in_function_argument_should_not_error") { - ScopedFastFlag sff{FFlag::LuauConsiderErrorSuppressionInTypes, true}; CheckResult result = check(R"( --!strict local function f(u: string) end @@ -2723,4 +2738,36 @@ TEST_CASE_FIXTURE(BuiltinsFixture, "fuzz_avoid_singleton_union") )")); } +TEST_CASE_FIXTURE(Fixture, "captured_globals_are_not_blocked") +{ + ScopedFastFlag sffs[] = { + {FFlag::DebugLuauForbidInternalTypes, true}, + {FFlag::LuauAvoidMintingMultipleBlockedTypesForGlobals, true}, + }; + + // We do not care about the errors here, only that there are no internal + // types in the final typed AST. + LUAU_REQUIRE_ERRORS(check(R"( + --!strict + local Cancelled: boolean = false + + function Start() + if Cancelled then + return + end + Selection = 42 + local _ = function () + if Selection then + end + end + end + + function Cancel() + Selection = nil + end + + return {} + )")); +} + TEST_SUITE_END(); diff --git a/tests/TypeInfer.typeInstantiations.test.cpp b/tests/TypeInfer.typeInstantiations.test.cpp index 46a9ff89b..287b2527a 100644 --- a/tests/TypeInfer.typeInstantiations.test.cpp +++ b/tests/TypeInfer.typeInstantiations.test.cpp @@ -7,6 +7,7 @@ using namespace Luau; LUAU_FASTFLAG(LuauSolverV2) LUAU_FASTFLAG(LuauExplicitTypeExpressionInstantiation) +LUAU_FASTFLAG(LuauBetterTypeMismatchErrors) TEST_SUITE_BEGIN("TypeInferExplicitTypeInstantiations"); @@ -58,6 +59,10 @@ TEST_CASE_FIXTURE(Fixture, "as_expression_incorrect") "Operator '+' could not be applied to operands of types string and number; there is no corresponding overload for __add" ); } + else if (FFlag::LuauBetterTypeMismatchErrors) + { + REQUIRE_EQ(toString(result.errors[0]), "Expected this to be 'number', but got 'string'"); + } else { REQUIRE_EQ(toString(result.errors[0]), "Type 'string' could not be converted into 'number'"); @@ -102,13 +107,21 @@ TEST_CASE_FIXTURE(Fixture, "as_stmt_incorrect") LUAU_REQUIRE_ERROR_COUNT(1, result); if (FFlag::LuauSolverV2) { - REQUIRE_EQ(toString(result.errors[0]), "Type 'string' could not be converted into 'boolean | number'"); + if (FFlag::LuauBetterTypeMismatchErrors) + REQUIRE_EQ(toString(result.errors[0]), "Expected this to be 'boolean | number', but got 'string'"); + else + REQUIRE_EQ(toString(result.errors[0]), "Type 'string' could not be converted into 'boolean | number'"); } else { - REQUIRE_EQ( - toString(result.errors[0]), "Type 'string' could not be converted into 'boolean | number'; none of the union options are compatible" - ); + if (FFlag::LuauBetterTypeMismatchErrors) + REQUIRE_EQ( + toString(result.errors[0]), "Expected this to be 'boolean | number', but got 'string'; none of the union options are compatible" + ); + else + REQUIRE_EQ( + toString(result.errors[0]), "Type 'string' could not be converted into 'boolean | number'; none of the union options are compatible" + ); } } } diff --git a/tests/TypeInfer.typePacks.test.cpp b/tests/TypeInfer.typePacks.test.cpp index ac41a88b2..b07371545 100644 --- a/tests/TypeInfer.typePacks.test.cpp +++ b/tests/TypeInfer.typePacks.test.cpp @@ -11,6 +11,7 @@ using namespace Luau; LUAU_FASTFLAG(LuauSolverV2) +LUAU_FASTFLAG(LuauBetterTypeMismatchErrors) LUAU_FASTFLAG(LuauInstantiateInSubtyping) TEST_SUITE_BEGIN("TypePackTests"); @@ -928,15 +929,34 @@ a = b if (FFlag::LuauSolverV2) { - const std::string expected = "Type\n\t" - "'() -> (number, ...boolean)'" - "\ncould not be converted into\n\t" - "'() -> (number, ...string)'; \n" - "this is because it returns a tail of the variadic `boolean` in the former type and `string` in the latter " - "type, and `boolean` is not a subtype of `string`"; + const std::string expected = + FFlag::LuauBetterTypeMismatchErrors + ? "Expected this to be\n\t" + "'() -> (number, ...string)'" + "\nbut got\n\t" + "'() -> (number, ...boolean)'" + "; \n" + "it returns a tail of the variadic `boolean` in the latter type and `string` in the former " + "type, and `boolean` is not a subtype of `string`" + : "Type\n\t" + "'() -> (number, ...boolean)'" + "\ncould not be converted into\n\t" + "'() -> (number, ...string)'; \n" + "this is because it returns a tail of the variadic `boolean` in the former type and `string` in the latter " + "type, and `boolean` is not a subtype of `string`"; CHECK(expected == toString(result.errors[0])); } + else if (FFlag::LuauBetterTypeMismatchErrors) + { + const std::string expected = R"(Expected this to be + '() -> (number, ...string)' +but got + '() -> (number, ...boolean)' +caused by: + Expected this to be 'string', but got 'boolean')"; + CHECK_EQ(expected, toString(result.errors[0])); + } else { const std::string expected = R"(Type @@ -1054,7 +1074,10 @@ TEST_CASE_FIXTURE(Fixture, "unify_variadic_tails_in_arguments") )"); LUAU_REQUIRE_ERROR_COUNT(1, result); - CHECK_EQ(toString(result.errors[0]), "Type 'number' could not be converted into 'string'"); + if (FFlag::LuauBetterTypeMismatchErrors) + CHECK_EQ(toString(result.errors[0]), "Expected this to be 'string', but got 'number'"); + else + CHECK_EQ(toString(result.errors[0]), "Type 'number' could not be converted into 'string'"); } TEST_CASE_FIXTURE(Fixture, "unify_variadic_tails_in_arguments_free") @@ -1071,10 +1094,20 @@ TEST_CASE_FIXTURE(Fixture, "unify_variadic_tails_in_arguments_free") LUAU_REQUIRE_ERROR_COUNT(1, result); if (FFlag::LuauSolverV2) - CHECK( - toString(result.errors.at(0)) == "Type pack '...number' could not be converted into 'boolean'; \nthis is because it has a tail of " - "`...number`, which is not a subtype of `boolean`" - ); + { + if (FFlag::LuauBetterTypeMismatchErrors) + CHECK( + toString(result.errors.at(0)) == "Expected this to be 'boolean', but got '...number'; \n" + "it has a tail of `...number`, which is not a subtype of `boolean`" + ); + else + CHECK( + toString(result.errors.at(0)) == "Type pack '...number' could not be converted into 'boolean'; \nthis is because it has a tail of " + "`...number`, which is not a subtype of `boolean`" + ); + } + else if (FFlag::LuauBetterTypeMismatchErrors) + CHECK_EQ(toString(result.errors[0]), "Expected this to be 'boolean', but got 'number'"); else CHECK_EQ(toString(result.errors[0]), "Type 'number' could not be converted into 'boolean'"); } diff --git a/tests/TypeInfer.typestates.test.cpp b/tests/TypeInfer.typestates.test.cpp index ba0e7a4a6..945949ed0 100644 --- a/tests/TypeInfer.typestates.test.cpp +++ b/tests/TypeInfer.typestates.test.cpp @@ -5,6 +5,7 @@ LUAU_FASTFLAG(LuauSolverV2) LUAU_FASTFLAG(LuauRefineDistributesOverUnions) +LUAU_FASTFLAG(LuauBetterTypeMismatchErrors) using namespace Luau; @@ -136,7 +137,10 @@ TEST_CASE_FIXTURE(TypeStateFixture, "assign_a_local_and_then_refine_it") )"); LUAU_REQUIRE_ERROR_COUNT(1, result); - CHECK("Type 'string' could not be converted into 'never'" == toString(result.errors[0])); + if (FFlag::LuauBetterTypeMismatchErrors) + CHECK("Expected this to be unreachable, but got 'string'" == toString(result.errors[0])); + else + CHECK("Type 'string' could not be converted into 'never'" == toString(result.errors[0])); } TEST_CASE_FIXTURE(TypeStateFixture, "recursive_local_function") diff --git a/tests/TypeInfer.unionTypes.test.cpp b/tests/TypeInfer.unionTypes.test.cpp index 7e04ab768..1244473bd 100644 --- a/tests/TypeInfer.unionTypes.test.cpp +++ b/tests/TypeInfer.unionTypes.test.cpp @@ -9,6 +9,7 @@ using namespace Luau; LUAU_FASTFLAG(LuauSolverV2) +LUAU_FASTFLAG(LuauBetterTypeMismatchErrors) TEST_SUITE_BEGIN("UnionTypes"); @@ -537,15 +538,31 @@ end if (FFlag::LuauSolverV2) { - - CHECK_EQ( - toString(result.errors[0]), - "Type 'X | Y | Z' could not be converted into '{ w: number }'; \n" - "this is because \n\t" - " * the 1st component of the union is `X`, which is not a subtype of `{ w: number }`\n\t" - " * the 2nd component of the union is `Y`, which is not a subtype of `{ w: number }`\n\t" - " * the 3rd component of the union is `Z`, which is not a subtype of `{ w: number }`" - ); + if (FFlag::LuauBetterTypeMismatchErrors) + CHECK_EQ( + toString(result.errors[0]), + "Expected this to be '{ w: number }', but got 'X | Y | Z'; \n" + "this is because \n\t" + " * the 1st component of the union is `X`, which is not a subtype of `{ w: number }`\n\t" + " * the 2nd component of the union is `Y`, which is not a subtype of `{ w: number }`\n\t" + " * the 3rd component of the union is `Z`, which is not a subtype of `{ w: number }`" + ); + else + CHECK_EQ( + toString(result.errors[0]), + "Type 'X | Y | Z' could not be converted into '{ w: number }'; \n" + "this is because \n\t" + " * the 1st component of the union is `X`, which is not a subtype of `{ w: number }`\n\t" + " * the 2nd component of the union is `Y`, which is not a subtype of `{ w: number }`\n\t" + " * the 3rd component of the union is `Z`, which is not a subtype of `{ w: number }`" + ); + } + else if (FFlag::LuauBetterTypeMismatchErrors) + { + CHECK_EQ(toString(result.errors[0]), R"(Expected this to be '{ w: number }', but got 'X | Y | Z' +caused by: + Not all union options are compatible. +Table type 'X' not compatible with type '{ w: number }' because the former is missing field 'w')"); } else { @@ -570,7 +587,14 @@ TEST_CASE_FIXTURE(Fixture, "error_detailed_union_all") LUAU_REQUIRE_ERROR_COUNT(1, result); if (FFlag::LuauSolverV2) - CHECK(toString(result.errors[0]) == "Type '{ w: number }' could not be converted into 'X | Y | Z'"); + { + if (FFlag::LuauBetterTypeMismatchErrors) + CHECK(toString(result.errors[0]) == "Expected this to be 'X | Y | Z', but got '{ w: number }'"); + else + CHECK(toString(result.errors[0]) == "Type '{ w: number }' could not be converted into 'X | Y | Z'"); + } + else if (FFlag::LuauBetterTypeMismatchErrors) + CHECK_EQ(toString(result.errors[0]), R"(Expected this to be 'X | Y | Z', but got 'a'; none of the union options are compatible)"); else CHECK_EQ(toString(result.errors[0]), R"(Type 'a' could not be converted into 'X | Y | Z'; none of the union options are compatible)"); } @@ -586,6 +610,14 @@ local a: X? = { w = 4 } LUAU_REQUIRE_ERROR_COUNT(1, result); if (FFlag::LuauSolverV2) CHECK("Table type '{ w: number }' not compatible with type 'X' because the former is missing field 'x'" == toString(result.errors[0])); + else if (FFlag::LuauBetterTypeMismatchErrors) + { + const std::string expected = R"(Expected this to be 'X?', but got 'a' +caused by: + None of the union options are compatible. For example: +Table type 'a' not compatible with type 'X' because the former is missing field 'x')"; + CHECK_EQ(expected, toString(result.errors[0])); + } else { const std::string expected = R"(Type 'a' could not be converted into 'X?' @@ -673,10 +705,16 @@ TEST_CASE_FIXTURE(BuiltinsFixture, "table_union_write_indirect") LUAU_REQUIRE_ERROR_COUNT(1, result); // NOTE: union normalization will improve this message - const std::string expected = "Type\n\t" - "'(string) -> number'" - "\ncould not be converted into\n\t" - "'((number) -> string) | ((number) -> string)'; none of the union options are compatible"; + const std::string expected = FFlag::LuauBetterTypeMismatchErrors + ? "Expected this to be\n\t" + "'((number) -> string) | ((number) -> string)'" + "\nbut got\n\t" + "'(string) -> number'" + "; none of the union options are compatible" + : "Type\n\t" + "'(string) -> number'" + "\ncould not be converted into\n\t" + "'((number) -> string) | ((number) -> string)'; none of the union options are compatible"; CHECK_EQ(expected, toString(result.errors[0])); } @@ -743,10 +781,17 @@ TEST_CASE_FIXTURE(Fixture, "union_of_functions_mentioning_generics") )"); LUAU_REQUIRE_ERROR_COUNT(1, result); - CHECK_EQ( - toString(result.errors[0]), - "Type '(a) -> a?' could not be converted into '((b) -> b) | ((b?) -> nil)'; none of the union options are compatible" - ); + + if (FFlag::LuauBetterTypeMismatchErrors) + CHECK_EQ( + toString(result.errors[0]), + "Expected this to be '((b) -> b) | ((b?) -> nil)', but got '(a) -> a?'; none of the union options are compatible" + ); + else + CHECK_EQ( + toString(result.errors[0]), + "Type '(a) -> a?' could not be converted into '((b) -> b) | ((b?) -> nil)'; none of the union options are compatible" + ); } TEST_CASE_FIXTURE(Fixture, "union_of_functions_mentioning_generic_typepacks") @@ -764,10 +809,16 @@ TEST_CASE_FIXTURE(Fixture, "union_of_functions_mentioning_generic_typepacks") LUAU_REQUIRE_ERROR_COUNT(1, result); - const std::string expected = "Type\n\t" - "'(number, a...) -> (number?, a...)'" - "\ncould not be converted into\n\t" - "'((number) -> number) | ((number?, a...) -> (number?, a...))'; none of the union options are compatible"; + const std::string expected = FFlag::LuauBetterTypeMismatchErrors + ? "Expected this to be\n\t" + "'((number) -> number) | ((number?, a...) -> (number?, a...))'" + "\nbut got\n\t" + "'(number, a...) -> (number?, a...)'" + "; none of the union options are compatible" + : "Type\n\t" + "'(number, a...) -> (number?, a...)'" + "\ncould not be converted into\n\t" + "'((number) -> number) | ((number?, a...) -> (number?, a...))'; none of the union options are compatible"; CHECK_EQ(expected, toString(result.errors[0])); } @@ -784,10 +835,16 @@ TEST_CASE_FIXTURE(Fixture, "union_of_functions_with_mismatching_arg_arities") LUAU_REQUIRE_ERROR_COUNT(1, result); - const std::string expected = "Type\n\t" - "'(number) -> number?'" - "\ncould not be converted into\n\t" - "'((number) -> nil) | ((number, string?) -> number)'; none of the union options are compatible"; + const std::string expected = FFlag::LuauBetterTypeMismatchErrors + ? "Expected this to be\n\t" + "'((number) -> nil) | ((number, string?) -> number)'" + "\nbut got\n\t" + "'(number) -> number?'" + "; none of the union options are compatible" + : "Type\n\t" + "'(number) -> number?'" + "\ncould not be converted into\n\t" + "'((number) -> nil) | ((number, string?) -> number)'; none of the union options are compatible"; CHECK_EQ(expected, toString(result.errors[0])); } @@ -804,10 +861,16 @@ TEST_CASE_FIXTURE(Fixture, "union_of_functions_with_mismatching_result_arities") LUAU_REQUIRE_ERROR_COUNT(1, result); - const std::string expected = "Type\n\t" - "'() -> number | string'" - "\ncould not be converted into\n\t" - "'(() -> (string, string)) | (() -> number)'; none of the union options are compatible"; + const std::string expected = FFlag::LuauBetterTypeMismatchErrors + ? "Expected this to be\n\t" + "'(() -> (string, string)) | (() -> number)'" + "\nbut got\n\t" + "'() -> number | string'" + "; none of the union options are compatible" + : "Type\n\t" + "'() -> number | string'" + "\ncould not be converted into\n\t" + "'(() -> (string, string)) | (() -> number)'; none of the union options are compatible"; CHECK_EQ(expected, toString(result.errors[0])); } @@ -824,10 +887,16 @@ TEST_CASE_FIXTURE(Fixture, "union_of_functions_with_variadics") LUAU_REQUIRE_ERROR_COUNT(1, result); - const std::string expected = "Type\n\t" - "'(...nil) -> (...number?)'" - "\ncould not be converted into\n\t" - "'((...string?) -> (...number)) | ((...string?) -> nil)'; none of the union options are compatible"; + const std::string expected = FFlag::LuauBetterTypeMismatchErrors + ? "Expected this to be\n\t" + "'((...string?) -> (...number)) | ((...string?) -> nil)'" + "\nbut got\n\t" + "'(...nil) -> (...number?)'" + "; none of the union options are compatible" + : "Type\n\t" + "'(...nil) -> (...number?)'" + "\ncould not be converted into\n\t" + "'((...string?) -> (...number)) | ((...string?) -> nil)'; none of the union options are compatible"; CHECK_EQ(expected, toString(result.errors[0])); } @@ -843,12 +912,24 @@ TEST_CASE_FIXTURE(Fixture, "union_of_functions_with_mismatching_arg_variadics") LUAU_REQUIRE_ERROR_COUNT(1, result); if (FFlag::LuauSolverV2) { - const std::string expected = "Type\n\t" - "'(number) -> ()'" - "\ncould not be converted into\n\t" - "'((...number?) -> ()) | ((number?) -> ())'"; + const std::string expected = FFlag::LuauBetterTypeMismatchErrors ? "Expected this to be\n\t" + "'((...number?) -> ()) | ((number?) -> ())'" + "\nbut got\n\t" + "'(number) -> ()'" + : "Type\n\t" + "'(number) -> ()'" + "\ncould not be converted into\n\t" + "'((...number?) -> ()) | ((number?) -> ())'"; CHECK(expected == toString(result.errors[0])); } + else if (FFlag::LuauBetterTypeMismatchErrors) + { + const std::string expected = R"(Expected this to be + '((...number?) -> ()) | ((number?) -> ())' +but got + '(number) -> ()'; none of the union options are compatible)"; + CHECK_EQ(expected, toString(result.errors[0])); + } else { const std::string expected = R"(Type @@ -872,10 +953,16 @@ TEST_CASE_FIXTURE(Fixture, "union_of_functions_with_mismatching_result_variadics LUAU_REQUIRE_ERROR_COUNT(1, result); - const std::string expected = "Type\n\t" - "'() -> (number?, ...number)'" - "\ncould not be converted into\n\t" - "'(() -> (...number)) | (() -> number)'; none of the union options are compatible"; + const std::string expected = FFlag::LuauBetterTypeMismatchErrors + ? "Expected this to be\n\t" + "'(() -> (...number)) | (() -> number)'" + "\nbut got\n\t" + "'() -> (number?, ...number)'" + "; none of the union options are compatible" + : "Type\n\t" + "'() -> (number?, ...number)'" + "\ncould not be converted into\n\t" + "'(() -> (...number)) | (() -> number)'; none of the union options are compatible"; CHECK_EQ(expected, toString(result.errors[0])); } diff --git a/tests/VisitType.test.cpp b/tests/VisitType.test.cpp index 4b604f50a..63541f41d 100644 --- a/tests/VisitType.test.cpp +++ b/tests/VisitType.test.cpp @@ -5,6 +5,7 @@ #include "Luau/RecursionCounter.h" #include "Luau/Type.h" +#include "Luau/IterativeTypeVisitor.h" #include "doctest.h" using namespace Luau; @@ -70,4 +71,179 @@ TEST_CASE_FIXTURE(Fixture, "some_free_types_have_bounds") CHECK("('a <: number)" == toString(&t)); } +struct TracingVisitor : IterativeTypeVisitor +{ + std::vector trace; + std::vector cycles; + + TracingVisitor(bool visitOnce, bool skipBoundTypes) + : IterativeTypeVisitor("TracingVisitor", visitOnce, skipBoundTypes) + {} + + void cycle(TypeId ty) override + { + cycles.emplace_back(ty); + } + + bool visit(TypeId ty) override + { + trace.emplace_back(toString(ty)); + return true; + } +}; + +TEST_CASE_FIXTURE(Fixture, "trace_a_simple_function") +{ + TypeId a = parseType("(number, string) -> boolean"); + + TracingVisitor vis(true, true); + vis.run(a); + + CHECK(4 == vis.trace.size()); + CHECK(vis.trace.at(0) == "(number, string) -> boolean"); + CHECK(vis.trace.at(1) == "number"); + CHECK(vis.trace.at(2) == "string"); + CHECK(vis.trace.at(3) == "boolean"); +} + +struct TableSkippingVisitor : IterativeTypeVisitor +{ + TableSkippingVisitor() + : IterativeTypeVisitor("TracingVisitor", /*visitOnce*/ true, /*skipBoundTypes*/ true) + {} + + std::vector trace; + + bool visit(TypeId ty) override + { + trace.emplace_back(toString(ty)); + return true; + } + + bool visit(TypeId ty, const TableType& tt) override + { + return false; + } +}; + +TEST_CASE_FIXTURE(Fixture, "skip_over_tables") +{ + TypeId a = parseType("(number, string, {x: number, y: number}) -> {x: number, y: number}"); + + TableSkippingVisitor vis; + vis.run(a); + + CHECK(3 == vis.trace.size()); + CHECK(vis.trace.at(0) == "(number, string, { x: number, y: number }) -> { x: number, y: number }"); + CHECK(vis.trace.at(1) == "number"); + CHECK(vis.trace.at(2) == "string"); +} + +TEST_CASE_FIXTURE(Fixture, "detects_cycles") +{ + // Alas. parseType() can't be used to create a cyclic type. + // F where + // F = (T, number) -> number + // T = {method: F} + + TypeId fType = arena.addType(BlockedType{}); + + TypeId tType = arena.addType(TableType{ + TableType::Props{{"method", fType}}, + std::nullopt, + TypeLevel{}, + TableState::Sealed + }); + + asMutable(fType)->ty.emplace( + arena.addTypePack({tType, getBuiltins()->numberType}), + getBuiltins()->emptyTypePack + ); + + TracingVisitor vis(true, true); + vis.run(fType); + + CHECK(3 == vis.trace.size()); + CHECK(vis.trace.at(0) == "t1 where t1 = ({ method: t1 }, number) -> ()"); + CHECK(vis.trace.at(1) == "t1 where t1 = { method: (t1, number) -> () }"); + CHECK(vis.trace.at(2) == "number"); + + CHECK(1 == vis.cycles.size()); + CHECK("t1 where t1 = ({ method: t1 }, number) -> ()" == toString(vis.cycles.at(0))); +} + +TEST_CASE_FIXTURE(Fixture, "skips_bound_types") +{ + TypeId a = arena.addType(BoundType{getBuiltins()->numberType}); + + TracingVisitor vis(true, true); + vis.run(a); + + CHECK(1 == vis.trace.size()); + CHECK("number" == vis.trace.at(0)); +} + +TEST_CASE_FIXTURE(Fixture, "can_be_configured_not_to_skip_bound_types") +{ + TypeId a = arena.addType(BoundType{getBuiltins()->numberType}); + + TracingVisitor vis(true, false); + vis.run(a); + + CHECK(2 == vis.trace.size()); + CHECK("number" == vis.trace.at(0)); + CHECK("number" == vis.trace.at(1)); +} + +TEST_CASE_FIXTURE(Fixture, "visitOnce") +{ + // An acyclic type that has redundant interior structure. + // ({x: number}, {x: number}) -> {x: number} + + TypeId xTable = arena.addType(TableType{ + TableType::Props{{"x", getBuiltins()->numberType}}, + std::nullopt, + TypeLevel{}, + TableState::Sealed + }); + + TypeId fnTy = arena.addType(FunctionType{ + arena.addTypePack({xTable, xTable}), + arena.addTypePack({xTable}) + }); + + SUBCASE("visitOnce_true") + { + TracingVisitor vis(true, true); + vis.run(fnTy); + + CHECK(3 == vis.trace.size()); + CHECK(vis.trace.at(0) == "({ x: number }, { x: number }) -> { x: number }"); + CHECK(vis.trace.at(1) == "{ x: number }"); + CHECK(vis.trace.at(2) == "number"); + + CHECK(0 == vis.cycles.size()); + } + + SUBCASE("visitOnce_false") + { + TracingVisitor vis(false, true); + vis.run(fnTy); + + CHECK(7 == vis.trace.size()); + CHECK(vis.trace.at(0) == "({ x: number }, { x: number }) -> { x: number }"); + CHECK(vis.trace.at(1) == "{ x: number }"); + CHECK(vis.trace.at(2) == "{ x: number }"); + CHECK(vis.trace.at(3) == "{ x: number }"); + CHECK(vis.trace.at(4) == "number"); + CHECK(vis.trace.at(5) == "number"); + CHECK(vis.trace.at(6) == "number"); + + CHECK(0 == vis.cycles.size()); + } +} + +// visitOnce +// RecursionLimiter (and note that it doesn't trip for the iterative variation) + TEST_SUITE_END(); diff --git a/tests/conformance/buffers.luau b/tests/conformance/buffers.luau index 370fb8a8b..8af072a97 100644 --- a/tests/conformance/buffers.luau +++ b/tests/conformance/buffers.luau @@ -599,6 +599,78 @@ end misc(table.create(16, 0)) +local function storeloadpreserve(n, m, f, ...) + local b = buffer.create(1000) + + buffer.writei8(b, 0, n) + buffer.writei8(b, 1, m) + buffer.writef64(b, 100, buffer.readi8(b, 0)) + buffer.writef64(b, 108, buffer.readi8(b, 1)) + + buffer.writeu8(b, 2, n) + buffer.writeu8(b, 3, m) + buffer.writef64(b, 116, buffer.readu8(b, 2)) + buffer.writef64(b, 124, buffer.readu8(b, 3)) + + buffer.writei16(b, 4, n) + buffer.writei16(b, 6, m) + buffer.writef64(b, 132, buffer.readi16(b, 4)) + buffer.writef64(b, 140, buffer.readi16(b, 6)) + + buffer.writeu16(b, 8, n) + buffer.writeu16(b, 10, m) + buffer.writef64(b, 148, buffer.readu16(b, 8)) + buffer.writef64(b, 156, buffer.readu16(b, 10)) + + buffer.writei32(b, 12, n) + buffer.writei32(b, 16, m) + buffer.writef64(b, 164, buffer.readi32(b, 12)) + buffer.writef64(b, 172, buffer.readi32(b, 16)) + + buffer.writeu32(b, 20, n) + buffer.writeu32(b, 24, m) + buffer.writef64(b, 180, buffer.readu32(b, 20)) + buffer.writef64(b, 188, buffer.readu32(b, 24)) + + buffer.writef32(b, 28, f) + buffer.writef64(b, 196, buffer.readf32(b, 28)) + + buffer.writef64(b, 32, f) + buffer.writef64(b, 204, buffer.readf64(b, 32)) + + assert(buffer.readi8(b, 0) == 0x12) + assert(buffer.readi8(b, 1) == -0x68) + assert(buffer.readu8(b, 2) == 0x12) + assert(buffer.readu8(b, 3) == 0x98) + assert(buffer.readi16(b, 4) == 0x7812) + assert(buffer.readi16(b, 6) == -0x4568) + assert(buffer.readu16(b, 8) == 0x7812) + assert(buffer.readu16(b, 10) == 0xba98) + assert(buffer.readi32(b, 12) == 0x34567812) + assert(buffer.readi32(b, 16) == -0x01234568) + assert(buffer.readu32(b, 20) == 0x34567812) + assert(buffer.readu32(b, 24) == 0xfedcba98) + assert(buffer.readf32(b, 28) == math.huge) + assert(buffer.readf64(b, 32) == 1e100) + + assert(buffer.readf64(b, 100) == 0x12) + assert(buffer.readf64(b, 108) == -0x68) + assert(buffer.readf64(b, 116) == 0x12) + assert(buffer.readf64(b, 124) == 0x98) + assert(buffer.readf64(b, 132) == 0x7812) + assert(buffer.readf64(b, 140) == -0x4568) + assert(buffer.readf64(b, 148) == 0x7812) + assert(buffer.readf64(b, 156) == 0xba98) + assert(buffer.readf64(b, 164) == 0x34567812) + assert(buffer.readf64(b, 172) == -0x01234568) + assert(buffer.readf64(b, 180) == 0x34567812) + assert(buffer.readf64(b, 188) == 0xfedcba98) + assert(buffer.readf64(b, 196) == math.huge) + assert(buffer.readf64(b, 204) == 1e100) +end + +storeloadpreserve(0x1234567812, 0x12fedcba98, 1e100) + local function bitops(size, base) local b = buffer.create(size) diff --git a/tests/conformance/interrupt.luau b/tests/conformance/interrupt.luau index 86712be57..73a7b831f 100644 --- a/tests/conformance/interrupt.luau +++ b/tests/conformance/interrupt.luau @@ -78,29 +78,40 @@ end local haystack = string.rep("x", 100) local pattern = string.rep("x?", 100) .. string.rep("x", 100) -function strhang1() +function hang1() string.find(haystack, pattern) end -function strhang2() +function hang2() string.match(haystack, pattern) end -function strhang3() +function hang3() string.gsub(haystack, pattern, "%0") end -function strhang4() +function hang4() for k, v in string.gmatch(haystack, pattern) do end end -function strhang5() +function hang5() local x = string.rep('x', 1000) string.match(x, string.rep('x.*', 100) .. 'y') end -function strhangpcall() +function hang6() + -- the reason an interrupt in RETURN is needed, CALL alone is not enough + local obj = setmetatable({}, { + __index = function(obj, n) + return if n < 2 then 1 else obj[n - 1] + obj[n - 2] + end + }) + + print(obj[100]) +end + +function hangpcall() for i = 1,100 do local status, msg = pcall(string.find, haystack, pattern) assert(status == false) diff --git a/tests/conformance/native.luau b/tests/conformance/native.luau index 39be8afc3..b316ab11a 100644 --- a/tests/conformance/native.luau +++ b/tests/conformance/native.luau @@ -248,6 +248,44 @@ end assert(pcall(fuzzfail24) == false) +local function fuzzfail25(...) + local _ = fill + while {[_ + _]={[bit32.bxor(65535,65535,_)]=bit32.bxor(-1610607103,65535,65535,_),},_=bit32.bxor(-1610607103,65535,65535,_),[538973697]=_,[_]=_,} do + end +end + +assert(pcall(fuzzfail25) == false) + +local function fuzzfail26(...) + local loops = 10 + for l0 in pcall,function(...):any + for _ in xpcall,_,_,_ do + end + function _():any + end + l0,l0._G,l0 = _,_ + end do + function _():any + l0,l0 = 0,{} + _() + end + + loops -= 1 + if loops == 0 then break end + end + return true +end + +assert(pcall(fuzzfail26)) + +local function fuzzfail27(...) + for _ = 1,67 do + assert(_, _ .. _ or _, {x=_ .. _ // _ .. _ or _ .. _,}) + end +end + +assert(pcall(fuzzfail27)) + local function arraySizeInv1() local t = {1, 2, nil, nil, nil, nil, nil, nil, nil, true} diff --git a/tests/conformance/native_integer_spills.luau b/tests/conformance/native_integer_spills.luau index 0780b6578..b98aa6dd8 100644 --- a/tests/conformance/native_integer_spills.luau +++ b/tests/conformance/native_integer_spills.luau @@ -372,6 +372,74 @@ FUNC_LIST[1] = function(loc_0, loc_1) end end +FUNC_LIST[2] = function(hashtable: buffer, entries: buffer, px: buffer, py: buffer, N: number, mult: number, size: number) + local function hash(x: number, y: number, mult: number, size: number) + local cx, cy = math.floor(x * mult), math.floor(y * mult) + local h = bit32.bxor((cx * 92837111), (cy * 689287499)) + return math.abs(h) % size; + end + + for i = 0, N-1, 8 do + local x0, x1, x2, x3, x4, x5, x6, x7 = + buffer.readf32(px, (i) * 4), + buffer.readf32(px, (i+1) * 4), + buffer.readf32(px, (i+2) * 4), + buffer.readf32(px, (i+3) * 4), + buffer.readf32(px, (i+4) * 4), + buffer.readf32(px, (i+5) * 4), + buffer.readf32(px, (i+6) * 4), + buffer.readf32(px, (i+7) * 4) + + local y0, y1, y2, y3, y4, y5, y6, y7 = + buffer.readf32(py, (i)) * 4, + buffer.readf32(py, (i+1) * 4), + buffer.readf32(py, (i+2) * 4), + buffer.readf32(py, (i+3) * 4), + buffer.readf32(py, (i+4) * 4), + buffer.readf32(py, (i+5) * 4), + buffer.readf32(py, (i+6) * 4), + buffer.readf32(py, (i+7) * 4) + + local h0, h1, h2, h3, h4, h5, h6, h7 = + hash(x0, y0, mult, size), + hash(x1, y1, mult, size), + hash(x2, y2, mult, size), + hash(x3, y3, mult, size), + hash(x4, y4, mult, size), + hash(x5, y5, mult, size), + hash(x6, y6, mult, size), + hash(x7, y7, mult, size) + + local vdecr0, vdecr1, vdecr2, vdecr3, vdecr4, vdecr5, vdecr6, vdecr7 = + buffer.readu32(hashtable, h0 * 4) - 1, + buffer.readu32(hashtable, h1 * 4) - 1, + buffer.readu32(hashtable, h2 * 4) - 1, + buffer.readu32(hashtable, h3 * 4) - 1, + buffer.readu32(hashtable, h4 * 4) - 1, + buffer.readu32(hashtable, h5 * 4) - 1, + buffer.readu32(hashtable, h6 * 4) - 1, + buffer.readu32(hashtable, h7 * 4) - 1 + + buffer.writeu32(entries, vdecr0 * 4, i) + buffer.writeu32(entries, vdecr1 * 4, i + 1) + buffer.writeu32(entries, vdecr2 * 4, i + 2) + buffer.writeu32(entries, vdecr3 * 4, i + 3) + buffer.writeu32(entries, vdecr4 * 4, i + 4) + buffer.writeu32(entries, vdecr5 * 4, i + 5) + buffer.writeu32(entries, vdecr6 * 4, i + 6) + buffer.writeu32(entries, vdecr7 * 4, i + 7) + + buffer.writeu32(hashtable, h0 * 4, vdecr0) + buffer.writeu32(hashtable, h1 * 4, vdecr1) + buffer.writeu32(hashtable, h2 * 4, vdecr2) + buffer.writeu32(hashtable, h3 * 4, vdecr3) + buffer.writeu32(hashtable, h4 * 4, vdecr4) + buffer.writeu32(hashtable, h5 * 4, vdecr5) + buffer.writeu32(hashtable, h6 * 4, vdecr6) + buffer.writeu32(hashtable, h7 * 4, vdecr7) + end +end + mem = buffer.create(1024 * 1024) return('OK') diff --git a/tests/conformance/native_userdata.luau b/tests/conformance/native_userdata.luau index b1b2a1033..2382748ed 100644 --- a/tests/conformance/native_userdata.luau +++ b/tests/conformance/native_userdata.luau @@ -39,4 +39,17 @@ end mu(vec2(0, 2)) +local v1: vertex = vertex(vector.create(1, 2, 3), vector.create(0.5, 0, 0.5), vec2(0.25, 0.5)) + +assert(v1.pos.X == 1 and v1.pos.Y == 2 and v1.pos.Z == 3) +assert(v1.normal.x == 0.5 and v1.normal.Y == 0 and v1.normal.Z == 0.5) +assert(v1.uv.X == 0.25) +assert(v1.uv.Y == 0.5) + +local function vertextest1(v: vertex) + return v.uv.X * v1.pos:Dot(v1.normal) +end + +assert(vertextest1(v1) == 0.5) + return 'OK'